Stop using ForkJoin for web application

Stop using ForkJoin for web application

# ForkJoin ?

ここで言う ForkJoin とは、非同期タスクを以下のように処理するシステム:

- CPU コア数以下のスレッドを持ち
- スレッドごとのタスクキューを持ち
- work stealing する

具体的には [ForkJoinPool (JVM)](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html) とか。

標準機能・ライブラリに ForkJoin 相当がない言語でも、
本稿の考え方自体は適用できることがあるだろう。
( 例えば node.js のようなイベントドリブンでも近い話はあるので )

# TL;DR

一時期の流行の影響があり騙されてしまうケースがあるが...

- ForkJoin、オメーは駄目だ (ほとんどの web アプリ用途では)
- 大抵の web アプリ用途では長所が活きない
- スレッド数が足りない
- `fairness` がないので、レイテンシの最悪値が青天井
- web アプリの並列処理では普通のスレッドプール + キューキングがベターなことが多い
- CPU コア数に関係なく単一のキューを使うのがポイント

※ web アプリ用途: 画面・API のレスポンスを返すまでの処理の中で並列処理をする用途の意

なお、CPU・メモリアクセス律速かつバッチ処理などでは ForkJoin 方式は有用 (例えば AI のミニバッチとか向いてそう、たぶん)。

1a18bf1e50d7d2bdfe52a6c9fceec244?s=128

saiya_moebius

February 21, 2019
Tweet

Transcript

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

  2. ForkJoin ? ここで言う ForkJoin とは、非同期タスクを以下のように処理するシステ ム: CPU コア数以下のスレッドを持ち スレッドごとのタスクキューを持ち work

    stealing する 具体的には ForkJoinPool (JVM) とか。 標準機能・ライブラリに ForkJoin 相当がない言語でも、 本稿の考え方自体は適用できることがあるだろう。 ( 例えば node.js のようなイベントドリブンでも近い話はあるので )
  3. TL;DR 一時期の流行の影響があり騙されてしまうケースがあるが... ForkJoin、オメーは駄目だ (ほとんどの web アプリ用途では) 大抵の web アプリ用途では長所が活きない スレッド数が足りない

    fairness がないので、レイテンシの最悪値が青天井 web アプリの並列処理では普通のスレッドプール + キューキングが ベターなことが多い CPU コア数に関係なく単一のキューを使うのがポイント ※ web アプリ用途: 画面・API のレスポンスを返すまでの処理の中で並列 処理をする用途の意 なお、CPU・メモリアクセス律速かつバッチ処理などでは ForkJoin 方式 は有用 (例えば AI のミニバッチとか向いてそう、たぶん)。
  4. ForkJoin のいいところ (ワークロード次第で) CPU の有効活用ができるのがメリット。 しかし、なぜそのようなメリットが得られうるのか?

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

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

  7. 並列処理におけるメモリ 並列処理のタスクは親子関係・パイプライン構造になることがある: 処理 A の出力を処理 B で使って... 処理 A と処理

    B を異なる CPU やコアで処理してしまうと、別のコアに 通信してキャッシュをもらいにいくかメモリと通信することになり CPU のサイクルがすごく無駄になる。 なので同じデータを使うタスクは同じ CPU コアで処理すると効率良い。
  8. ForkJoin におけるタスク割当て ForkJoin では、タスクが作ったタスクは同じコアのキューに入れる。 タスクとそのタスクの子タスクは同じデータを読み書きする可能性高 ↓ キャッシュヒット率向上 ↓ CPU の有効活用

    ※ 実際はこれ以外にも命令キャッシュや分岐予測なども絡んでくる
  9. CPU コアごとにタスクのキューを持つ意味 メモリアクセスの局所化以外にも、キューの排他制御が高速になる。 work stealing (後述)以外では単一のコアがキューを専有するので、 spin lock, biased lock

    といった手法が有効に機能し、 キュー操作に伴うロックのオーバーヘッドが小さくなる。 特に小さい非同期タスクを大量にさばく場合に有用な性質。
  10. しかし多くの web アプリではメリットが... DB アクセス、API コール、ログ書込み... 等の理由で I/O が発生する。 I/O

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

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

    コアごとの局所性は悪化するので、ForkJoin 方式のメリットがない。 ※ とはいえ、ForkJoin で作ってしまったシステムで緩和策としてスレッ ド数を引き上げるのは有効な回避策
  13. ForkJoin のヤバイところ: fairness がない 例: ほとんどのリクエストは 0.1 [ms] で返っているのに、謎の 10

    [sec] 超えが出たりすることがあって困る ForkJoin 方式では fairness (不平等にタスクを待たせない)性質は担保 されないため、このようなことは起き得る。
  14. ForkJoin のヤバイところ: fairness がない 各スレッドのキューに多少のタスクが定常的に存在する場合に... 1. 特定のスレッドが重い処理を掴む or 待ち状態になる 2.

    そのスレッドのキューにあるタスクは待たされる 3. 他のスレッドはキューにタスクがある限り work stealing しない 問題のスレッドのタスクを他スレッドが引き取ってくれない 4. 結果、問題のキューは進まないが、他のキューは消化される 5. しかも問題のスレッドのキューにも新規タスクが入ってしまう CPU コアの局所性を fairness より重視するため 結果、そのキューにいるタスクの待ち時間がひどいことになる 一部のタスクだけが不平等に待たされてしまっている。
  15. 一般的なキューイングを用いた場合の fairness CPU・スレッドごとにキューを作るのではなく、単純に単一のキューに タスクを積む方式ならば fairness を確保できる。 単一のキューを FIFO 順に処理するので、一部のタスクだけが後回しに なることはない。

    ※ ミリ秒差で来たタスクが順序逆転したりする実装はあるが、しかし概 ね fair であることがほとんど 加えて、CPU コアの局所性にこだわらずに、余裕のあるスレッドを用意 すれば、数本のスレッドが詰まってもシステム全体の動作は継続する。
  16. まとめ: 今回言及したナレッジ メモリキャッシュや各種のロックにアクセスに局所性があると CPU の有効活用にはつながる レイテンシの大小関係: CPU <<< メモリアクセス <<<<<<

    I/O
  17. まとめ: 今回言及した観点 アプリケーション全体としてのボトルネック要因を意識しよう 処理速度と fairness どちらがサービスとして重要か考えよう マイクロベンチマークは前者を測っていることがほとんど 大抵の web アプリ用途でどうするべきか

    普通のスレッドプール(単一のキュー)を使いましょう 並行実行したい数に応じた数のワーカースレッドを用意しましょう フレームワーク類のデフォルト値が CPU コア数になっている ことがあるので要注意 ※ AI のミニバッチのようなケースでは、CPU利用効率・スループットが 大事なので ForkJoin がいい、という結論もありえる