乾貨|效能提升金鑰,由程式碼細節帶來的極致體驗
前言
眾所周知,程式碼是專案的核心所在,一段小小的程式碼可能會影響到整個專案的體驗。一個專案從 0 到 1,從成長到成熟,離不開程式碼的精心打磨。細節決定成敗,一個優秀的開源專案也正是如此,本篇乾貨經驗貼,將以 ShardingSphere 5.1.0 效能提升為例,帶大家感受程式碼細節帶來的極致體驗,如何在程式碼上實現飛躍。
吳偉傑,SphereEx 基礎設施研發工程師,Apache ShardingSphere Committer。目前專注於 Apache ShardingSphere 及其子專案 ElasticJob 的研發。
最佳化內容
更正 Optional 的使用方式
Java 8 引入的
java.util.Optional
能夠讓程式碼更加優雅,例如避免方法直接返回
null
。其中
Optional
有兩個比較常用的方法:
public T
orElse(
T other) {
return
value !=
null ?
value : other;
}
public T
orElseGet(
Supplier<? extends T> other) {
return
value !=
null ?
value : other.
get();
}
在 ShardingSphere 的類
org.apache.shardingsphere.infra.binder.segment.select.orderby.engine.OrderByContextEngine
中有這麼一段使用了
Optional
的程式碼:
Optional<OrderByContext> result =
// 省略程式碼...
return result.orElse(getDefaultOrderByContextWithoutOrderBy(groupByContext));
以上這種使用
orElse
的寫法,即使 result 的結果不為空,
orElse
裡面的方法也會被呼叫,尤其是
orElse
裡面的方法涉及修改操作時,可能會發生意料之外的事情。涉及方法呼叫的情況下應調整為下面的寫法:
Optional<OrderByContext> result =
// 省略程式碼...
return result.orElseGet(
() -> getDefaultOrderByContextWithoutOrderBy(groupByContext));
使用 lambda 提供一個
Supplier
給
orElseGet
,這樣只有 result 為空的時候才會呼叫
orElseGet
裡面的方法。
相關 PR:
避免高頻併發呼叫 Java 8 ConcurrentHashMap 的 computeIfAbsent
java.util.concurrent.ConcurrentHashMap
是我們在併發場景下比較常用的一種 Map,相比對所有操作以
synchronized
修飾的
java.util.Hashtable
,
ConcurrentHashMap
在保證執行緒安全的情況下提供了更好的效能。但在 Java 8 的實現中,
ConcurrentHashMap
的
computeIfAbsent
在 key 存在的情況下,仍然會在
synchronized
程式碼塊中獲取 value,在對同一個 key 高頻呼叫
computeIfAbsent
的情況下非常影響併發效能。
參考:
這個問題在 Java 9 解決了,但為了在 Java 8 上也能保證併發效能,我們在 ShardingSphere 的程式碼中調整寫法規避這一問題。
以 ShardingSphere 的一個高頻呼叫的類
org.apache.shardingsphere.infra.executor.sql.prepare.driver.DriverExecutionPrepareEngine
為例:
// 省略部分程式碼...
private
static
final
Map<
String, SQLExecutionUnitBuilder> TYPE
TOBUILDER
MAP =
new ConcurrentHashMap<>(
8,
1);
// 省略部分程式碼...
public DriverExecutionPrepareEngine(
final <span class="hljs-builtin" style="font-size: inherit; line-height: inherit; margin: 0px; padding: 0px; color: rgb(170, 87, 60); word-wrap: inherit !important; word-break: inherit !important;">String type,
final
int maxConnectionsSizePerQuery,
final ExecutorDriverManager<C, ?, ?> executorDriverManager,
final StorageResourceOption option,
final Collection<ShardingSphereRule> rules) {
super(maxConnectionsSizePerQuery, rules);
this.executorDriverManager = executorDriverManager;
this.option = option;
sqlExecutionUnitBuilder = TYPE
TOBUILDER_MAP.computeIfAbsent(type,
key -> TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder.
class, key,
new Properties()));
}
以上程式碼傳入
computeIfAbsent
的
type
只有 2 種,而且這段程式碼是大部分 SQL 執行的必經之路,也就是說會併發高頻地對相同 key 呼叫
computeIfAbsent
方法,導致併發效能受限。我們採用如下方式規避這一問題:
SQLExecutionUnitBuilder result;
if (
null == (result = TYPE_TO_BUILDER_MAP.
get(type))) {
result = TYPE_TO_BUILDER_MAP.computeIfAbsent(type, key -> TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder.
class, key,
new Properties()));
}
return result;
相關 PR:
避免高頻呼叫 java.util.Properties
java.util.Properties
是 ShardingSphere 在配置方面比較常用的一個類,
Properties
繼承了
java.util.Hashtable
,因此要避免在併發情況下高頻呼叫
Properties
的方法。
我們排查到 ShardingSphere 與資料分片演算法有關的類
org.apache.shardingsphere.sharding.algorithm.sharding.inline.InlineShardingAlgorithm
中存在高頻呼叫
getProperty
的邏輯,導致併發效能受限。我們的處理方式為:將涉及
Properties
方法呼叫的邏輯放在
InlineShardingAlgorithm
的
init
方法內完成,避免在分片演算法計算邏輯的併發效能。
相關 PR:
避免使用 Collections.synchronizedMap
在排查 ShardingSphere 的 Monitor Blocked 過程中,發現在
org.apache.shardingsphere.infra.metadata.schema.model.TableMetaData
這個類中使用了
Collections.synchronizedMap
修飾會被高頻讀取的 Map,影響併發效能。經過分析,被修飾的 Map 只會在初始化階段有修改操作,後續都是讀取操作,我們直接移除
Collections.synchronizedMap
修飾方法即可。
相關 PR:
字串拼接代替不必要的 String.format
在 ShardingSphere 的類
org.apache.shardingsphere.sql.parser.sql.common.constant.QuoteCharacter
有這麼一段邏輯:
public String
wrap(
final String
value) {
return String.format(
"%s%s%s", startDelimiter,
value, endDelimiter);
}
顯然上面的邏輯就是做一個字串拼接,但使用
String.format
的方式相比直接字串拼接的開銷會更大。我們修改成以下方式:
public String
wrap(
final String
value) {
return startDelimiter +
value + endDelimiter;
}
我們用 JMH 做一個簡單的測試,測試結果:
# JMH version: 1.33
# VM version: JDK 17.0.1, Java HotSpot(TM) 64-Bit Server VM, 17.0.1+12-LTS-39
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 3 iterations, 5 s each
# Measurement: 3 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 16 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
Benchmark Mode Cnt Score Error Units
StringConcatBenchmark.benchFormat thrpt
9
28490416.
644 ±
1377409.
528 ops/s
StringConcatBenchmark.benchPlus thrpt
9
163475708.
153 ±
1748461.
858 ops/s
可以看出,使用
String.format
相比使用
+
拼接字串的開銷會更大,且自 Java 9 起最佳化了直接拼接字串的效能。由此可見選擇合適的字串拼接方式的重要性。
相關 PR:
使用 for-each 代替高頻 stream
ShadingSphere 5.x 程式碼中使用了較多的
java.util.stream.Stream
。
在我們之前做的一次 BenchmarkSQL(TPC-C 測試的 Java 實現) 壓測 ShardingSphere-JDBC + openGauss 的效能測試中,我們發現將壓測過程中發現的所有高頻 stream 替換為 for-each 後,ShardingSphere-JDBC 的效能提升明顯。
* 注:ShardingSphere-JDBC 與 openGauss 分別在 2 臺 128 核 aarch64 的機器上,使用畢昇 JDK 8。
以上測試結果也可能和 aarch64 平臺及 JDK 有關。不過 stream 本身存在一定開銷,效能在不同場景下差異較大,對於高頻呼叫且不確定 stream 能夠最佳化效能的邏輯,我們考慮優先使用 for-each 迴圈。
相關 PR:
避免不必要的邏輯(重複)呼叫
避免不必要的邏輯重複呼叫有很多案例:
hashCode 計算
ShardingSphere 有個類
org.apache.shardingsphere.sharding.route.engine.condition.Column
實現了
equals
和
hashCode
方法:
@RequiredArgsConstructor
@Getter
@ToString
public
final
class
Column {
private
final String name;
private
final String tableName;
@Override
public
boolean
equals
(
final Object obj) {...}
@Override
public
int
hashCode
() {
return Objects.hashCode(name.toUpperCase(), tableName.toUpperCase());
}
}
顯而易見,上面這個類是不可變的,但是卻在
hashCode
方法的實現中每次都呼叫方法計算
hashCode
。如果這個物件頻繁在 Map 或者 Set 中存取,就會多出很多不必要的計算開銷。
調整後:
@Getter
@ToString
public
final
class
Column {
private
final String name;
private
final String tableName;
private
final
int hashCode;
public
Column
(
final String name,
final String tableName) {
this.name = name;
this.tableName = tableName;
hashCode = Objects.hash(name.toUpperCase(), tableName.toUpperCase());
}
@Override
public
boolean
equals
(
final Object obj) {...}
@Override
public
int
hashCode
() {
return hashCode;
}
}
相關 PR:
使用 lambda 代替反射呼叫方法
在 ShardingSphere 原始碼中,有以下場景需要記錄方法及引數呼叫,並在需要的時候對指定物件重放方法呼叫:
-
向 ShardingSphere-Proxy 傳送 begin 等語句;
-
使用 ShardingSpherePreparedStatement 為指定位置的佔位符設定引數。
以如下程式碼為例,重構前,使用反射的方式記錄方法呼叫及重放,反射呼叫方法本身存在一定的效能開銷,且程式碼可讀性欠佳:
@Override
public
void
begin
() {
recordMethodInvocation(Connection.class,
"setAutoCommit",
new Class[]{
boolean.class},
new Object[]{
false});
}
重構後,避免了使用反射呼叫方法的開銷:
@Override
public
void
begin
() {
connection.getConnectionPostProcessors().add(target -> {
try {
target.setAutoCommit(
false);
}
catch (
final SQLException ex) {
throw
new RuntimeException(ex);
}
});
}
相關 PR:
Netty Epoll 對 aarch64 的支援
Netty 的 Epoll 實現自
4.1.50.Final
支援 aarch64 架構的 Linux 環境。在 aarch64 Linux 環境下,使用 Netty Epoll API 相比 Netty NIO API 能夠提升效能。
參考:
5.1.0 與 5.0.0 ShardingSphere-Proxy TPC-C 效能測試對比
我們使用 TPC-C 對 ShardingSphere-Proxy 進行基準測試,以驗證效能最佳化的成果。由於更早期版本的 ShardingSphere-Proxy 對 PostgreSQL 的支援有限,無法進行 TPC-C 測試,因此使用 5.0.0 與 5.1.0 版本對比。
為了突出 ShardingSphere-Proxy 本身的效能損耗,本次測試將使用資料分片(1 分片)的 ShardingSphere-Proxy 對比 PostgreSQL 14.2。
測試按照官方文件中的《BenchmarkSQL 效能測試( https://shardingsphere.apache.org/document/current/cn/reference/test/performance-test/benchmarksql-test/)》進行,配置由 4 分片縮減為 1 分片。
測試環境
測試引數
BenchmarkSQL 引數:
- warehouses=192 (資料量)
- terminals=192 (併發數)
- terminalWarehouseFixed=false
- 執行時間 30 mins
PostgreSQL JDBC 引數:
- defaultRowFetchSize=50
- reWriteBatchedInserts=true
ShardingSphere-Proxy JVM 部分引數:
- -Xmx16g
- -Xms16g
- -Xmn12g
- -XX:AutoBoxCacheMax=4096
- -XX:+UseNUMA
- -XX:+DisableExplicitGC
- -XX:LargePageSizeInBytes=128m
- -XX:+SegmentedCodeCache
- -XX:+AggressiveHeap
測試結果
在本文的環境與場景中所得到的結論:
- 以 ShardingSphere-Proxy 5.0.0 + PostgreSQL 為基準,5.1.0 效能提升約 26.8%。
- 以直連 PostgreSQL 為基準,ShardingSphere-Proxy 5.1.0 相比 5.0.0 損耗減少了約 15%,由 42.7% 降低至 27.4%。
由於程式碼細節最佳化遍佈 ShardingSphere 各模組,以上測試結果並未覆蓋所有最佳化點。
如何看待效能問題
可能不時會有人問,“ShardingSphere 效能怎麼樣?損耗多少?”
在我看來,效能能夠滿足需求即可。效能是一個比較複雜的問題,受非常多的因素影響。在不同的環境、場景下,ShardingSphere 的效能損耗有可能不到 1%,也有可能高達 50%,我們無法在脫離環境和場景的情況下給出答案。此外,ShardingSphere 作為基礎設施,其效能是研發過程中重點考慮的因素之一,ShardingSphere 社群中的團隊、個人也會持續發揮工匠精神,不斷地將 ShardingSphere 的效能推向極致。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70001955/viewspace-2869363/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 從零入門 Serverless | Knative 帶來的極致 Serverless 體驗Server
- 乾貨|九九互動的資料效能提升之路
- 10個細節提升後臺系統的使用者體驗
- 乾貨 | APP介面設計的色彩注意細節,有哪些?APP
- 超級乾貨:帶你全面瞭解低程式碼!
- 3倍+提升,高德地圖極致效能優化之路地圖優化
- Spring Security 實戰乾貨:AuthenticationManager的初始化細節Spring
- git生成ssh金鑰詳細步驟 git如何生成ssh金鑰Git
- mongodb網路傳輸處理原始碼實現及效能調優-體驗核心效能極致設計MongoDB原始碼
- 3倍+提升,高德地圖極致效能最佳化之路地圖
- 乾貨分享 | 阿里專家親授如何提升研發效能阿里
- 談談程式碼效能優化中的一些小細節優化
- SQL效能優化的祕訣,快來圍觀,乾貨!SQL優化
- 低程式碼如何快速提升客戶體驗
- git如何生成ssh金鑰 git生成配置ssh金鑰key詳細步驟Git
- 展現無 限創意,Luxion KeyShot 2023為您帶來極 致的3D渲染體驗!UX3D
- 讓Elasticsearch飛起來!——效能優化實踐乾貨Elasticsearch優化
- 讓 Elasticsearch 飛起來!——效能優化實踐乾貨Elasticsearch優化
- 【乾貨】驗證碼的常見型別總結型別
- 帶來高價值使用者體驗的低程式碼開發平臺
- 乾貨 | 揭秘京東數科強一致、高效能的分散式事務中介軟體JDTX分散式
- 金鑰,私鑰,公鑰的區分
- SourceTree生成SSH金鑰(拉取GitLab程式碼)Gitlab
- 開箱即用,Knative 給您極致的容器 Serverless 體驗Server
- 【乾貨】遊戲介面設計 (四)體驗設計遊戲
- Sensei for Mac:提升Mac效能的終極利器Mac
- 提高javascript效能的小細節JavaScript
- kmdjs整合uglifyjs2打造極致的程式設計體驗JS程式設計
- 10個CSS技巧,極大提升使用者體驗CSS
- 開箱即用的JS乾貨助力金三銀四JS
- 體驗高畫質媒體的極 致品質——Movist Pro for MacMac
- 乾貨|如何做有效的程式碼走查
- 乾貨| 關於程式碼對齊的探討
- 乾貨!谷歌推薦的技術能力提升指南谷歌
- 「事件管理」如何讓使用者體驗更加極致?事件
- 提升網站使用者體驗 讓GoDaddy幫你完善這幾個小細節!網站Go
- 【乾貨】個人工作文件節選:XAML MVVM 框架易用性細節優化TipsMVVM框架優化
- bitlocker如何恢復金鑰 bitlocker恢復金鑰的方法