分散ビルド

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

リモート キャッシュ

最も単純な分散ビルドは、リモート キャッシュのみを利用する分散ビルドです(図 1 を参照)。

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

図 1. リモート キャッシュを示す分散ビルド

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

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

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

リモート実行

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

リモート実行システム

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

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

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

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

Google での分散ビルド

Google は 2008 年からリモート キャッシュとリモート実行の両方を採用する分散ビルドシステムを使用してきました(図 3 を参照)。

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

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

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

Google のリモート実行システムは Forge と呼ばれます。ディストリビューターと呼ばれる Blaze の Forge クライアント(Bazel の内部で同等品)は、スケジューラというデータセンターで実行されているジョブに各アクションのリクエストを送信します。スケジューラは、アクションの結果のキャッシュを保持し、システムの他のユーザーによってアクションがすでに作成されている場合は、すぐにレスポンスを返すことができます。一致しない場合、アクションはキューに追加されます。大規模なエグゼキュータ ジョブのプールが、このキューからアクションを継続的に読み取り、実行して、結果を直接 ObjFS Bigtable に保存します。これらの結果は、エグゼキュータが将来のアクションのために利用するか、エンドユーザーが objfsd を介してダウンロードすることができます。

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