Jenkinsビルドの遅延トラブルシューティング:一般的なボトルネックと解決策

Jenkinsビルドのパフォーマンス問題を特定し解決するための実践的な手順を紹介します。ログ分析、エグゼキュータ設定の最適化、ビルドキャッシュメカニズムの活用、パイプラインスクリプトの効率化を通じて、より高速で効率的なCI/CDプロセスを実現する方法を解説します。

Jenkinsビルドの遅延トラブルシューティング:一般的なボトルネックと解決策

Jenkinsビルドが遅いと、フィードバックが遅れるため問題です。開発者が小さな変更をプッシュし、20分待ってから、最初の1分でテストが失敗したことを知る——そんな状況です。チューニングを始める前に、キュー時間、エージェント起動時間、チェックアウト時間、依存関係セットアップ、テスト時間、パッケージング、デプロイメントを分けて考えましょう。これらは異なる問題であり、異なる修正方法が必要です。

目標はダッシュボード上でJenkinsを速く見せることではありません。目標は、次の有用なシグナルをより早く届けることです。

1. 初期診断:時間はどこで消費されているか?

修正を適用する前に、遅延の原因を特定する必要があります。Jenkinsには初期診断に優れた組み込みツールがあります。

ビルドログの分析

最も直接的なリソースは、遅いビルドのコンソール出力です。連続するステップ間のタイムスタンプに大きなギャップがないか確認します。

  • 長時間実行ステップの特定: どのビルドステップ(例:mvn clean install、スクリプト実行、依存関係ダウンロード)が最も時間を消費しているかを確認します。
  • 外部呼び出し: ネットワークアクティビティを含むステージ(例:外部依存関係の取得、リモートアーティファクトリポジトリへの接続)に注意します。これらは多くの場合、Jenkins自体ではなく外部依存関係が原因です。

ビルド時間グラフの活用

Jenkins Blue OceanやクラシックUIパイプラインでは、ステージの所要時間を視覚的に表示することがよくあります。この視覚的な補助を使って、どのステージが不釣り合いに長いかを確認します。

ヒント: 特定のステージが複数のビルドにわたって一貫して予想以上に時間がかかる場合、それが最適化の主要ターゲットです。

2. Jenkinsインフラストラクチャのボトルネック

ビルドステップ自体は速いが、ジョブ間の待機時間が長い場合、問題はJenkinsコントローラ(マスター)またはエージェント(スレーブ)のインフラストラクチャにある可能性が高いです。

エグゼキュータの可用性と過負荷

最も一般的なインフラ問題は、ビルド容量の不足です。

エグゼキュータの理解

エグゼキュータは、Jenkinsノード上でジョブを実行するための並列スロットです。ノードに5つのエグゼキュータがある場合、5つのジョブを同時に実行できます。

  • 症状: CPU/メモリ使用率が低いように見えても、ビルドが常にキューイングされている。
  • 解決策: プライマリビルドノードのエグゼキュータ数を増やすか、ファームにノード/エージェントを追加します。

設定確認(エージェント管理): エージェント設定画面を確認します。そのエージェントに割り当てられたハードウェアに適した「エグゼキュータ数」が設定されていることを確認します。

コントローラの負荷

Jenkinsコントローラノードが過負荷の場合、エージェントが空いていてもジョブを適切にスケジュールできません。

  • 症状: UIの応答が遅い、ビルドスケジュールが遅延する、コントローラのシステムモニターでCPU/メモリ使用率が高い。
  • 解決策: コンパイルなどの負荷の高いタスクをエージェントにオフロードします。コントローラには、主に管理タスク専用の十分なリソース(CPU、十分なRAM)を確保します。

ディスクI/Oパフォーマンス

ディスクの入出力(I/O)が遅いと、Gitリポジトリのクローンや大きなアーカイブの展開など、大規模なファイル操作を含むステップに影響します。

  • ベストプラクティス: JenkinsワークスペースとJenkinsホームディレクトリには、特にビルドエージェント上で、高速ストレージ(SSDや高スループットのネットワークストレージ)を使用します。

3. パイプラインスクリプトの最適化

非効率な宣言型またはスクリプト型パイプラインは、不必要なオーバーヘッドを引き起こす可能性があります。

ワークスペース管理

古いアーティファクトでいっぱいの大きなワークスペースは、クローンやクリーンアップなどの後続の操作を遅くする可能性があります。

  • ws() ステップの賢い使用: スクリプト型パイプラインを使用する場合、ワークスペース全体に対する操作に注意します。
  • ワークスペースのクリーン: 正常完了後にワークスペースをクリーンするようにジョブを設定するか、cleanWs() ステップを適切に使用します。警告: インクリメンタルビルドや実行間のアーティファクトキャッシュに依存している場合は、ワークスペースをクリーンしないでください。

