Slide 1

Slide 1 text

Web アプリケーションで ForkJoin 使うのはやめてくれー または、web アプリケーションにおけるマルチスレッド 処理の勘所 Seiya Yazaki (@saiya_moebius)

Slide 2

Slide 2 text

ForkJoin ? ここで言う ForkJoin とは、非同期タスクを以下のように処理するシステ ム: CPU コア数以下のスレッドを持ち スレッドごとのタスクキューを持ち work stealing する 具体的には ForkJoinPool (JVM) とか。 標準機能・ライブラリに ForkJoin 相当がない言語でも、 本稿の考え方自体は適用できることがあるだろう。 ( 例えば node.js のようなイベントドリブンでも近い話はあるので )

Slide 3

Slide 3 text

TL;DR 一時期の流行の影響があり騙されてしまうケースがあるが... ForkJoin、オメーは駄目だ (ほとんどの web アプリ用途では) 大抵の web アプリ用途では長所が活きない スレッド数が足りない fairness がないので、レイテンシの最悪値が青天井 web アプリの並列処理では普通のスレッドプール + キューキングが ベターなことが多い CPU コア数に関係なく単一のキューを使うのがポイント ※ web アプリ用途: 画面・API のレスポンスを返すまでの処理の中で並列 処理をする用途の意 なお、CPU・メモリアクセス律速かつバッチ処理などでは ForkJoin 方式 は有用 (例えば AI のミニバッチとか向いてそう、たぶん)。

Slide 4

Slide 4 text

ForkJoin のいいところ (ワークロード次第で) CPU の有効活用ができるのがメリット。 しかし、なぜそのようなメリットが得られうるのか?

Slide 5

Slide 5 text

ForkJoin のアーキテクチャ CPU コア数かそれ以下のスレッドを持ち スレッドごとのタスクキューを持ち work stealing する 暇なスレッドが他のスレッドのキューからタスクを奪う

Slide 6

Slide 6 text

CPU から見たメモリ 遅い。メモリへのアクセスは 100 サイクルとか掛かる。 なので CPU はメモリアクセスをキャッシュしたりしているが、キャッシ ュはCPUのコアやダイごとに持っている。

Slide 7

Slide 7 text

並列処理におけるメモリ 並列処理のタスクは親子関係・パイプライン構造になることがある: 処理 A の出力を処理 B で使って... 処理 A と処理 B を異なる CPU やコアで処理してしまうと、別のコアに 通信してキャッシュをもらいにいくかメモリと通信することになり CPU のサイクルがすごく無駄になる。 なので同じデータを使うタスクは同じ CPU コアで処理すると効率良い。

Slide 8

Slide 8 text

ForkJoin におけるタスク割当て ForkJoin では、タスクが作ったタスクは同じコアのキューに入れる。 タスクとそのタスクの子タスクは同じデータを読み書きする可能性高 ↓ キャッシュヒット率向上 ↓ CPU の有効活用 ※ 実際はこれ以外にも命令キャッシュや分岐予測なども絡んでくる

Slide 9

Slide 9 text

CPU コアごとにタスクのキューを持つ意味 メモリアクセスの局所化以外にも、キューの排他制御が高速になる。 work stealing (後述)以外では単一のコアがキューを専有するので、 spin lock, biased lock といった手法が有効に機能し、 キュー操作に伴うロックのオーバーヘッドが小さくなる。 特に小さい非同期タスクを大量にさばく場合に有用な性質。

Slide 10

Slide 10 text

しかし多くの web アプリではメリットが... DB アクセス、API コール、ログ書込み... 等の理由で I/O が発生する。 I/O は、メモリアクセスよりも遥かに遅い。 I/O がある限り、ForkJoin で計算効率を上げても誤差未満の効果。 仮に CPU 利用効率が問題でも... ForkJoin のオーバーヘッドどころではなく重い処理が... HTTP プロトコルの読み書きコスト JSON などのパース・生成コスト TLS/SSL の処理コスト フレームワーク内部でなんか色々やるコスト 動的メソッド呼び出し, 実行時コード生成, Proxy, ... Garbage Collector, 参照カウント, メモリバリア, ... そもそもコードがイケてなくて処理のオーダーが爆発するパターン

