Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

マルチスレッド問題の特定と再現に頑張った話

3,770 views

Published on

JJUG CCC 2018 Spring

Published in: Technology
  • Follow the link, new dating source: ❤❤❤ http://bit.ly/2Qu6Caa ❤❤❤
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Dating direct: ♥♥♥ http://bit.ly/2Qu6Caa ♥♥♥
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

マルチスレッド問題の特定と再現に頑張った話

  1. 1. マルチスレッド問題の特定と 再現に頑張った話 Koji Lin, LINE Fukuoka @kojilin
  2. 2. 自己紹介 ● LINE Fukuoka Corp ○ サーバーサイドエンジニア ● Taiwan Java User Group メンバー ○ https://www.meetup.com/taiwanjug/
  3. 3. 今日のセッションの経緯 ● 原因を探すための道のりが長かった ● GitHub で Issue と修正 PR 等も作った ● 似てるような問題にあったら、参考できるかも
  4. 4. 今日の内容 ● 実際にあった問題 ● Java VisualVM でヒープダンプ解析 ● コードみて、デバッグモードで再現 ● Issue と PR 作って解決してもらった/した
  5. 5. Redis への再接続しない問題
  6. 6. 実際にあった問題 ● ある日サービス中の一台サーバが OOM ○ 一回目は直接再起動してしまって、ヒープダンプを取るの を忘れてた ● 数カ月後にまた発生 ○ 今回はちゃんとヒープダンプが取れた ● リアル環境は数十〜数百台のインスタンス
  7. 7. 調査と対応 ● OOM なのでわかりやすい ● Java VisualVM を使ってヒープダンプ解析 ○ https://visualvm.github.io/ ○ ファイルが大きい場合、visualvm.conf で Xmx の設定が 必要
  8. 8. 調査と対応 ● Lettuce Command Queue が溢れていた
  9. 9. Lettuce ● 非同期な Redis クライアント ○ 問題発生時のバージョンは 4.3.2 と 4.4.2
  10. 10. どこで使っているのか? ● データベース前のキャッシュとして使っている Application MySQL Redis ここ
  11. 11. 調査と対応 ● Issue を立てて報告をする ○ https://github.com/lettuce-io/lettuce-core/issues/466 ● 原因を調べるが、最悪の OOM は回避したい ○ 先に requestQueueSize を設定する ○ デフォルトは Integer.MAX_VALUE ● それでも OOM が発生 ○ FastCountingDeque.java を見てみる
  12. 12. 調査と対応 ● Issue を立てて報告をする ○ https://github.com/lettuce-io/lettuce-core/issues/466 ● 原因を調べるが、最悪の OOM は回避したい ○ 先に requestQueueSize を設定する ○ デフォルトは Integer.MAX_VALUE ● それでも OOM が発生 ○ FastCountingDeque.java を見てみる ○ Queue のサイズ計算を assert 付きで実行していた
  13. 13. 調査と対応 ● その後 Queue の上限問題が修正された ○ 数カ月後に新しく command drop のエラーログが出る ○ 一応 OOM を回避できた
  14. 14. 調査と対応 ● Queue が一杯になるのはコマンドがを送れないから ○ 問題発生時にネットワーク問題がなかった ■ 他のサーバは正常 ■ 他サービスとの接続も正常 ○ 一時的ではなく、発生後元に戻らない
  15. 15. 調査と対応 ● Lettuce 自体の Redis 接続に問題あるかも ○ 再接続かな? ○ Queue のレファレンスは CommandHandler ○ 正常な CommandHandler と問題の CommandHandler を比べてみる
  16. 16. ConnectionWatchDog を読む ● 何かの原因で再接続しない? ● 矛盾な状態を確認 ○ Timer の state の値は 2? ○ reconnectScheduleTimeout が null ではないが、 すでに実行されている?
  17. 17. HashedWheelTimer を読む ● ST_EXPIRED(2) ならすでに実行済みであっている ○ #expire で確認
  18. 18. シナリオを考える ● ConnectionWatchDog#scheduleReconnect が実行 ● timer#newTimeout が呼ばれる ● ConnectionWatchDog#run が実行
  19. 19. デバッグモードで実行してみる ● IDE で簡単にストップができる
  20. 20. デバッグモードでハッピーパスを見る ● IDE で簡単にログを残す
  21. 21. シナリオを考える ● ConnectionWatchDog#scheduleReconnect が実行 ● timer#newTimeout が呼ばれる ● ConnectionWatchDog#run が実行 (reconnectScheduleTimeout = null) ● timer#newTimeout が値を戻す (reconnectScheduleTimeout != null)
  22. 22. ConnectionWatchDog をデバッグ ● 仮設を再現したい
  23. 23. デバッグモードで再現してみる ● IDE で簡単にスレッド別ストップができる
  24. 24. 再現してみる ● Breakpoint の設定 ○ HashedWheelTimer.java#L424 ○ ConnectionWatchDog.java#L246
  25. 25. 報告と修正 ● 再現手順とシナリオを伝えて、修正 PR を送った ● 修正後数ヶ月でまだ再発はしていない
  26. 26. 表示する資料が古い問題
  27. 27. 実際にあった問題 ● ある日ランキングの内容が偶に古い ○ 一部のアクセスだけ古いデータが見える
  28. 28. 問題ある API はキャッシュをしていた ● アプリケーション内に一部メモリキャッシュして、キャッシュミス 時は Redis を問いに行く Application Memory Cache MySQL Redis
  29. 29. 調査と対応 ● リリース環境には数台以上のサーバがあった ○ 発生したのは一台のサーバだけ ● Redis 内のキャッシュも古くない ● MySQL 内のデータも異常な変更がない ● 問題は各サーバのメモリ内でキャッシュしたデータかな
  30. 30. 問題ある API はキャッシュをしていた ● アプリケーション内に一部メモリキャッシュして、キャッシュミス 時は Redis を問いに行く Application Memory Cache MySQL Redis ここ
  31. 31. Caffeine ● パフォーマンス重視のキャッシュライブラリ ○ 問題発生時のバージョンは 2.5.6
  32. 32. 調査と対応 ● 何故いままで問題なかった、又は気づかなかったのか? ○ サーバのリリース周期が短い ○ これまではデータの変動がそれほど頻繁ではなかった ● 調査中は状況によってメモリキャッシュを無効にして、Redis へ直接アクセスさせる事もできた ○ Redis への負担も調べる
  33. 33. 調査と対応 ● ヒープダンプを取る ○ 間違ったデータを探し出して、関連する手がかりを探して みる ○ Object Query Language を使って探しやすくする
  34. 34. 調査と対応 ● 正常なデータと異様なデータを比べる ○ writeTime が変 ○ でもどっちも CompletableFuture が完了している ● Issue を立てる ○ https://github.com/ben-manes/caffeine/issues/212 ○ 作者から LocalAsyncLoadingCache#get で writeTime が更新すると教えてもらう
  35. 35. ソースを読む ● どこで writeTime を更新しているかを探す ○ NodeFactory$SStWRMW から始める ○ $AddTaskとLocalAsyncLoadingCache#replace に辿り着く ● 作者の言ってた LocalAsyncLoadingCache も見てみる ○ LocalAsyncLoadingCache#replace に辿り着く
  36. 36. これまで解ったこと ● Caffeine は非同期取得中のデータが失効しなように、現在時 間に Long.MAX_VALUE を足して writeTime を設定して いた ● データ取得後はちゃんと現在時間にリセットする..はず ● AddTask#run と #replace の実行に交錯があれば?
  37. 37. シナリオを考える ● AddTask で先に synchronized に入り、now を取る直前 に進む ● replace で synchronized に入る前に now を決定する (now = 100) ● AddTask がそのまま進み、writeTime を 設定する(now = 200 + Long.MAX_VALUE ) ● AddTask が synchronized から出たあとに replace の computeIfPresent で hasExpired(n, 100) に 引っかかって return する 100 200
  38. 38. シナリオを考える ● replace では expired と判断されるが、これ以降の時間 にとっては未来時間になってしまう
  39. 39. 再現してみる ● Breakpoint の設定 ○ BoundedLocalCache.java#L1338 ○ BoundedLocalCache.java#L1929
  40. 40. 報告と修正 ● 再現手順とシナリオを伝えて、作者が修正した ● 解決できたかの検証 ● 同時運行して、元のバージョンでは数件データ見つかるが、修 正後はもうない
  41. 41. まとめ ● 問題発生したら、ちゃんとヒープダンプを取ろう ○ VisualVM と OQL で問題のインスタンスを探す ○ 異常と思ったプロパティで色々な手がかりがあるかも ● スレッド実行のシナリオをソースをみながら想像する ○ デバッグモードで実行スレッドの確認やシナリオの再現が 簡単にできる ● アップストリームに貢献できればみんな嬉しい
  42. 42. Q&A

×