分散ビルド

問題を報告する ソースを表示

コードベースが大きい場合、依存関係のチェーンがかなり深くなる可能性があります。単純なバイナリであっても、数万のビルド ターゲットに依存していることがよくあります。この規模では、1 台のマシンで妥当な時間内にビルドを完了することは不可能です。どのマシンも、マシンのハードウェアに課せられる物理物理の基本法を回避できません。これを行うための唯一の方法は、分散ビルドをサポートするビルドシステムを使用することです。ビルドシステムは、システムによって実行される作業単位が、任意でスケーラブルな数のマシンに分散されます。システムの作業が小さなユニットとして分割されていると仮定すると(後で詳しく説明します)、支払い可能な金額であれば、どのようなサイズのビルドでも完了できます。このスケーラビリティは、アーティファクトベースのビルドシステムを定義することで実現してきた聖なる目標です。

リモート キャッシュ

最もシンプルな分散ビルドは、図 1 に示すように、リモート キャッシュのみを使用するものです。

リモート キャッシュを使用した分散ビルド

図 1. リモート キャッシュを示す分散ビルド

デベロッパーのワークステーションと継続的インテグレーション システムの両方を含むビルドを実行するすべてのシステムは、共通のリモート キャッシュ サービスへの参照を共有します。このサービスは、Redis のような高速のローカル短期ストレージ システムや、Google Cloud Storage のようなクラウド サービスである可能性があります。ユーザーがアーティファクトを直接、または依存関係としてビルドする必要があるときは必ず、最初にリモート キャッシュがそのアーティファクトがすでに存在するかどうかを確認します。その場合、アーティファクトをビルドせずにダウンロードできます。それ以外の場合、システムはアーティファクト自体をビルドして、結果をキャッシュにアップロードし直します。つまり、頻繁に変更されない低レベルの依存関係を一度だけ作成して、ユーザー間でビルドし直すことなくユーザー間で共有できるためです。Google では、多くのアーティファクトをゼロから構築するのではなく、キャッシュから提供することで、ビルドシステムの実行コストを大幅に削減しています。

リモート キャッシュ システムが機能するためには、ビルドが完全に再現可能であることをビルドシステムが保証する必要があります。つまり、どのビルド ターゲットであっても、同じ入力セットがどのマシンでもまったく同じ出力を生成するように、そのターゲットへの入力セットを決定できる必要があります。これは、アーティファクトのダウンロード結果が、自分で作成した結果と同じになるようにする唯一の方法です。なお、この場合、キャッシュ内の各アーティファクトをそのターゲットと入力のハッシュの両方でキー付けする必要があります。これにより、異なるエンジニアが同じターゲットに対して同時に異なる変更を行い、リモート キャッシュでそれらのアーティファクトをすべて保存して、競合することなく適切に提供できるようになります。

当然ながら、リモート キャッシュの恩恵を受けるには、アーティファクトをビルドよりも速くダウンロードする必要があります。特に、ビルドを実行しているマシンからキャッシュ サーバーが遠い場合は、特に例外があります。Google のネットワークとビルドシステムは、ビルド結果を迅速に共有できるように慎重に調整されています。

リモート実行

リモート キャッシュは真の分散ビルドではありません。キャッシュが失われた場合や、すべてを再ビルドする必要がある低レベルの変更を行った場合でも、ビルド全体をマシン上でローカルに実行する必要があります。真の目標は、ビルドの実行の実際の作業を任意の数のワーカーに分散できるリモート実行をサポートすることです。図 2 は、リモート実行システムを示しています。

リモート実行システム

図 2. リモート実行システム

各ユーザーのマシン(人間のエンジニアまたは自動ビルドシステム)で実行されているビルドツールは、中央のビルド マスターにリクエストを送信します。ビルド マスターは、リクエストをコンポーネント アクションに分割し、スケーラブルなワーカープールでの実行の実行をスケジュールします。各ワーカーは、ユーザーが指定した入力によって要求された操作を行い、結果のアーティファクトを書き出します。これらのアーティファクトは、最終出力が生成されてユーザーに送信されるまで、アーティファクトを実行する他のマシン間で共有されます。

