すべて伝聞なんだけど、面白い(興味深い)バグの話を聞いたので、メモしておく。
バグに関する話なので、本来はバージョンをちゃんと書くべきなんだけど、伝聞なので具体的なバージョンは書けないので悪しからず。
事の発端は、Sparkのジョブがたまに止まる(返ってこない)という事象が起きたことらしい。
この状態のStackTraceを見るとjava.util.HashMapのputメソッドで起きていたらしい。実行環境はJava7。これが起きるとCPU使用率が異様に高くなる。
つまり、@ITの『ThreadとHashMapに潜む無限回廊は実に面白い?』と同様の無限ループが起きていた模様。
で、Sparkはcommons-lang(lang3)のSerializationUtils.clone()を呼び出しており、そこでHashMapがスレッドセーフでない使われ方をしていたのが原因らしい。
(SerializationUtilsのソースはたぶんこれ。cloneメソッドの中でClassLoaderAwareObjectInputStreamインスタンスを作ってるんだけど、そのコンストラクター内でstaticフィールドのHashMap(primitiveTypesという変数名)にputしている。SerializationUtilsクラスには#ThreadSafe#というコメントがあるんだけど、スレッドセーフじゃないやんけ!(苦笑))
(ちなみに既に修正のプルリクエストは送られている)
(SerializationUtils.clone()は、最初の頃は別の実装(serializeしてdeserializeする)だったので、スレッドセーフだった模様)
で、個人的興味の1番目としては、「これは何のバグ(と言われるのか)?」ということ。
Spark使用中に起きたことなので、Spark利用者から見るとSparkのバグである。
しかし実際には、Sparkから利用しているユーティリティーのバグである。
StackTraceを見るとHashMapのバグに見えるかもしれない。
実はjava.util.HashMapのソースは、Java7とJava8では大幅に異なっている。
putメソッドを見た感じでは、今回のような無限ループがJava8で起きる可能性はかなり低いと思う。(スレッドセーフでないことに変わりはないので、変な結果にはなり得る。特にputし終わった後のsizeメソッドの結果は実際にかなりの頻度で変になる。無限ループにはなりにくい(ならない?)だろう、というだけ)
なので、もし実行環境をJava8に変えて試したとしたら同様の事象はほぼ起きなかったと思われるわけで、Java7のバグと見做されたかもしれない。
(なお、HashMapは明確にスレッド非セーフと謳われているので、スレッドセーフでない使い方をする方が問題である。HashMapで変な挙動をしていると思ったら、マルチスレッドで呼ばれてないかを確認するのが常道)
もうひとつの興味は、バージョンアップの問題。
SeralizationUtilsの変更履歴を見ると、最初はスレッドセーフだったのが、途中でスレッドセーフでなくなった(バグが入り込んだ)ようだ。
SparkがいつからSerializationUtilsを使い始めたのかは知らないが、最初は問題なかったのに、ライブラリーのバージョンを上げたら問題になった、ということなのかもしれない。
マルチスレッドの問題だけに、Spark側で少々テストを行ったくらいでは発覚しない可能性も高い。
やはりバージョンアップは難しいことだ、と改めて認識させられた。
あと、commons-lang内でのレビュー方法はどうだったのか、という問題もありそうではある。
マルチスレッドの問題が起きてからこのソースを見ると一目で問題あると分かるけれど、そうでないときにこの修正を見て問題があると分かるのは相当熟練者のような気も。
てゆーか、commons-langくらいのものなら、相当の熟練者にレビューして欲しい^^;
(追記1)こういう調査が出来るので、オープンソースは素晴らしいw
(追記2)@ITの記事を昔見ていたおかげで、HashMapで無限ループという事象がすんなり納得できた。障害の事例を公開してくれるのはほんとありがたいです。
※コメント投稿者のブログIDはブログ作成者のみに通知されます