分散ビルド

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

リモート キャッシング

最も単純なタイプの分散ビルドは、図 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 に直接保存します。これらの 結果は、今後のアクションで Executor が使用したり、エンドユーザーが objfsd 経由でダウンロード したりできます。

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