Slide 11

Slide 11 text

ForkJoin の残念なところ: スレッド数の問題 ForkJoin の目的は CPU コアごとの局所性を高めることで CPU を有効活 用すること。 しかし web アプリでは CPU のフル活用はそもそも難しい。例: API コールや DB へのクエリの Blocking I/O すべての I/O が非同期化できていればいいが... ログ書き出しの待ち時間 標準出力でもファイル追記でも排他制御が発生 性質上、ログは非同期書き出ししにくい・したくない スパイク的に書き込みが発生しがちなので buffer も埋まりがち コネクションプールの獲得などのためのロック処理 GC による一時停止や、メモリバリアに起因するロック待ち

Slide 12

Slide 12 text

ForkJoin の残念なところ: スレッド数の問題 さまざまな理由で CPU が待ち状態になるのは無駄なので、コア数よりは 多くのスレッドを用意したほうが CPU 資源も活用できる。 しかしスレッド数を増やしてコンテキストスイッチすると コアごとの局所性は悪化するので、ForkJoin 方式のメリットがない。 ※ とはいえ、ForkJoin で作ってしまったシステムで緩和策としてスレッ ド数を引き上げるのは有効な回避策

Slide 13

Slide 13 text

ForkJoin のヤバイところ: fairness がない 例: ほとんどのリクエストは 0.1 [ms] で返っているのに、謎の 10 [sec] 超えが出たりすることがあって困る ForkJoin 方式では fairness (不平等にタスクを待たせない)性質は担保 されないため、このようなことは起き得る。

Slide 14

Slide 14 text

ForkJoin のヤバイところ: fairness がない 各スレッドのキューに多少のタスクが定常的に存在する場合に... 1. 特定のスレッドが重い処理を掴む or 待ち状態になる 2. そのスレッドのキューにあるタスクは待たされる 3. 他のスレッドはキューにタスクがある限り work stealing しない 問題のスレッドのタスクを他スレッドが引き取ってくれない 4. 結果、問題のキューは進まないが、他のキューは消化される 5. しかも問題のスレッドのキューにも新規タスクが入ってしまう CPU コアの局所性を fairness より重視するため 結果、そのキューにいるタスクの待ち時間がひどいことになる 一部のタスクだけが不平等に待たされてしまっている。

Slide 15

Slide 15 text

一般的なキューイングを用いた場合の fairness CPU・スレッドごとにキューを作るのではなく、単純に単一のキューに タスクを積む方式ならば fairness を確保できる。 単一のキューを FIFO 順に処理するので、一部のタスクだけが後回しに なることはない。 ※ ミリ秒差で来たタスクが順序逆転したりする実装はあるが、しかし概 ね fair であることがほとんど 加えて、CPU コアの局所性にこだわらずに、余裕のあるスレッドを用意 すれば、数本のスレッドが詰まってもシステム全体の動作は継続する。

Slide 16

Slide 16 text

まとめ: 今回言及したナレッジ メモリキャッシュや各種のロックにアクセスに局所性があると CPU の有効活用にはつながる レイテンシの大小関係: CPU <<< メモリアクセス <<<<<< I/O

Slide 17

Slide 17 text

まとめ: 今回言及した観点 アプリケーション全体としてのボトルネック要因を意識しよう 処理速度と fairness どちらがサービスとして重要か考えよう マイクロベンチマークは前者を測っていることがほとんど 大抵の web アプリ用途でどうするべきか 普通のスレッドプール(単一のキュー)を使いましょう 並行実行したい数に応じた数のワーカースレッドを用意しましょう フレームワーク類のデフォルト値が CPU コア数になっている ことがあるので要注意 ※ AI のミニバッチのようなケースでは、CPU利用効率・スループットが 大事なので ForkJoin がいい、という結論もありえる