一覧に戻る
    続・JenkinsとEFS ― 問題の発火は1/26、でも根本原因は1/13にあった
    SRE
    PRこの記事には広告が含まれています

    続・JenkinsとEFS ― 問題の発火は1/26、でも根本原因は1/13にあった

    14 分で読める

    前回、「JenkinsがじわじわGitクローンできなくなっていった話」と「プロビジョンドスループットで1万円使った反省」の2つの記事で、EFSのメタデータIOPS枯渇問題とその対応について書きました。

    月曜の朝にJenkinsが重くなり、調査の結果、Git一時ファイル(tmp_pack_*)が約15GB蓄積していたことが原因だと突き止めました。応急処置としてプロビジョンドスループットを300 MiB/sに変更し、その後Elastic Throughputに切り替えて、定期クリーンアップジョブも作成しました。

    「これで解決だ」と思っていました。

    でも実は、まだ終わっていませんでした。


    グラフが語る「本当の始まり」

    トラブル対応後、念のためEFSのBurst Credit Balanceのグラフを確認していたところ、1/13頃から明確な減少トレンドが始まっていることに気づきました。

    EFS Burst Credit Balanceの推移 - 1/13から減少が始まり、1/26に最低レベルに到達

    問題が顕在化したのは1/26-1/27でしたが、根本原因は2週間前の1/13前後に発生していたことになります。

    つまり、前回の記事で書いた「tmp_pack_*の蓄積」は症状の悪化であり、真の根本原因は別にあるということです。

    正直ここで詰まりました。1/13前後で何が変わったのか、思い当たる節がいくつかありましたが、確定的な情報ではありません。


    1/13前後の変更を洗い出す

    思い当たる変更点は2つありました。

    1. エージェント方式の変更

    1/13前後に、Jenkinsエージェントの運用方式を大きく変更していました。

    従来の方式: 共有エージェント

    • EC2インスタンス: c5.large など
    • 複数のジョブで共有して使用
    • ワークスペースを再利用
    • git pull で差分のみ取得

    新しい方式: 使い捨てエージェント

    • EC2インスタンス: t3.small など
    • 1ジョブ1エージェント
    • 使い終わったら破棄
    • 毎回 git clone でフルクローン

    コスト削減のための変更でしたが、これがメタデータIOPSに与える影響は考えていませんでした。

    2. 年始の開発加速

    年始なので各チームの開発が加速し、Jenkins全体の負荷が上がった可能性もあります。


    仮説: 使い捨て方式がメタデータIOPSを激増させた

    ここで立てた仮説は、使い捨てエージェント方式への変更が、メタデータIOPSを激増させたのではないかというものでした。

    簡単な計算をしてみます:

    ビルド回数: 50回/日(推定)
    1クローンあたりのファイル作成: 5,000個
    
    共有エージェント方式:
      初回のみクローン = 5,000回のメタデータ操作
    
    使い捨てエージェント方式:
      50ビルド × 5,000ファイル = 250,000回のメタデータ操作/日

    50倍のメタデータIOPS増加です。

    年始で開発が加速すれば、この数字はさらに増えます。コスト最適化のつもりが、別の問題を引き起こしていた可能性が高いと思いました。


    キャッシュディレクトリの謎

    調査を進める中で、/mnt/efs/jenkins/caches/ というディレクトリに気づきました。

    /mnt/efs/jenkins/caches/git-3e9b32912840757a720f39230c221f0e

    このようなディレクトリが大量にあります。これは何でしょうか?

    実は、これはJenkinsのGitプラグインがリポジトリをキャッシュするためのbare repositoryです。

    Gitキャッシュの仕組み

    JenkinsのGitプラグインは、効率化のために以下のような仕組みを持っています:

    1. リモートリポジトリを /mnt/efs/jenkins/caches/git-{hash}/ にbare repositoryとしてキャッシュ
    2. 各ジョブのワークスペースには、このキャッシュから git clone --reference でクローン
    3. ハッシュ値はリポジトリURL + ブランチなどから生成

    ここで新たな疑問が浮かびました: 使い捨てエージェント方式に変更したことで、このキャッシュが無効化されているのではないか?

    • 共有エージェント: キャッシュが有効活用される
    • 使い捨てエージェント: 毎回新しいエージェントなので、キャッシュの恩恵を受けられず、毎回フルクローン

    tmp_pack_*の正体を再確認

    前回の記事で見つけた tmp_pack_* ファイルの場所を改めて確認してみます:

    jobs/sample-job/jobs/sample-pipeline/builds/104/libs/
      └── 335abf.../root/.git/objects/pack/
          └── tmp_pack_WqmOyE  ← 100-300MB

    これはビルドごとのディレクトリに存在しています。つまり:

    jobs/sample-job/jobs/sample-pipeline/
    └── builds/
        ├── 104/
        │   └── libs/.../tmp_pack_WqmOyE
        ├── 105/
        │   └── libs/.../tmp_pack_XYZ123
        └── 106/
            └── libs/.../tmp_pack_ABC456

    毎ビルドごとにPipeline Shared Libraryが再チェックアウトされ、それぞれで tmp_pack_* が残っている状態です。

    ここで新しい疑問が出てきます: なぜ毎ビルドごとにShared Libraryが再チェックアウトされているのか?

    通常、Shared Libraryもキャッシュされるはずです。


    根本原因の発見: キャッシュ設定がOFFだった

    Jenkinsの設定を確認したところ、決定的な設定ミスを見つけました。

    Shared Libraryの設定で、Cache fetched versions on controller for quick retrieval のチェックが外れていました。

    つまり:

    • Shared Libraryのキャッシュが完全に無効
    • 毎ビルドごとにリモートリポジトリからフルフェッチ
    • .git/objects/pack/ に一時ファイルが生成される
    • メタデータIOPSが激増

    これで点と点が線になりました:

    1. 使い捨てエージェント方式への変更(1/13)→ エージェント側で毎回フルクローン
    2. Shared Libraryキャッシュ無効(以前から)→ Controller側で毎回フルフェッチ
    3. 年始の開発加速 → ビルド回数増加

    この3つが重なり、メタデータIOPSが激増。2週間かけてBurst Creditが枯渇し、1/26に症状が顕在化しました。


    対策: キャッシュを有効化する

    すぐに設定を変更しました:

    1. Cache fetched versions on controller for quick retrievalON
    2. Refresh time in minutes180分に設定

    Refresh timeをどう決めたか

    ここは意外と肝です。選択肢としては:

    • 60-120分: 積極的な開発期向け。変更が早く反映されるがIOPS削減効果は中程度
    • 180分(3時間): バランス型。1日8回程度の更新チェック
    • 360分(6時間): 安定運用向け。1日4回の更新チェック
    • 1440分(24時間): 最大限のIOPS削減。変更頻度が低い場合

    今回は180分(3時間)を選びました。理由は:

    1. 1日8回程度の更新チェック(9時、12時、15時、18時...)
    2. Shared Libraryの変更は通常半日以内に反映されれば問題ない
    3. IOPSを大幅に削減できる(毎ビルド → 3時間に1回)
    4. 緊急時は手動でキャッシュクリアも可能

    実は、Jenkinsには「強制的にリフレッシュ」する機能があるので、急ぎの変更があっても問題ありません。ただ、その存在を忘れてしまいそうなので、運用ドキュメントに書いておくことにしました。


    設定変更後の効果測定

    設定変更後、以下のタイムラインで効果を確認する予定です:

    短期(24-48時間後)

    • 新しい tmp_pack_* が生成されなくなったか
    • EFSのメタデータIOPSが減少しているか

    中期(1週間後)

    • Burst Credit Balanceが回復傾向にあるか
    • ビルドのパフォーマンスが安定しているか

    長期(1ヶ月後)

    • クレジットが安定して維持されているか
    • 問題が再発していないか

    振り返りと学び

    根本原因は複合的だった

    今回の問題は、単一の原因ではなく、複数の要因が重なって発生していました:

    1. Shared Libraryキャッシュ無効化(以前から存在)
    2. 使い捨てエージェント方式への変更(1/13前後)
    3. 年始の開発加速(ビルド回数増加)

    それぞれ単体では大きな問題にならなかったかもしれませんが、3つが重なることで、メタデータIOPSが臨界点を超えました。

    症状と根本原因の時期は異なる

    • 症状発現: 1/26-1/27
    • 根本原因: 1/13前後
    • クレジット枯渇: 2週間かけて進行

    グラフを時系列で見る重要性を痛感しました。目の前の症状だけを見て対応すると、表面的な修正に終わってしまいます。

    アーキテクチャ変更のリスク

    使い捨てエージェント方式への変更は、コスト最適化が目的でした。確かにEC2のコストは削減できましたが、別の場所でコストとパフォーマンスの問題を引き起こしていました

    アーキテクチャを変更する際は:

    • パフォーマンス影響を事前評価する
    • 監視指標を設定してから変更する
    • 変更後も継続的にメトリクスを追跡する

    これらが重要だと改めて感じました。

    メタデータIOPSの特性

    EFSのメタデータIOPSについて、重要な特性がわかりました:

    • 小さいファイルの大量作成・削除が致命的
    • ストレージ容量よりファイル数が重要
    • バーストモードはクレジット管理が鍵
    • クレジット枯渇は段階的に進行する

    特に、.git/objects/ 配下のような「小さなファイルが大量にある」ケースでは、通常のファイルI/Oとは全く異なる挙動を示します。


    次の問い

    今回の対応で、Shared Libraryのキャッシュを有効にしました。しかし、使い捨てエージェント方式自体は継続しています。

    エージェント側のGitキャッシュは、使い捨て方式でも有効活用できるのか?

    これについては、もう少し調査が必要です。可能性としては:

    1. EFS上のGitキャッシュを全エージェントで共有する
    2. エージェントのライフサイクルを少し長くして、複数ジョブで再利用する
    3. S3などにキャッシュを配置し、起動時に同期する

    コストとパフォーマンスのバランスをどう取るか。これは次の課題として残ります。


    関連記事

    この記事は、以下の記事の続編です:

    JenkinsがじわじわGitクローンできなくなっていった話 ― EFSメタデータIOPSとの闘い

    JenkinsがじわじわGitクローンできなくなっていった話 ― EFSメタデータIOPSとの闘い

    Tech
    SRE
    トラブル解決後の仕事 ― プロビジョンドスループットで1万円使った反省

    トラブル解決後の仕事 ― プロビジョンドスループットで1万円使った反省

    Tech
    SRE

    関連書籍

    今回のようなインフラ運用やトラブルシューティングについてさらに学びたい方におすすめの書籍です。

    この記事は役に立ちましたか?

    Coffee cup

    この記事が、何かの整理につながったら

    コーヒー1杯分の応援をもらえると嬉しいです。

    ※ これは応援とは別の話ですが、

    同じようなテーマを自分の文脈で整理したい場合は、 (文章だけだと詰まりやすい人向けに) 思考整理の壁打ちという形で対話の時間も取っています。

    対話の時間について