分散ビルド

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

リモート キャッシング

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

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

図 1。リモート キャッシングを示す分散ビルド

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

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

もちろん、リモート キャッシュのメリットを得るには、アーティファクトのダウンロードがビルドよりも高速である必要があります。特に、キャッシュ サーバーがビルドを行うマシンから遠く離れている場合は、そうでないことがあります。 Google の ネットワークとビルドシステムは、ビルド 結果を迅速に共有できるように慎重に調整されています。

リモート実行

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

リモート実行システム

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

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

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

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

Google の分散ビルド

2008 年以降、Google は リモート キャッシングとリモート実行の両方を使用する分散ビルドシステムを使用しています。図 3 に示します。

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

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

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

Google のリモート実行システムは Forge と呼ばれます。Blaze (Bazel の内部相当)の Forge クライアントである Distributor は、各アクションのリクエストを、Scheduler というデータセンターで実行されているジョブに送信します。Scheduler はアクションの結果のキャッシュを保持しているため、システム内の他のユーザーがすでにアクションを作成している場合は、すぐにレスポンスを返すことができます。そうでない場合は、アクションを キューに入れます。Executor ジョブの大規模なプールは、このキューからアクションを継続的に読み取り、 実行して、結果を ObjFS Bigtable に直接保存します。これらの 結果は、今後のアクションで実行者が利用できるほか、objfsd を介してエンドユーザーがダウンロード することもできます。

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