分散ビルド

問題を報告 ソースを表示

コードベースが大きい場合、依存関係のチェーンが非常に深くなる可能性があります。単純なバイナリでさえ、数万ものビルド ターゲットに依存することが少なくありません。この規模では、1 台のマシンで妥当な時間でビルドを完了することはほぼ不可能です。ビルドシステムでは、マシンのハードウェアにかかる物理学の基本法則を回避できます。これを機能させるには、分散ビルドをサポートするビルドシステムを使用するしかありません。この場合、システムによって行われる作業単位が任意のスケーラブルな数のマシンに分散されます。システムの処理を十分な小さな単位に分割したと仮定すると(これについては後述します)、これにより、あらゆるサイズのビルドを可能な限り迅速に完了できます。このスケーラビリティは 私たちがこれまで アーティファクトベースのビルドシステムを定義してきた

リモート キャッシュ

最も単純な分散ビルドは、図 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 の内部で同等のもの)が、各アクションのリクエストを、スケジューラと呼ばれるデータセンターで実行されているジョブに送信します。スケジューラはアクション結果のキャッシュを保持し、そのアクションがシステムの他のユーザーによってすでに作成されている場合は、すぐにレスポンスを返すことができます。そうでない場合は、アクションをキューに入れます。エグゼキュータ ジョブの大規模なプールが、このキューからアクションを読み取り、実行し、結果を ObjFS Bigtable に直接保存します。これらの結果は、今後のアクションのためにエグゼキュータで使用できます。また、エンドユーザーは objfsd を介してダウンロードすることもできます。

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