はじめに
職場で遭遇したかなり苦しかったバグについて、具体的なコードでの言及は避けて説明する。
今回のバグに遭遇した状況は以下である。
- scala2.11.8
- DBアクセスにSlickを使用、ただしコネクションプールについては設定していない
遭遇したバグ
端的に言うとOOMErrorが発生した。
トレースバックは以下であった。
Oct 21 04:24:07 java[2171]: Caused by: java.lang.OutOfMemoryError: unable to create new native thread Oct 21 04:24:07 java[2171]: java.lang.OutOfMemoryError: unable to create new native thread ...
発生したバグについて
調べた限り、JVM系のランタイムでOOMErrorが発生することはあまり珍しくないようで、色々な解決策が見つかるが、その際に留意すべきことがいくつかある。
OOMErrorには種類があるということ
JVMについて多少知っている方ならわかると思うが、JVMにはいくつかのメモリ領域に分かれており、どこの領域で発生したものなのかで対応方法が変わってくる。
今回私が遭遇したOOMErrorは、どちらかというと少数派のOOMErrorで、メモリではなく生成できるスレッド数の限界に達したことで発生するものであった。
調査にはヒープダンプをとるのが最も簡単な方法
JVMの操作には様々なツールがあり、中にはGUIでそのJVMの状況をビジュアライズして視覚的に各パラメータの変動を確認できたりする。
今回の様なスレッドに関わるエラーの場合はjstack
のようなヒープダンプを取得できるCLIを活用するのが最も簡単である。デッドロックに関わるエラーであればおそらくこれで完全に解決できると思う。
結局の原因
原因の候補は以下2点あった。
- 内部では
await result
をネストして使用している箇所があったこと - Slickでコネクションプールを使用せずマルチスレッドで運用していたこと
おそらく前者がOOMErrorの原因であったと考えられるが、同時に修正したため真因は不明である。
await resultのネストの危険性
正直いうと、色々調べたが確実な原因はわかっていない。
await resultはblocking、つまり指定された処理が終了するまで次の処理を実行しない同期的な処理を提供する機能。
await resultは内部ではblocking{}
で処理をラップしているのだが、どうもとある記事によるとscala2.11系においてはこれはデフォルトのスレッドプールの最大値を超えてスレッドを生成するらしい。
正直今回の件はScalaのソースコードを確認する必要があったのだが、Scalaビギナーの私には実装を確認するのは難しく、以下の記事を参考にさせて頂いた。
以下もawait resultについて知る上で参考になった。
最後に
Scalaはmultithreadに関して他の言語より高機能なapiを用意してくれている様に感じるが、globalExecutionContextについてはあまりよりアルゴリズムではないとのみかたもあるようで、使用する際にはJVMのパフォーマンスを評価するツールで異常な状況が検出されないか監視したほうがいいかもしれない。