分散ビルド

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

リモート キャッシュ

最も単純なタイプの分散ビルドは、図 1 に示すように、リモート キャッシュのみを利用するビルドです。

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

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

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

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

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

リモート実行

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

リモート実行システム

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

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

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

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

Google での分散ビルド

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

ビルドシステムの概要

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

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

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

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