I.はじめに
毎日0時にバッチ処理で日次テーブルを生成するデータミラーリングシステムがあるのですが、データ量が増加し続けるため、処理時間が以前は1時間だったのが、1日あたり2時間近くまでかかるようになりました。 、BI や在庫などの多くのタスクが影響を受けます。リーダーはブロガーの最適化とスピードアップを可能にします。
ブロガーは、GC コレクター、カウント可能なサイクルの安全なポイントの配置、CPU とスレッドのコロケーションなど、多方向から分析して実践します。
2. スピードアップ方向
まずはコードを見てみましょう
List<A> as = mapper.get(queryDTO, i);
if (CollectionUtils.isEmpty(as )) {
break;
}
/**------------**/
CountDownLatch latch;
if (as .size() < paramConfig.getInsertBatchQuantity()) {
latch = new CountDownLatch(1);
} else if (as .size() % paramConfig.getInsertBatchQuantity() == 0) {
latch = new CountDownLatch(assetCollects.size() / paramConfig.getInsertBatchQuantity());
} else {
latch = new CountDownLatch(as .size() / paramConfig.getInsertBatchQuantity() + 1);
}
Lists.partition(as , paramConfig.getInsertBatchQuantity()).forEach(a -> {
Runnable runnable = () -> {
try {
log.info("线程" + Thread.currentThread().getName() + "开始执行");
mapper.batchSave(a, tableName);
log.info("线程" + Thread.currentThread().getName() + "执行完成");
} catch (Exception e) {
log.error("数据镜像执行异常", e);
}
finally {
latch.countDown();
}
};
dbInsertExecutorService.execute(runnable);
});
latch.await();
データのチェックと日次テーブルへの挿入を同時に実行します。元の同時実行数はクエリ/挿入の数に基づいています。挿入時には、シンクロナイザー CountDownLatch を使用して挿入が完了するのを待つ必要があります。
1、GC
最初はコードを触りたくなかったので、データクエリ文とgcから高速化できないか試してみましたが、クエリSQLは複雑ではなく言うことはありません。
収集頻度は約 60、マイナー gc は約 30ms、メジャー gc は約 200ms です。実際、それは理解できます。結局のところ、データは常にアクセスされており、参照チェーンはすぐに失われるため、集中的なリサイクルが行われるのが通常です。
それでは、GC stop the world の時間を短縮することは可能でしょうか? jvm の構成を見てください。
javaagent:/app/skywalking/agent/skywalking-agent.jar -jar -Djava.security.egd=file:/dev/./urandom -server -Xms3998m -Xmx3998m -XX:MetaspaceSize=200m -XX:+UseGCLogFileRotation -XX :NumberOfGCLogFiles=3 -XX:GCLogFileS
ize=10m -Xmn1999m -Xloggc:/dev/shm/gc_%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:SurvivorRatio=8 -XX: -UseAdaptiveSizePolicy -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/ /app/code/app.jar
ガベージ コレクターには特別な設定はありません。つまり、jdk8 のデフォルトの UseParallelGC です。このコレクターのペアは、収集プロセス中にワールドを停止する必要があります。現在、CMS と G1 は主に、それぞれインクリメンタル コレクションを介して同時収集に使用されています。更新方法と元のスナップショットにより、3 色地図マーキングの問題が解決され、同時収集が実現されます。
運用保守部門と連携して jvm 構成を変更し、G1 を使用して処理速度の向上を確認しましたが、結果は最初のステップでスタックし、運用保守部門は、システムのすべての jvm 構成が同じ Linux マウント パスから取得されると述べました。リリース イメージの生成 ファイルへの変更は他のシステムに影響を及ぼします。ブロガーは K8S 設定ファイルやリリース ミラーリング メカニズムについてあまり知らないため、反論することはできません。以前にも同じようなことがありましたが、これは無知の喪失です。以前は他にも調べることがたくさんありましたが、コンテナについて勉強する気力が湧いてきたので、ブロガーは密かに「K8S 徹底理解」を購入しており、後ですべての場所を探索する予定です。
2. 安全なポイント
コレクターは通過できません。他の解決策を見てみましょう。「Java 仮想マシンの詳細な理解」の中で、著者は、jvm が可算ループに安全なポイントを挿入しないと述べました。時間のかかる操作を行うと、GC 収集プロセス中にスレッドが安全なポイントに入るまで長時間待機する必要が生じます。
この場合、関数シンボルはこの状況を満たしません。ループの分割が可算ループであり、ループ本体がデータベースに挿入するスレッドを開くことであることがわかります。この場合、ネットワーク io+データベース io は、普通のコードなので書き換えて試してみましたが、最終結果は数十秒速くなっただけで、これも合格です。
3. CPUとスレッド
以前は、開発者はスレッド プールを使用してマルチスレッドを有効にしてから、シンクロナイザーを使用してプロセスをブロックしていました。ダウンタイムやその他の理由でキュー内のデータが失われるのが心配な場合は、シンクロナイザーを削除してみてはいかがでしょうか? 、その後、ダウンタイムは元々、テーブルを空にした後に再挿入する必要があるデザインはなぜですか?
ここでスレッド プールの構成を見てみましょう。ThreadPoolExecutor を使用して、カスタム スレッド プールを生成します。使用仕様に従って、有界キューを設定します。キューの数は 10 です。問題は拒否戦略にあります。デフォルトでは、拒否戦略は破棄して例外をスローする 前回の開発を推測する 担当者はスレッド プールの拒否戦略についてあまり知識がなく、他の戦略を構成することを考えていませんでした。
new ThreadPoolExecutor(poolProperties.getCorePoolSize(), poolProperties.getMaxPoolSize(),
poolProperties.getKeepAliveSeconds(), TimeUnit.SECONDS, new ArrayBlockingQueue<>(poolProperties.getMaxQueueSize()), factory);
ブロガーはスレッドプールの拒否ポリシーをメインスレッドの実行に設定しますが、これもブロガーが開発中によく使用するもので、どの企業もメッセージデータを失うことはできないと感じています。
new ThreadPoolExecutor(poolProperties.getCorePoolSize(), poolProperties.getMaxPoolSize(),
poolProperties.getKeepAliveSeconds(), TimeUnit.SECONDS, new ArrayBlockingQueue<>(poolProperties.getMaxQueueSize()), factory, new ThreadPoolExecutor.CallerRunsPolicy());
次にシンクロナイザーを取り外します
// CountDownLatch latch;
// if (as.size() < paramConfig.getInsertBatchQuantity()) {
// latch = new CountDownLatch(1);
// } else if (as.size() % paramConfig.getInsertBatchQuantity() == 0) {
// latch = new CountDownLatch(as.size() / paramConfig.getInsertBatchQuantity());
// } else {
// latch = new CountDownLatch(as.size() / paramConfig.getInsertBatchQuantity() + 1);
// }
Lists.partition(as, paramConfig.getInsertBatchQuantity()).forEach(a-> {
Runnable runnable = () -> {
try {
log.info("线程" + Thread.currentThread().getName() + "开始执行");
assetService.batchSave(a, tableName);
log.info("线程" + Thread.currentThread().getName() + "执行完成");
} catch (Exception e) {
log.error("数据镜像执行异常", e);
}
// finally {
// latch.countDown();
// }
};
dbInsertExecutorService.execute(runnable);
});
// latch.await();
スレッド プールは 15 に調整されましたが、代わりに速度が少し低下しました。よく考えてみると、これは IO 集中型 (ネットワーク IO+データベース IO) であり、スレッドには計算がないため、CPU は分割を実行するだけです。コンテキストの切り替えとデータベースの主キーの自動増加ロックにより、代わりに時間がかかる場合があります。
スレッドとキューを 6 に調整すると、速度が向上し、テスト環境が 10% 以上向上しました。
テスト環境の最適化までに時間がかかる
最適化後
もう一度オンラインで見てみる
オンライン最適化の前に
オンライン最適化後
オンライン速度が 50% 以上向上していることがわかります。
3. まとめ
最適化と高速化を実現するために、ブロガーはGCコレクター→可算サイクル安全点問題→スレッドプール調整、多次元分析から始め、学生が安全点問題などの原理をもっと理解できるようにしたいと考えています。ある程度の高精度で最適化をミリ秒レベルで高速化する可能性があり、コレクタに関連する JVM の知識は、多くの最適化や問題のトラブルシューティングに使用できます。「Java 仮想マシンの詳細な理解」を読むことをお勧めします。多くを得るでしょう。