blockingとOOM [scala]

ExecutionContextとblockingについて調べたメモ [scala] - だいたいよくわからないブログ の続きです。

今回の記事の結論

  • blockingをたくさん呼ぶとOOMになるまでスレッド数が増え続けるっぽい。 ==> 追記: scala2.12 だとlimitがあるので安心して使えるらしいです。 scala.concurrent.blocking() - Septeni Engineer's Blog
  • blockingはThreadFactoryをカスタマイズしないと有効にならないのでForkJoinPoolからExecutionContextを生成した場合などは単に無視されるため安全(ただ、スレッド数は増えないのでデッドロックなどには注意)
  • ExecutionContext.global、akkaのdispatcherなどをExecutionContextとして使うとblockingが有効になるため、注意が必要。(ExecutionContext.globalは手抜き用なので良いとしても、akkaの場合は注意したほうが良い。)
  • とはいえblocking自体は有用なので使いたいケースもありそう

前回の記事のまとめ

前回の記事ではblockingでブロックする処理を包むと自動的にスレッド数が増えるため高速に処理されて嬉しいよという話を書きました。加えて、それが有効になるのはExecutionContext.globalなどのScalaが内部で提供しているExecutionContextかakkaのdispatcherを利用した場合だという話も書きました。

blockingとOOM

なるほどーと思ってそのまま放置していたのですが、ふと実際にスレッド数見てみるかと思って下記のような要領で確認してみました。 ちょっと長いですが、下記の3種類のExecutionContextでblockingな処理を大量に呼び出してどうなるかをみています。

  1. ExecutionContext.global
  2. ExecutionContext.fromExecutorService(new ForkJoinPool(50))
  3. ExecutionContext.fromExecutorService(new ForkJoinPool(1000, new DefaultThreadFactory, uncaughtExceptionHandler, false) (BlockContext付きのThreadFactory入りのForkJoinPool)

gist.github.com

上記コードだとxms1G, xmx1Gで、1,3のケースでは 5000~10000回程度呼び出すとOOMになりました。 BlockContextがついていない2のケースでは時間はかかりますがしっかり実行してくれます。 今までblockingだとスレッド数は ManagedBlocker が良い感じにしてくれるという雑な理解をしていましたが、どうも普通に増えるだけっぽいです(?)(単に実験コードがわるいだけの可能性があるので注意が必要ですが、少なくともOOMになるケース自体はあり得るようです。)

そもそもBlockContextつきのThreadFactory自体がscala処理系は簡単に使う方法を外部に公開している訳ではないことからも考えると、blockingはExecutionCotext.globalのような、そこまでヘビーに使わないときに便利なもの・・?みたいな理解になりました。

akkaのdispatcher

前回の記事でakkaのdispatcherはBlockContextを自前で実装していることを述べました。実験として以下の様なコードでOOMになるかどうかを実験しました。

gist.github.com

試した結果、ちゃんと(?)OOMになったので、akkaでブロック処理を行う際はblockingを書かないようにするか、十分に注意(DBでのブロックなら障害時にスレッド数が増えすぎないか?などを考える)する必要がありそうです。

blockingが動かないとコードによってはデッドロックもあり得る・・?といった背景もあるようですが、いずれにせよakkaでblockする処理を行う際はちゃんと並列数を見積もって予め適切なサイズにExecutionContextを分離しておくというオーソドックスなやり方が一番良さそうだなと思った次第です。

openjdk

openjdkの実装を少し見るかーと思ったのですが、あたりは全体的なロジックが掴めてないので部分的に読むのは難しそうでした・・。 今度時間を書けて読みたいような読みたくないような・・・。

jdk7u-jdk/ForkJoinPool.java at f4d80957e89a19a29bb9f9807d2a28351ed7f7df · openjdk-mirror/jdk7u-jdk · GitHub

jdk7u-jdk/ForkJoinPool.java at f4d80957e89a19a29bb9f9807d2a28351ed7f7df · openjdk-mirror/jdk7u-jdk · GitHub

ただ、 no compensation needed, create a replacement というのがあるので必ず増えるわけではないかもしれません。

まとめ

OOMについて調べましたが今回の実験ではメモリを1GB程度と少なめにしているので実際にはもっと耐えてくれるはずです。ただ負荷が強いプロダクションなどに投入するときは注意しましょう。という認識になりました。 blockingが要るのか要らないのかは微妙なところですがスレッドが足りなくてデッドロックしちゃうような処理があるばあいはblockingしたほうが良さそうですねー。とはいえそしたらスレッドプール分ければいいのではという気持ちがあったりしますが、常に簡単に分割できるって感じでもないと思うので・・。逆にスレッド増え続けてしまうくらい処理が重たくなるならblockingしないほうが良い気がします。そもそもblockingで処理包むの忘れるから使えてない