コードベースが大きい場合、依存関係のチェーンが非常に深くなる可能性があります。単純なバイナリでさえ、数万のビルド ターゲットに依存することが少なくありません。この規模では、単一のマシンで妥当な時間内にビルドを完了することは不可能です。どのビルドシステムも、マシンのハードウェアに課せられる基本的な物理法則を回避することはできません。これを機能させるには、分散ビルドをサポートするビルドシステムを使用するしか方法がありません。この場合、システムによって行われる作業単位が任意のスケーラブルな数のマシンに分散されます。システムの作業を十分に小さい単位に分割したと仮定すると(これについては後で説明します)、任意のサイズのビルドを、支払う金額に応じて迅速に完了できます。このスケーラビリティは 私たちがこれまで アーティファクトベースのビルドシステムを定義してきた
リモート キャッシュ
最も単純な分散ビルドは、図 1 のリモート キャッシュのみを利用するものです。
図 1. リモート キャッシュを示す分散ビルド
ビルドを実行するすべてのシステム(デベロッパー ワークステーションと継続的インテグレーション システムの両方を含む)は、共通のリモート キャッシュ サービスへの参照を共有します。このサービスは、Redis などの高速でローカルな短期ストレージ システム、または Google Cloud Storage などのクラウド サービスです。ユーザーがアーティファクトを直接または依存関係としてビルドする必要がある場合、システムはまずリモート キャッシュでそのアーティファクトがすでに存在するかどうかを確認します。既存のアーティファクトが存在する場合は、ビルドする代わりにアーティファクトをダウンロードできます。存在しない場合、システムはアーティファクト自体をビルドし、結果をキャッシュにアップロードします。つまり、頻繁に変更されない低レベルの依存関係は、各ユーザーが再ビルドする必要なく、1 回ビルドしてユーザー間で共有できます。Google では、多くのアーティファクトがゼロからビルドされるのではなく、キャッシュから提供されるため、ビルドシステムの実行コストが大幅に削減されます。
リモート キャッシュ システムが機能するには、ビルドシステムがビルドを完全に再現可能にする必要があります。つまり、どのビルド ターゲットでも、同じ入力セットがどのマシンでもまったく同じ出力を生成するように、そのターゲットへの入力セットを決定できる必要があります。これは、アーティファクトをダウンロードした結果と自分でビルドした結果が同じであることを確認する唯一の方法です。これを行うには、キャッシュ内の各アーティファクトをターゲットと入力のハッシュの両方でキーにする必要があります。これにより、異なるエンジニアが同じターゲットに同時に異なる変更を加えることができ、リモート キャッシュは結果として得られるすべてのアーティファクトを保存し、競合することなく適切に提供します。
もちろん、リモート キャッシュを活用するには、アーティファクトのダウンロードがビルドよりも高速である必要があります。ただし、キャッシュ サーバーがビルドを行うマシンから離れている場合は、必ずしもそうとは限りません。Google のネットワークとビルドシステムは、ビルド結果を迅速に共有できるように慎重に調整されています。
リモート実行
リモート キャッシュは真の分散ビルドではありません。キャッシュが失われた場合や、すべてを再ビルドする必要がある低レベルの変更を行った場合でも、マシンでビルド全体をローカルに実行する必要があります。実際の目標は、リモート実行をサポートすることです。これにより、ビルドの実際の作業を任意の数のワーカーに分散できます。図 2 は、リモート実行システムを示しています。
図 2. リモート実行システム
各ユーザーのマシンで実行されているビルドツール(ユーザーは人間のエンジニアまたは自動ビルド システム)は、中央のビルドマスターにリクエストを送信します。ビルドマスターは、リクエストをコンポーネント アクションに分割し、スケーラブルなワーカープール全体でこれらのアクションの実行をスケジュールします。各ワーカーは、ユーザーが指定した入力を使用して、要求されたアクションを実行し、結果のアーティファクトを出力します。これらのアーティファクトは、最終的な出力を生成してユーザーに送信できるまで、それらを必要とするアクションを実行する他のマシン間で共有されます。
このようなシステムを実装する際の最も難しい部分は、ワーカー、マスター、ユーザーのローカルマシン間の通信を管理することです。ワーカーは、他のワーカーによって生成された中間アーティファクトに依存する場合があります。また、最終的な出力をユーザーのローカルマシンに送り返す必要があります。これを行うには、前述の分散キャッシュの上に構築し、各ワーカーが結果をキャッシュに書き込み、キャッシュから依存関係を読み取るようにします。マスターは、依存するすべてが終了するまでワーカーの続行をブロックします。終了すると、キャッシュから入力を読み取れるようになります。最終プロダクトもキャッシュに保存され、ローカルマシンがダウンロードできるようにします。また、ワーカーがビルド前に変更を適用できるように、ユーザーのソースツリー内のローカル変更をエクスポートする別の手段も必要です。
これを実現するには、前述のアーティファクト ベースのビルドシステムのすべての部分を統合する必要があります。ビルド環境は完全に自己記述型である必要があります。これにより、人間の介入なしでワーカーを起動できます。各ステップが異なるマシンで実行される可能性があるため、ビルドプロセス自体は完全に自己完結型である必要があります。各ワーカーが他のワーカーから受け取った結果を信頼できるように、出力は完全に確定的でなければなりません。このような保証は、タスクベースのシステムでは非常に提供しづらく、信頼性の高いリモート実行システムを構築することはほぼ不可能です。
Google での分散ビルド
2008 年以来、Google は、図 3 に示すように、リモート キャッシュとリモート実行の両方を採用する分散ビルドシステムを使用しています。
図 3. Google の分散ビルドシステム
Google のリモート キャッシュは ObjFS と呼ばれます。これは、本番環境マシンのフリート全体に分散された Bigtable にビルド出力を保存するバックエンドと、各デベロッパーのマシンで実行される objfsd という名前のフロントエンド FUSE デーモンで構成されています。FUSE デーモンを使用すると、エンジニアはワークステーションに保存されている通常のファイルのようにビルド出力を参照できますが、ユーザーが直接リクエストした少数のファイルに対してのみ、ファイル コンテンツがオンデマンドでダウンロードされます。ファイルのコンテンツをオンデマンドで提供すると、ネットワークとディスクの両方の使用量が大幅に削減され、すべてのビルド出力をデベロッパーのローカル ディスクに保存した場合に比べて、システムで 2 倍の速さでビルドできます。
Google のリモート実行システムは Forge と呼ばれています。Blaze の Forge クライアント(Bazel の内部同等物)である Distributor は、各アクションのリクエストを、Google のデータセンターで実行されている Scheduler というジョブに送信します。スケジューラはアクション結果のキャッシュを保持するため、システムの他のユーザーによってアクションがすでに作成されている場合は、すぐにレスポンスを返すことができます。空でない場合、アクションはキューに追加されます。Executor ジョブの大規模なプールが、このキューからアクションを継続的に読み取り、実行し、結果を ObjFS Bigtable に直接保存します。これらの結果は、実行者が将来のアクションに使用できます。また、エンドユーザーが objfsd を介してダウンロードすることもできます。
最終的には、Google で実行されるすべてのビルドを効率的にサポートするようにスケーリングされるシステムが完成します。Google のビルドの規模は非常に大きく、Google は毎日数百万件のビルドを実行し、数百万件のテストケースを実行し、数十億行のソースコードからペタバイト規模のビルド出力を生成しています。このようなシステムにより、エンジニアは複雑なコードベースを迅速に構築できるだけでなく、ビルドに依存する大量の自動化ツールとシステムを実装することもできます。