冗長な操作(依存関係のダウンロード)

同じ依存関係を繰り返しダウンロードすると時間を浪費します。

  • 依存関係のキャッシュ: エージェント環境内でビルドツール固有のキャッシュ戦略(例:Mavenローカルリポジトリ、npmキャッシュ)を実装します。可能であれば、キャッシュディレクトリが永続的で共有されていることを確認します。
// 例:エージェント上でMavenリポジトリの永続性を確保
steps {
    sh 'mvn -B clean install -Dmaven.repo.local=/path/to/shared/maven/cache'
}

独立したステージの並列化

パイプライン内のステージが独立している場合、宣言型パイプラインの parallel ブロックを使用して同時に実行します。

pipeline {
    agent any
    stages {
        stage('Build & Test') {
            parallel {
                stage('Unit Tests') {
                    steps { sh './run_tests.sh' }
                }
                stage('Static Analysis') {
                    steps { sh './run_sonar.sh' }
                }
            }
        }
        stage('Package') {
            // Build & Test ステージの両方が完了した後に実行
            steps { sh './create_jar.sh' }
        }
    }
}

4. ビルドキャッシュメカニズムの活用

Dockerイメージやコンパイル済みソースファイルなど、大きなコンポーネントを再利用するビルドでは、キャッシュが速度向上に不可欠です。

Dockerレイヤーキャッシュ

パイプラインがDockerイメージをビルドする場合、レイヤーキャッシュを効果的に活用します。

  1. 順序が重要: 頻繁に変更されるステップ(例:COPY . .)は、まれにしか変更されないステップ(例:ベース依存関係のインストール)よりもDockerfileの後半に配置します。
  2. Dockerエージェントの使用: Dockerを実行するJenkinsエージェントを使用する場合、ビルドプロセスがフルプル/ビルドを試みる前に、既存のローカルイメージキャッシュを活用するようにします。

インクリメンタルビルド

該当する場合、ビルドツールがインクリメンタルビルド用に設定されていることを確認します(例:Gradleのビルドキャッシュ、特定のコンパイラフラグの使用)。

5. エージェント設定とリソース割り当て

エージェントは実際の処理を行う場所です。適切にプロビジョニングおよび設定されていることを確認します。

ハードウェアサイジング

ビルド中にCPU使用率が高い場合、エージェントはより多くの処理能力を必要としています。ビルドが頻繁にリソース(メモリなど)を待っている場合は、RAMを増やします。

エージェント起動方法

  • 静的エージェント: 起動が速いですが、スケーリングの柔軟性は低いです。
  • 動的エージェント(例:KubernetesまたはEC2エージェント): セットアップに少し時間がかかりますが、必要なときに正確にリソースをスケーリングできるため、ピーク時の長いキューを回避できます。

ベストプラクティス: 動的スケーリングの場合、新しいエージェントの起動時間が、ジョブがキューでタイムアウトするまでの時間よりも十分に短いことを確認します。エージェントのプロビジョニングに10分かかるのに、ジョブの待機時間が3分しかない場合、スケーリングは即時のボトルネックを解決しません。

実践的なスロービルドプレイブック

  1. ログの分析: どのパイプラインステップが最も時間を消費しているかを特定します。
  2. エグゼキュータの確認: エージェントのエグゼキュータ数が予想される同時負荷と一致していることを確認します。
  3. I/Oの最適化: ワークスペースとキャッシュが高速ストレージ上にあることを確認します。
  4. 依存関係のキャッシュ: Maven、npm、その他の依存関係キャッシュの永続化を実装します。
  5. 並列化: 独立したパイプラインステージを同時に実行するように書き換えます。
  6. プロファイルツール: ビルドツール(Maven、Gradle)がインクリメンタルビルド機能を使用していることを確認します。

これらの潜在的なボトルネックに——インフラ容量からスクリプト効率まで——体系的に対処することで、遅くてイライラするビルドを、高速で信頼性の高いCI/CDワークフローの構成要素に変えることができます。

スロービルドをより正直に読む方法

午後を無駄にする最も速い方法は、すべての遅いJenkinsビルドをJenkinsの問題として扱うことです。時にはJenkinsがボトルネックです。多くの場合、Jenkinsは単なるメッセンジャーです。パイプラインが遅く見えるのは、キューで待機するため、エージェントの起動に時間がかかるため、Gitチェックアウトが遅いため、ビルドツールが再びインターネットからダウンロードするため、テストがシリアル化されているため、または下流のデプロイメントステップが別のシステムを待つためです。

遅いジョブを見るとき、私は総時間を4つのバケットに分割します:キュー時間、エージェントプロビジョニング時間、ワークスペースセットアップ時間、実際のビルド/テスト時間。Jenkinsはビルドページとパイプラインステージビューでこれらの一部を表示しますが、コンソールログが依然として最も有用な記録です。タイムスタンプが欠落している場合は追加します。次に、遅い実行と通常の実行を比較します。2つのタイムラインが最初に分岐する場所を探しています。

