序文
ご存知のとおり、コードはプロジェクトの中核であり、小さなコードがプロジェクト全体のエクスペリエンスに影響を与える可能性があります。成長から成熟までの0から1までのプロジェクトは、コードを注意深く磨くことと切り離せません。詳細は成功または失敗を決定し、これはまさに優れたオープンソースプロジェクトです。この記事では、ShardingSphere 5.1.0のパフォーマンスの向上を例として取り上げ、コードの詳細によってもたらされる究極のエクスペリエンスとその方法を示します。コードを飛躍させます。
Wu Weijie、SphereExインフラストラクチャR&Dエンジニア、ApacheShardingSphereコミッター。現在、ApacheShardingSphereとそのサブプロジェクトElasticJobの研究開発に焦点を当てています。
コンテンツを最適化する
オプションの使用方法を修正する
Java 8で導入され、直接メソッドの戻りを回避するなど、コードをより洗練されたものにすることができます。より一般的に使用される方法は2つあります。 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
orElse
orElse
Optional<OrderByContext> result = // 省略代码...
return result.orElseGet(() -> getDefaultOrderByContextWithoutOrderBy(groupByContext));
ラムダを使用してラムダを提供し、結果が空の場合にのみ内部のメソッドが呼び出されるようにします。 Supplier
orElseGet
orElseGet
関連するPR:https ://github.com/apache/shardingsphere/pull/11459/files
Java8ConcurrentHashMapのcomputeIfAbsentへの高頻度の同時呼び出しを回避します
java.util.concurrent.ConcurrentHashMap
これは、並行シナリオで一般的に使用される一種のマップです。すべての操作と比較して、スレッドセーフを確保しながらパフォーマンスを向上させます。ただし、Java 8の実装では、キーが存在する場合でも値はコードブロックで取得されるため、同じキーを頻繁に呼び出す場合の同時実行性のパフォーマンスに大きく影響します。 synchronized
java.util.Hashtable
ConcurrentHashMap
ConcurrentHashMap
computeIfAbsent
synchronized
computeIfAbsent
参考:https://bugs.openjdk.java.net/browse/JDK-8161372
この問題はJava9で解決されましたが、Java 8での同時パフォーマンスを確保するために、この問題を回避するためにShardingSphereコードの書き込み方法を調整しました。
例として、頻繁に呼び出されるShardingSphereのクラスを取り上げます。 org.apache.shardingsphere.infra.executor.sql.prepare.driver.DriverExecutionPrepareEngine
// 省略部分代码...
private static final Map<String, SQLExecutionUnitBuilder> TYPETOBUILDERMAP = 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 = TYPETOBUILDER_MAP.computeIfAbsent(type,
key -> TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder.class, key, new Properties()));
}
上記のコードには2つのタイプしかなく、このコードはほとんどのSQL実行の唯一の方法です。つまり、メソッドは同じキーで同時に頻繁に呼び出され、同時実行のパフォーマンスが制限されます。この問題を次のように回避します。 computeIfAbsent
type
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:https ://github.com/apache/shardingsphere/pull/13275/files
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:https ://github.com/apache/shardingsphere/pull/13282/files
Collections.synchronizedMapを避けてください
ShardingSphereのMonitorBlockedをチェックする過程で、このクラスでは高周波読み取りで装飾されたMapが使用されていることがわかりました。これは、同時パフォーマンスに影響を与えます。分析後、変更されたマップには初期化フェーズでの変更操作のみが含まれ、後続の操作はすべて読み取り操作になります。変更メソッドを直接削除できます。 org.apache.shardingsphere.infra.metadata.schema.model.TableMetaData
Collections.synchronizedMap
Collections.synchronizedMap
関係PR:https ://github.com/apache/shardingsphere/pull/13264/files
不要な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
連結文字列を使用するオーバーヘッドは、連結文字列を使用するオーバーヘッドよりも大きく、直接連結文字列のパフォーマンスはJava9以降最適化されていることがわかります。これは、適切な文字列連結方法を選択することの重要性を示しています。 String.format
+
関連するPR:https ://github.com/apache/shardingsphere/pull/11291/files
高周波ストリームの代わりにfor-eachを使用
ShadingSphere5.xコードはより多くを使用しました。 java.util.stream.Stream
ShardingSphere-JDBC + openGaussのストレステストを行ったBenchmarkSQL(TPC-CテストのJava実装)のパフォーマンステストで、ストレステスト中に見つかったすべての高周波ストリームをfor-each、ShardingSphere-パフォーマンスに置き換えた後、 JDBCの改善は明らかです。
*注: ShardingSphere-JDBCとopenGaussは、それぞれ2台の128コアaarch64マシンでBishengJDK8を使用します。
上記のテスト結果は、aarch64プラットフォームとJDKにも関連している可能性があります。ただし、ストリーム自体には一定のオーバーヘッドがあり、パフォーマンスはシナリオによって大きく異なります。頻繁に呼び出され、ストリームがパフォーマンスを最適化できるかどうかわからないロジックの場合は、最初にfor-eachループを使用することを検討します。
関連するPR:https ://github.com/apache/shardingsphere/pull/13845/files
不要な論理(繰り返し)呼び出しを避ける
ロジックの不必要な繰り返し呼び出しを避けるために多くの場合があります:
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());
}
}
明らかに、上記のクラスは不変ですが、メソッドの実装では、メソッドの計算が毎回呼び出されます。このオブジェクトがMapまたはSetで頻繁にアクセスされる場合、多くの不要な計算オーバーヘッドが発生します。 hashCode
hashCode
調整済み:
@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:https ://github.com/apache/shardingsphere/pull/11760/files
リフレクションの代わりにラムダを使用してメソッドを呼び出す
ShardingSphereソースコードには、メソッドとパラメーターの呼び出しを記録し、必要に応じて指定されたオブジェクトへのメソッド呼び出しを再生する必要がある次のシナリオがあります。
-
beginなどのステートメントをShardingSphere-Proxyに送信します。
-
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:
https://github.com/apache/shardingsphere/pull/10466/files
https://github.com/apache/shardingsphere/pull/11415/files
aarch64のNettyEpollサポート
NettyのEpollは、aarch64アーキテクチャをサポートするLinux環境に実装されています。aarch64 Linux環境では、Netty Epoll APIを使用すると、NettyNIOAPIと比較してパフォーマンスを向上させることができます。 4.1.50.Final
参考:https://stackoverflow.com/a/23465481/7913731
5.1.0および5.0.0ShardingSphere-ProxyTPC-Cパフォーマンステストの比較
TPC-Cを使用してShardingSphere-Proxyのベンチマークを行い、パフォーマンスの最適化の結果を検証します。以前のバージョンのShardingSphere-ProxyではPostgreSQLのサポートが制限されているため、TPC-Cテストを実行できません。そのため、バージョン5.0.0と5.1.0を比較に使用します。
ShardingSphere-Proxy自体のパフォーマンスの低下を強調するために、このテストでは、データシャーディング(1シャード)を備えたShardingSphere-Proxyを使用して、PostgreSQL14.2と比較します。
テストは、公式ドキュメントの「BenchmarkSQL Performance Test( https://shardingsphere.apache.org/document/current/cn/reference/test/performance-test/benchmarksql-test/ )」と構成に従って実行されます。 4スライスから1シャードに減少します。
テスト環境
テストパラメータ
BenchmarkSQLパラメーター:
- ウェアハウス=192(データ量)
- terminals = 192(同時実行数)
- terminalWarehouseFixed = false
- 実行時間30分
PostgreSQL JDBCパラメーター:
- defaultRowFetchSize = 50
- reWriteBatchedInserts = true
ShardingSphere-プロキシ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%向上します。
- ShardingSphere-Proxy 5.1.0は、PostgreSQLへの直接接続をベンチマークとして、損失を5.0.0と比較して約15%削減し、42.7%から27.4%に削減します。
コードの詳細はShardingSphereの各モジュール全体で最適化されているため、上記のテスト結果はすべての最適化ポイントを網羅しているわけではありません。
パフォーマンスの問題を表示する方法
時々、誰かが「ShardingSphereのパフォーマンスはどうですか?損失はいくらですか?」と尋ねることがあります。
私の意見では、パフォーマンスはニーズを満たすことができます。パフォーマンスは複雑な問題であり、多くの要因の影響を受けます。さまざまな環境とシナリオでは、ShardingSphereのパフォーマンスの低下は1%未満または50%に達する可能性があります。環境とシナリオがなければ、答えを出すことはできません。さらに、ShardingSphereのインフラストラクチャとして、そのパフォーマンスは研究開発プロセスにおける重要な考慮事項の1つです。ShardingSphereコミュニティのチームと個人は、ShardingSphereのパフォーマンスを極限まで高めるために、引き続き職人技を発揮します。