スレッドプールのコアスレッド数の計算方法
スレッドプールのコアスレッド数をいくつに設定すべきかは、プログラムのワークロードによって決まります。一般に、計算集約型、IO 集約型、混合型の 3 つのシナリオに分けられます。それぞれ見ていきましょう。
計算集約型
大量の計算を必要とし、CPU 使用率が高く、CPU Loading は 90〜100% になります。CPU が I/O(ディスク/メモリ)の読み書きを行うことはあっても、それらの I/O はごく短い時間で完了し、大部分は CPU が大量のデータ演算や数学的計算を行っています。例えばデータ分析やストリーム処理などで、この種のプログラムの実行中は CPU 使用率が一般に非常に高くなります。
仮にシングルコア CPU の場合、スレッドプールに 6 つのスレッドがあっても、シングルコアなので同時に実行できるスレッドは 1 つだけです。スレッド間のコンテキストスイッチの時間コストを考えると、単一スレッドで実行するほうがむしろ効率的です。つまり、シングルコア CPU で計算集約型プログラムを処理するなら、マルチスレッドは使わないほうがよいのです。
仮に 6 コアの CPU で 6 スレッドを設定すれば、理論上は実行速度が 6 倍になります(実際にはそこまで届きません。マルチスレッド間には並行処理や最適化すべき点があるためです)。各スレッドに実行用の CPU が割り当てられ、CPU タイムスライスの待ちが発生せず、スレッド切り替えのオーバーヘッドもありません。マルチコア CPU こそ計算集約型プログラムの処理に適しており、その間スレッドのコンテキストスイッチも発生しない可能性があります(1 コアに 1 スレッドを対応させ、通常は CPU 数 + 1 を超えない設定にします)。
IO 集約型
計算集約型とは逆に、ディスクやネットワーク上の IO の処理が中心で、CPU の占有率は高くありません。大部分の時間は CPU が IO の応答を待っており、待っている間スレッドはブロックされ、CPU はアイドル状態です。負荷はむしろディスクやネットワークの転送効率にかかっています。このシナリオで単純に CPU コア数 × 2 でスレッド数を設定するのは実は厳密さに欠けます。IO 集約型には当てはめられる公式があります。例えば Web アプリケーションのバックエンドでは、データベースやキャッシュへの CRUD が主な場面で、多くの API の所要時間はディスクやネットワークの IO に費やされています。そういう場合、スレッドプールのコアスレッド数を設定するときは、サーバーに割り当てられた CPU リソースに基づいて公式を当てはめればよいのです。
『Java Concurrency in Practice(Java 並行処理プログラミング)』にある計算公式:
Nthreads = Ncpu × Ucpu × (1 + W/C)
- Ncpu:CPU コア数
- Ucpu:CPU 使用率
- W/C:待ち時間 / 計算時間
公式から期待されるスレッド数を導くことはできますが、実際のプログラムでは、正確な待ち時間と計算時間を得るのは一般に非常に困難です。プログラムは複雑で、「計算」だけではないからです。一つのコードの中には、メモリの読み書き、計算、I/O などの複合的な操作が数多く含まれており、この 2 つの指標を正確に取得するのは難しい。そのため公式だけでスレッド数を計算するのは理想論に過ぎます。とはいえ、これをベースにして、さらに負荷テストを通じてコアスレッド数を具体的に調整すれば、最高の効率という期待値に到達できます。
混合型
アプリケーションの中には、データの計算処理をしながら、同時にディスクやネットワークのデータ転送も必要とするものがあります。このようなプログラムでは、どうスレッドプールを構成すれば最高の性能が得られるでしょうか。一般的には、サーバーの CPU コア数に基づいて分割し、2 つのスレッドプールを作成します。一方は計算部分に、もう一方は IO 部分に対応させる構成が合理的です。ネット上には別の方式もあります:
核心线程数 = (线程等待时间 / 线程 CPU 时间 + 1) × CPU 核心数
実際のプログラムにおけるスレッド数
では実際のプログラム、つまり Java の業務システムでは、スレッド数(スレッドプールのサイズ)をどれくらいに計画するのが適切でしょうか。
先に結論を言うと、決まった答えはありません。まず期待値を設定します。例えば期待する CPU 使用率、負荷、GC 頻度などの指標です。その後、公式でコアスレッド数を設定し、テストを通じて合理的なスレッド数へと調整し続けます。
公式で出せるのはおおよその値だけで、実際には妨害要因が非常に多く、特にサーバーの計算リソースの奪い合いが問題になります。アプリケーションがサーバー上で直接動いていて他のプログラムの干渉がなければ、負荷テストと調整を繰り返すことで適切なスレッド数を見つけられます。しかし現在、多くのアプリケーションはすでにコンテナ化されており、K8S 環境ではコンテナが異なる worker ノードに分散しています。1 つの worker ノードで多くのコンテナが動いていることもあり、コンテナにリソース制限をかけないと、計算リソースの奪い合いが容易に発生します。そのため各コンテナの CPU、メモリ、ディスク、ネットワークなどのリソース制限をきちんと設定する必要があります。さもないと実行効率が期待を大きく下回る可能性があります(計算リソースが他のコンテナに奪われてしまうためです)。
まとめ
スレッドプールのコアスレッド数に万能の数値はありません。計算集約型は CPU コア数に沿って設定し、IO 集約型は Nthreads = Ncpu × Ucpu × (1 + W/C) で見積もり、混合型は計算と IO で 2 つのスレッドプールに分割することを検討します。公式が与えるのは出発点に過ぎず、待ち時間と計算時間は実際のプログラムでは正確に測定しにくく、さらにコンテナ化環境でのリソース奪い合いの問題も重なります。したがって合理的なやり方は、まず公式で初期値を決め、CPU 使用率や負荷などの期待指標を設定したうえで、負荷テストによって期待に合致するまで段階的に調整していくことです。
COMMENTS