たとえば、遅い実行が最初のシェルコマンドが開始される前に8分を費やしている場合、Mavenをチューニングしても役に立ちません。エグゼキュータの可用性、ラベルマッチング、クラウドエージェントプロビジョニング、保留中のジョブを確認します。遅い実行がすぐに開始されるが、git fetchに5分かかる場合、リポジトリサイズ、refspec、タグ、ネットワークパス、ワークスペースの再利用を確認します。チェックアウトが速いが、npm ciが毎回遅い場合、エージェントからのキャッシュ永続性とレジストリアクセスを調査します。

記憶だけで最適化しないでください。最近の3つのビルドを選びます:1つは高速、1つは典型的、1つは低速。各ステージの所要時間を書き留めます。その小さな表が通常、正しいレイヤーを指し示します。

キュー時間:ビルド開始前のボトルネック

キュー時間は、まだ何も失敗していないため無視されがちです。開発者はビルドがそこにあるのを見るだけです。Jenkinsでは、長いキューは通常、次の4つのいずれかを意味します:エグゼキュータが十分でない、ラベルが狭すぎる、ロックが作業をシリアル化している、動的エージェントの出現が遅い。

ジョブページとエグゼキュータステータスパネルから始めます。多くのエージェントがアイドル状態であるにもかかわらずジョブがキューイングされている場合、ラベル式が厳しすぎる可能性があります。linux && docker && java17 && large というラベルのジョブは、すべてのラベルに一致するノードでのみ実行できます。これはプロダクションリリースビルドでは意図的かもしれませんが、通常のプルリクエストチェックでは偶発的であることがよくあります。一般的なビルドがDockerとJavaのみを必要とする場合、特別な理由がない限り、1つの特別なマシンに縛らないでください。

ロックも静かな遅延の原因です。Lockable Resourcesプラグインは、テストが共有データベース、ハードウェアデバイス、ステージング名前空間への排他的アクセスを必要とする場合に便利です。ロック内にあまりにも多くの作業があると、問題になります。ロックされたセクションは可能な限り小さく保ちます。ロックの外でアーティファクトをビルドし、ロックを取得し、共有リソースステップのみを実行し、解放します。

クラウドエージェントの場合、起動時間を個別に測定します。スケジュールに2分かかるKubernetesポッドは問題ないかもしれません。実行のたびに大きなカスタムイメージをプルするために15分かかるポッドは問題です。一般的なイメージを事前にプルするか、イメージサイズを減らすか、CIトラフィックが予測可能な場合は小さなウォームプールを維持します。

チェックアウト時間:Gitが問題全体になる可能性がある

遅いチェックアウトは、リポジトリが徐々に成長するため、古いJenkinsインストールで一般的です。最初のいくつかの大きなバイナリに誰も気づきませんが、ある日、すべてのビルドが何年もの履歴の代償を払うことになります。

Gitプラグインの設定を慎重に使用します。浅いクローンは現在のコミットのみを必要とするジョブに役立ちますが、タグからバージョンを計算したり、以前のコミットと比較したりするビルドを壊す可能性があります。タグのフェッチも、タグの多いリポジトリでは驚くほどの時間を追加する可能性があります。ジョブがタグを必要としない場合は、タグフェッチを無効にします。パイプラインが複数のリポジトリをチェックアウトする場合、各チェックアウトを個別に計測して、1つの遅い依存関係リポジトリが一般的な「SCM」ステージ内に隠れないようにします。

ワークスペースの再利用はトレードオフです。ワークスペースを再利用すると git fetch がはるかに速くなりますが、古いファイルが奇妙な障害を引き起こす可能性があります。すべてのビルドの前にワークスペースをワイプするのはクリーンですが、大規模なモノレポではコストがかかる可能性があります。実用的な中間点は、.git ディレクトリを保持しながら追跡されていないファイルを削除するクリーンチェックアウトコマンドを使用するか、失敗したビルドとスケジュールされたクリーンアップのために完全なワークスペースワイプを予約することです。

ビジーなエージェントでは、チェックアウト速度はディスクの問題でもあります。10のビルドが同じ小さなボリュームで大規模なリポジトリをクローンする場合、CPUは正常に見えてもディスクI/Oが飽和している可能性があります。ビルド実行中に iostat、クラウドボリュームメトリクス、またはエージェントのストレージダッシュボードを確認します。ワークスペースをより高速なローカルSSDストレージに移動すると、Jenkinsの設定よりもビルド時間が変わる可能性があります。

依存関係キャッシュには所有権が必要

