2 ポイント 投稿者 GN⁺ 2025-03-01 | 1件のコメント | WhatsAppで共有
  • 以前、私のシステムのCPU使用率が3,200%に達し、32コアすべてが埋まっていた
  • Java 17ランタイムを使用しており、スレッドダンプでCPU時間を確認してCPU時間順に並べると、よく似たスレッドが多数見つかった
  • 問題のコードを分析
    • スタックトレースを通じて BusinessLogic クラスの29行目を確認
    • 該当コードは unrelatedObjects リストを反復しながら、relatedObject の値を treeMap に挿入する形になっていた
    • これはループ内部で unrelatedObject を使用していない非効率なコードだった

コード修正とテスト

  • 不要なループを削除し、treeMap.put(relatedObject.a(), relatedObject.b()); の1行に修正
  • 修正前後で単体テストを実施したが、問題は再現できなかった
  • treeMapunrelatedObjects のサイズがそれぞれ1,000,000件を超えていても問題は発生しなかった

問題の原因を発見

  • treeMap は複数スレッドから同時にアクセスされていたが、同期化されていなかった
  • これは複数スレッドが TreeMap を同時に変更したことで発生した問題だった

実験による問題の再現

  • 複数スレッドが共有された TreeMap をランダムに更新する実験を行った
  • try-catch ブロックを使い、NullPointerException を無視するように設定
  • 実験の結果、CPU使用率が500%まで上昇する現象を確認した

結論

  • 同期化されていない TreeMap の同時変更は、深刻な性能問題を引き起こす可能性がある
  • このような問題を防ぐため、TreeMap を同期化するか、ConcurrentMap のようなスレッドセーフなコレクションを使用することが推奨される

1件のコメント

 
GN⁺ 2025-03-01
Hacker Newsの意見
  • レースコンディションはデータ破損やデッドロックを引き起こすものだと思っていたが、性能問題も引き起こしうるとは考えていなかった。データが無限ループを生成する形で破損することがある

    • プロジェクトでは、エラーや異常動作、警告は原則として修正すべきだと考えている。無関係に見える別の問題を引き起こす可能性があるため
    • Java のコアコレクションが設計上スレッドセーフではないことはよく知られている。OP はコードの他の部分でも複数スレッドがコレクションを操作していないか確認すべき
    • TreeMap を Collections.synchronizedMap でラップするか、ConcurrentHashMap に切り替えて必要なときだけソートするのが最も簡単な解決策
    • 個々のマップ操作をスレッドセーフにすることはできても、一連の操作がスレッドセーフかどうかは分からない。TreeMap を所有するオブジェクト自体がスレッドセーフかどうかも確信できない
    • 議論の余地がある対処法として訪問済みノードを追跡する方法があるが、良いやり方ではない。コレクションは依然としてスレッドセーフではなく、別の微妙な形で失敗する可能性がある
    • 細部に注意を払う開発者なら、スレッドと TreeMap の組み合わせに気づくか、ソート済み要素が不要なら TreeMap を使わないよう提案できたはず。しかし今回はそうならなかった
    • 問題はコレクションの契約に違反していることであり、TreeMap を HashMap に変えてもなお誤りである
  • 複数スレッドが動作するコードでは、すべてのオブジェクトを不変にし、不変にできないオブジェクトは小さく自己完結した厳密に管理された区画に閉じ込めるのが唯一確実な戦略

    • この原則に従ってコアモジュールを書き直したところ、継続的な問題の発生源だったものが、コードベースの中でも最も堅牢なセクションの一つに変わった
    • こうした指針ができたことで、コードレビューがはるかに容易になった
  • 「ssh にほとんど接続できなかった」という記述で、大学院時代に Sun UltraSparc 170 を使っていた状況を思い出した

    • 新しいユーザーや学生が並列に処理を実行しようとして、大きなテキストファイルを行番号で複数の区間に分割し、それぞれを並列処理していた
    • 大量の RAM が使われ、スワップしようとすると同じファイルの別セクションを読むために激しくシークが発生していた
    • コンソールではログインプロンプトすら出せなかったが、すでにログイン済みのセッションがあり、root セッションを取って問題を解決できた
    • 問題はシステムの限界を理解していなかったことだった
  • コードは単に次のようにまで簡略化できる

    • 元のコードでは <i>unrelatedObjects</i> が空でないときだけ <i>treeMap.put</i> を実行している。これはバグかもしれない
    • <i>a</i> と <i>b</i> が毎回同じ値を返すか確認し、<i>treeMap</i> が本当にマップとして振る舞っているか確認すべき
  • 無限ループを引き起こす別の方法として、一貫した全順序を実装していない <i>Comparator</i> または <i>Comparable</i> 実装を使うケースがある

    • これは並行性とは無関係で、特定のデータと処理順序の組み合わせで発生しうる
  • 増加カウンタを使ってサイクルを検出し、ツリーの深さやコレクションサイズを超えたら例外を投げる方法を検討できる

    • これはメモリや CPU のオーバーヘッドをほとんど必要とせず、より受け入れられやすい可能性がある
  • Java でスレッドセーフでないオブジェクトに対して並行処理を行うと、最も興味深いバグが生まれる

  • 保護されていない TreeMap が 3,200% の使用率を引き起こしうるのかという疑問がある

    • 2009 年ごろに似た問題を見たことがあり、今でも起こりうる
    • データレースを「少し悪い程度」と考える人にとっては残念な話
  • 著者は Poison Pill の一種を発見した。これはイベントソーシングシステムでより一般的で、遭遇するものすべてを殺すメッセージのこと

    • データ構造が不正な状態に達すると、その後のすべてのスレッドが同じ論理爆弾にはまり込む
  • スレッド内の例外は絶対的な問題

    • C++、select()、そしてスレッドが例外を振り回す恐怖のバグハント話がある