分散ビルド

コードベースが大きくなると、依存関係のチェーンが非常に深くなることがあります。 単純なバイナリでも、数万のビルドターゲットに依存することがよくあります。この規模では、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(Bazel の内部相当)の Forge クライアントである Distributor は、各アクションのリクエストを、Scheduler というデータセンターで実行されているジョブに送信します。Scheduler はアクション結果のキャッシュを保持しているため、システム内の他のユーザーがすでにアクションを作成している場合は、すぐにレスポンスを返すことができます。そうでない場合は、アクションをキューに入れます。Executor ジョブの大規模なプールは、このキューからアクションを継続的に読み取り、実行して、結果を ObjFS Bigtable に直接保存します。これらの結果は、今後のアクションで実行されるか、objfsd を介してエンドユーザーがダウンロードできます。

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