キャッシュは、誰かがそれを所有している場合にのみ役立ちます。ランダムに消える、無制限に成長する、互換性のないツールバージョンを混在させるキャッシュは、節約する以上の問題を引き起こす可能性があります。

MavenとGradleの場合、永続的なローカルリポジトリまたはビルドキャッシュにより、繰り返しのダウンロードを削減できます。キャッシュは使い捨てのワークスペースの外に存在する必要があります。また、同時ビルドに対して安全である必要があります。Mavenのローカルリポジトリは通常の依存関係読み取りには通常問題ありませんが、中断されたダウンロードは不良ファイルを残す可能性があります。チェックサムエラーや破損したアーティファクトが表示された場合は、習慣的にキャッシュ全体を削除するのではなく、特定の依存関係パスをクリアします。

npmの場合、再現可能なインストールには npm ci を優先し、オペレーティングシステム、CPUアーキテクチャ、Nodeバージョン、ロックファイルが安定していることが確実でない限り、node_modules ではなくnpmパッケージキャッシュをキャッシュします。異なるエージェントイメージ間で node_modules をキャッシュすることは、CIでのみ発生するネイティブモジュール障害を引き起こす古典的な方法です。

Dockerビルドの場合、最も価値のあるキャッシュは通常レイヤーキャッシュです。Dockerfileでは、安定した依存関係インストールステップをソースコードコピーステップの前に配置します。Dockerデーモンがビルドポッドごとに分離されており、毎回空で起動する場合、ローカルレイヤーキャッシュはあまり役に立ちません。その場合は、環境がサポートしていれば、BuildKitキャッシュエクスポート/インポートまたはレジストリバックアップキャッシュを使用します。

テスト時間:慎重に並列化する

テストは、健全なパイプラインの最長部分であることがよくあります。目標は、単により多くのことを並列に実行することではありません。目標は、不安定な結果を生み出さずにフィードバックを短縮することです。

単体テストは通常、うまく並列化できます。統合テストは、データベース、ポート、キュー、バケット、外部アカウントを共有する可能性があるため、より注意が必要です。2つのテストブランチが同じスキーマに書き込んだり、同じキュー名を再利用したりする場合、並列実行によりパイプラインが高速化されると同時に信頼性が低下する可能性があります。可能な場合は、各ブランチに独自の名前空間、データベーススキーマ、一時ディレクトリ、サービスポート範囲を割り当てます。

テストスイートは、ファイル数ではなく、測定された所要時間で分割します。10個の小さなテストファイルは、1つの大きなブラウザテストよりも速く実行される場合があります。多くのチームは、テスト所要時間を記録し、各並列ブランチがほぼ同じ時間になるようにグループをバランスさせることで、より良い結果を得ています。

また、遅い失敗にも注意します。デッドサービスを10分待ってから失敗するテストステージは、明確なヘルスチェックで30秒で失敗するステージよりも悪いです。長いテストコマンドの前に明示的な準備完了チェックを配置し、ハングする可能性のあるネットワーク呼び出しにタイムアウトを設定します。

コントローラの健全性も重要

ビルド作業はエージェントに属しますが、コントローラは依然としてジョブのスケジュール、ログの提供、パイプラインロジックの評価、プラグインのロード、UIトラフィックの処理を行います。コントローラが過負荷の場合、エージェントに空き容量があっても、すべてのジョブが遅く感じられます。

UIページの遅さ、コンソールログの更新遅延、長いガベージコレクションポーズ、高いコントローラCPUを探します。大規模なパイプラインログ、保持されているビルドが多すぎる、積極的なポーリング、重いプラグインはすべて負荷を追加する可能性があります。ビルドの保持を現実的にします。必要なアーティファクトのみをアーカイブします。大規模なテストレポートとログは、Jenkinsホームボリュームが苦戦している場合は外部ストレージに移動します。

コントローラ上でビルドを実行しないでください。小さなジョブでは無害に見えるかもしれませんが、インシデントの原因を推測するのが難しくなります。コントローラは調整を行うべきです。エージェントはコンパイル、テスト、パッケージ化、デプロイを行うべきです。

実践的な操作順序

チームがJenkinsが遅い理由を尋ねるときは、次の順序を使用します。

  1. キュー時間と実行時間を測定します。
  2. 最近のビルドから最も遅いステージを見つけます。
  3. 遅い実行と通常の実行を比較します。
  4. 遅延が待機、チェックアウト、依存関係ダウンロード、テスト、パッケージング、デプロイメントのいずれであるかを確認します。
  5. 1つのボトルネックを修正し、再度測定します。

最後のステップが重要です。チェックアウトが6分から1分に短縮された場合、簡単に祝ってから測定を続けます。次のボトルネックが見えてきます。CIパフォーマンスの作業は通常、1つの魔法の設定ではなく、一連の小さな検証済みの改善です。