このようなシステムを実装するうえで最も難しいのは、ワーカー、マスター、ユーザーのローカルマシンの間の通信を管理することです。ワーカーは他のワーカーによって生成された中間アーティファクトに依存し、最終的な出力をユーザーのローカルマシンに返送する必要があります。これを行うには、前述の分散キャッシュの上に、各ワーカーが結果を書き込み、キャッシュから依存関係を読み取るようにすることで、ビルドを行います。マスターは、依存するすべての要素が終了するまでワーカーがブロックされます。その場合、ワーカーはキャッシュから入力を読み取ることができます。最終製品もキャッシュに保存されます。これにより、ローカルマシンでダウンロードできます。また、ワーカーがビルド前にその変更を適用できるように、ユーザーのソースツリーのローカルの変更をエクスポートする別の手段も必要です。

これを機能させるには、前述のアーティファクト ベースのビルドシステムのすべての部分を組み合わせる必要があります。人間が介入せずにワーカーを起動できるように、ビルド環境は完全に自己記述型でなければなりません。各ステップは異なるマシンで実行される場合があるため、ビルドプロセス自体は完全に自己完結している必要があります。各ワーカーが他のワーカーから受け取った結果を信頼できるように、出力は完全に確定的である必要があります。このような保証は、タスクベースのシステムにとって非常に困難であり、信頼性の高いリモート実行システムをその上に構築することはほぼ不可能です。

Google における分散ビルド

2008 年以来、Google はリモート キャッシュとリモート実行の両方を使用する分散ビルドシステムを使用しています(図 3 を参照)。

ハイレベル ビルドシステム

図 3. Google の分散ビルドシステム

Google のリモート キャッシュは ObjFS と呼ばれます。これは、本番環境マシンのフリート全体に分散された Bigtable にビルド出力を保存するバックエンドと、各デベロッパーのマシンで実行される objfsd という名前のフロントエンド FUSE デーモンで構成されています。FUSE デーモンを使用すると、エンジニアはワークステーションに通常のファイルを格納しているかのようにビルド出力を参照できますが、ファイル コンテンツはユーザーから直接リクエストされた少数のファイルに対してのみオンデマンドでダウンロードされます。ファイルのコンテンツをオンデマンドで提供すると、ネットワークとディスクの使用量が大幅に削減され、すべてのビルド出力をデベロッパーのローカル ディスクに保存したときの 2 倍の速さでシステムを構築できます。

Google のリモート実行システムは Forge と呼ばれています。ディストリビューターと呼ばれる Blaze の Forge クライアント(Bazel の内部同等のもの)は、スケジューラと呼ばれるデータセンターで実行中のジョブに各アクションのリクエストを送信します。スケジューラは、アクション結果のキャッシュを保持し、アクションがシステムの他のユーザーによってすでに作成されている場合には、すぐにレスポンスを返すようにします。存在しなければ、アクションをキューに追加します。エグゼキュータ ジョブの大規模なプールは、このキューからアクションを継続的に読み取り、実行して、結果を ObjFS Bigtable に直接保存します。これらの結果は、エグゼキュータが将来のアクションのために利用したり、objfsd を介してエンドユーザーがダウンロードしたりできます。

最終的に、Google で実行されるすべてのビルドを効率的にサポートするようにスケーリングされるシステムが作成されます。Google のビルドの規模は本当に巨大です。Google は、何百万ものビルドを実行し、何百万ものテストケースを実行し、毎日数十億行のソースコードからペタバイト規模のビルド出力を生成しています。このようなシステムにより、エンジニアは複雑なコードベースを迅速に構築できるだけでなく、Google のビルドに依存する大量の自動ツールやシステムを実装できます。