HN 日本語サマリー

← 一覧へ戻る
インフラ・DevOps

AIコーディングがCIをボトルネックにしたため、LinearはCIを再構築した

AI coding has made CI a bottleneck, so we reworked ours to keep up (linear.app)

178 pointsby julian_digital184 コメント

要約

AIによるコード生成の加速により、継続的インテグレーション(CI)の処理時間がボトルネックとなり、インフラコストの増加と開発者フィードバックの遅延を招いていました。Linearは、インフラのアップグレード、ジョブの最適化、セットアップの効率化、テスト実行の改善などを通じて、CIのパフォーマンスを大幅に向上させました。

全文翻訳

今年初め、私はLinearを開き、CTOのTuomasが私に「CIコストが高い」というタイトルの課題を割り当てているのを見つけました。ついでに、彼はCIをより速くしてほしいとも望んでいました。 エージェントはコードの出荷を指数関数的に速くしましたが、それらの変更の検証は同じ速度で追いついていません。すべてのPRは依然としてCIを通過する必要があるため、開発が加速するにつれて、CIはボトルネックとなり、インフラコストを押し上げ、開発者とエージェントをフィードバックの待ち時間で長く放置することになります。 LinearでCIのパフォーマンスを向上させるために、PRがCIで待機する時間と、それが消費するランナー時間を最適化しました。年初からテストスイートがほぼ4倍になったにもかかわらず、プルリクエストの待機時間を6分強から5分強に短縮し、テストあたりのランナー時間を約半分に削減しました。 これは、1月の第1週を基準としたテストスイートのパフォーマンスです。機械時間あたりのテストを追跡する白い線は、待機時間を短縮し、より多くの機械時間を消費するテストシャーディングを追加したときにスパイクし、チェックアウトの遅延問題中に再びスパイクしました。 全体として、CIを4つの方法で改善しました。 インフラとツールのアップグレード 他の作業をゲートするジョブの最適化 繰り返しのセットアップを削減 テスト実行の効率化 Linearのコードベースは主にTypeScriptですが、これらの最適化の多くは、言語やツールチェーン全体に適用できます。 インフラとツールのアップグレード 私たちの初期の成果のいくつかは、CI自体の最適化をほとんど必要としませんでした。ワークロードをGitHub Actionsから、より高速なCPU、高性能ストレージ、および優れたキャッシュインフラストラクチャを備えたサードパーティランナーに移行したことで、同じパイプラインを実行するためのより高速なマシンが得られました。切り替え前後の2日間を比較すると、ジョブは平均で34%速く実行され、tscのような一部のワークロードは52%低下しました。 別途、ツールチェーンの近代化も成果を上げました。ネイティブTypeScriptコンパイラであるtsgoに切り替えたことで、tscチェックの週次中央値が73%削減され、タイプチェックからボトルネックを完全に解消するのに十分な大きさでした。 タイプチェッカーなしでのLint Lintingも別の初期ターゲットでした。カスタムLintルールのいくつかは、制限を強制するため、または自動修正を適用するために、TypeScriptの型情報に依存していました。そのため、すべてのLint実行で、それらのルールを評価する前に完全な型グラフをビルドする必要があり、Lintingはメモリ集約型のCIジョブの1つになっていました。 AST(抽象構文木)に対する静的解析を使用してルールを書き直し、型情報なしで関数のような構造とガードパターンを識別できるようにしました。これにより、ESLintはTypeScriptを完全にドロップでき、API Lint時間を68%、リポジトリ全体のLint時間を55%削減できました。メモリ使用量も大幅に低下しました。 型情報への依存を削除したことで、ルールが純粋に構文を操作するOxlintへの移行も容易になり、ルールは簡単に移植できるようになりました。Oxlint自体は、Lintingに費やされるCIランナー時間を削減しました。 他の作業をゲートするジョブの最適化 基盤となるインフラストラクチャと個々のチェックがより高速に実行されるようになったため、CIをシステムとして全体的に見直しました。これにより、すべての上に位置する小さなジョブに注目が集まりました。すべての実行は、PRがどのパスを変更したか、およびそれらのテストが同じ入力に対して既にパスしたかどうかを確認することから始まります。スキップされた作業がランナーを予約しないようにジョブレベルでゲートしていますが、それによりそれらはクリティカルパスに直接配置されます。8つのAPIテストシャーディングのいずれも、それらが完了するまで開始できないため、わずかな遅延でも不釣り合いに重要になります。 各ジョブが必要とするものだけを取得 いくつかのワークフローは、次に何を実行するかを決定する変更検出ジョブから始まります。たとえば、差分にデータベースマイグレーションが含まれているかどうかを確認し、関連するデータベースCIチェックをスケジュールするために使用される信号を出力します。これらのジョブは、一部のサブセットしか必要としていなかったにもかかわらず、完全な作業ツリーをチェックアウトしていました。フェッチの深さを制限したことで、これらのゲートの中で最も遅いものが94秒から20秒になり、作業ツリーを必要としないジョブからはチェックアウトを完全に削除し、それらに費やされていた時間を27秒から7秒に削減しました。パスの差分を確認する必要があるコミットプッシュおよびマージキューイベントでは、限定的な履歴を持つスパースでブロブレスなチェックアウトで十分であることがわかり、さらに11秒ほど節約できました。 変更検出ジョブの中央値期間は26秒から8秒に、p90は31秒から12秒に、最も遅い実行は138秒から37秒に低下しました。 チェックアウトの耐性を向上 基盤となるランナーインフラストラクチャを切り替えた後、ジョブでのチェックアウト時間(actions/checkoutを使用)が長くなり、ハングすることがあることに気づきました。サードパーティランナーはGitHubのネットワーク外にあるため、GitHubに到達するための直接IPリンクに依存しています。プロバイダーは、ハングをそのリンクの断続的な劣化に起因すると特定しました。ワークフローの多くはチェックアウトから始まるため、フェッチが遅延するとCI実行全体が遅れる可能性があります。 ネットワークの不安定性に対する耐性を確保するため、actions/checkoutを、バックオフで再試行するコンポジットアクションに置き換えました。これにより、GIT_HTTP_LOW_SPEED_LIMITとGIT_HTTP_LOW_SPEED_TIMEが設定され、ハングする代わりに約30秒後に遅延した接続が中止され、チェックアウトキャッシュも使用されるようになります。これにより、チェックアウトの完了を待ってクリティカルパスのジョブがアイドル状態になる実行が大幅に減少しました。 クリティカルパス上のものを最小限に抑える クリティカルパス上のすべてのジョブがそこにある必要はありませんでした。マージ前の最終チェックの一部としてキャッシュマーカーを書き込んでいたため、テストがパスした後でもプルリクエストがマージキューに残る可能性がありました。これを、テストシャーディングが完了した後に実行されるが何もゲートしないジョブに移動したことで、APIプルリクエストおよびマージキューエントリごとにマージパスから42秒を削減しました。 これらを組み合わせることで、キャッシュミス時のAPIプルリクエストに必要なチェックから約1分が短縮され、ランナースタートも削減されました。 繰り返しのセットアップを削減 次に、ランナーの起動、パッケージのインストール、ビルド依存関係のプロビジョニングなど、すべてのジョブで繰り返されるセットアップコストに焦点を当てました。このオーバーヘッドにより、数秒しか実質的な作業を行わないジョブでも、数分間のインフラストラクチャ時間を消費する可能性があります。この問題に対処するために取ったいくつかのステップを次に示します。 CIイメージでの共有依存関係の事前インストール APIテストシャーディングは、各実行でaptを使用して同じPostgresクライアントをインストールするのに7〜8秒費やしていました。これを、Nodeとクライアントを含む小さなCIベースイメージに移動したため、各シャーディングは実行準備のできた環境から開始できました。その後、セットアップ中にダウンロードするとハングする可能性があることが判明したため、必要なネイティブビルドヘッダーをイメージに追加し、テールを短縮しました。 各ジョブが必要とする依存関係のみをインストール Linearのコードベースは、pnpmワークスペースとして管理されているモノレポです。APIテストワークフローは、APIパッケージとその依存関係のみを必要としていたにもかかわらず、ワークスペース全体をインストールしていました。APIパッケージにインストールを制限したことで、pnpm installは44〜73秒から16〜18秒に削減されました。APIに近いジョブにも同じパターンを適用し、それらはリポジトリ全体をインストールし、後でほとんど使用されない依存関係キャッシュをアップロードしていました。 再構築するよりもキャッシュする方が速くない場合はキャッシュしない node_modulesのキャッシュもテストしましたが、再構築する方が速いことがわかりました。キャッシュキーは頻繁に変更されるロックファイルに依存しており、キャッシュヒットでも復元に約28秒かかりましたが、フィルタリングされたインストールでは約7.5秒でした。キャッシュは、目に見える利点をもたらすことなく、保存時間と変動性を追加していました。 これら3つの変更により、シャーディングあたりのセットアップ時間は約44%、つまり110〜140秒から67〜73秒に削減されました。 テストシャーディングのp95期間 これ以外にも、回避できる繰り返しのセットアップ形式がありました。 変更されていないセットアップの再生を回避 一部のセットアップ作業は、入力が変更された場合にのみ繰り返す必要があります。