永続ワーカー

このページでは、永続的なワーカーの使用方法、メリット、要件、ワーカーがサンドボックスに与える影響について説明します。

永続ワーカーは、Bazel サーバーによって開始される長時間実行プロセスです。これは、実際のツール(通常はコンパイラ)のラッパーまたはツールとして機能します。永続的なワーカーでメリットを生かすには、ツールで一連のコンパイルを実行でき、ラッパーがツールの API と下記のリクエスト/レスポンスの形式の間で変換する必要があります。同じワーカーで、--persistent_worker フラグの有無にかかわらず同じワーカーが呼び出され、ツールの起動と呼び出しを適切に行い、終了時にワーカーをシャットダウンする役割があります。各ワーカー インスタンスには <outputBase>/bazel-workers の下にある個別の作業ディレクトリが割り当てられます(root 権限は取得されません)。

永続ワーカーの使用は、起動オーバーヘッドを削減し、より多くの JIT コンパイルを可能にし、アクション実行における抽象構文ツリーのキャッシュ保存などを可能にする実行戦略です。この戦略では、長時間実行プロセスに複数のリクエストを送信することで、これらの改善を実現します。

永続ワーカーは、Java、ScalaKotlin などの複数の言語に対して実装されます。

NodeJS ランタイムを使用するプログラムでは、@bazel/worker ヘルパー ライブラリを使用してワーカー プロトコルを実装します。

永続ワーカーの使用

Bazel 0.27 以降では、ビルドを実行するときにデフォルトで永続ワーカーが使用されますが、リモート実行が優先されます。永続ワーカーをサポートしていないアクションの場合、Bazel は各アクションのツール インスタンスを起動します。該当するツールのニーモニックに worker 戦略を設定することで、永続ワーカーを使用するようにビルドを明示的に設定できます。ベスト プラクティスとして、次の例では、worker 戦略へのフォールバックとして local を指定しています。

bazel build //my:target --strategy=Javac=worker,local

ローカル戦略の代わりにワーカー戦略を使用すると、実装によってはコンパイル速度が大幅に向上します。Java の場合、ビルドが 2 ~ 4 倍速くなり、増分コンパイルではさらに速くなる可能性があります。Bazel でのコンパイルの速度は、ワーカーの約 2.5 倍です。詳細については、「ワーカーの数を選択する」をご覧ください。

ローカルビルド環境と一致するリモート ビルド環境がある場合、試験運用版の動的戦略を使用して、リモート実行とワーカー実行を実行できます。動的戦略を有効にするには、--experimental_spawn_scheduler フラグを渡します。この戦略ではワーカーが自動的に有効になるため、worker 戦略を指定する必要はありませんが、代わりに local または sandboxed をフォールバックとして使用できます。

ワーカーの数を選択する

ニーモニックあたりのワーカー インスタンスのデフォルト数は 4 ですが、worker_max_instances フラグで調整できます。使用可能な CPU を有効に活用することと、取得する JIT コンパイルとキャッシュ ヒットの量にはトレードオフの関係があります。ワーカーが増えると、ターゲットを増やすと、非 JIT 付きコードを実行してコールド キャッシュに達する際の起動コストが増加します。ビルドするターゲットの数が少ない場合、1 つのワーカーでコンパイル速度とリソースの使用量のバランスが最適になります(たとえば、問題 #8586 を参照)。 worker_max_instances フラグでは、ニーモニックあたりの最大ワーカー インスタンス数とフラグセット(下記参照)が設定されます。そのため、混合型システムでは、デフォルト値をそのまま使用すると大量のメモリが使用される可能性があります。増分ビルドの場合、複数のワーカー インスタンスのメリットはさらに小さくなります。

このグラフは、64 GB の RAM を搭載した 6 コアのハイパースレッド Intel Xeon 3.5 GHz Linux ワークステーションでの Bazel(ターゲット //src:bazel)のゼロしてからのコンパイル時間を示しています。ワーカー構成ごとに、5 つのクリーンビルドが実行され、最後の 4 つが取得されます。

クリーンビルドのパフォーマンスの向上を示すグラフ

図 1. クリーンビルドのパフォーマンスの向上を示すグラフ。

この構成では、2 つのワーカーが最もコンパイルが高速ですが、1 つのワーカーと比較して 14% の改善にすぎません。メモリ使用量を減らしたい場合は、1 つのワーカーを使用することをおすすめします。

通常、増分コンパイルにはさらにメリットがあります。クリーンビルドは比較的まれですが、特にテストドリブンな開発では、コンパイル時に 1 つのファイルを変更することが一般的です。上記の例には、Java 以外のパッケージ化アクションもあり、増分コンパイル時間を隠すことができます。

AbstractContainerizingSandboxedSpawn.java で内部文字列定数を変更した後に Java ソースのみを再コンパイル(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar)すると、3 倍の高速化が可能です(1 つのウォームアップ ビルドが破棄され、平均 20 の増分ビルドが行われます)。

増分ビルドのパフォーマンスの向上を示すグラフ

図 2. 増分ビルドのパフォーマンスの向上を示すグラフ。

高速化は変更の内容によって変わります。上記の状況では、よく使用される定数を変更すると、係数 6 の高速化が測定されます。

永続ワーカーの変更

--worker_extra_flag フラグを渡して、起動フラグをニーモニック キーでワーカーに指定できます。たとえば、--worker_extra_flag=javac=--debug を渡すと、Javac のデバッグのみが有効になります。このフラグの使用ごとに設定できるワーカーフラグは 1 つだけで、ニーモニックごとに 1 つだけです。ワーカーは、ニーモニックごとに個別にだけでなく、起動フラグのバリエーションに対しても作成されます。ニーモニック フラグと起動フラグの各組み合わせは WorkerKey に結合され、WorkerKey ごとに最大 worker_max_instances 個のワーカーを作成できます。アクション構成で設定フラグを指定する方法については、次のセクションをご覧ください。

通常の優先度のニーモニックよりも優先して実行されるニーモニックを指定するには、--high_priority_workers フラグを使用します。これにより、常にクリティカル パスに存在するアクションに優先順位を付けることができます。リクエストを実行している優先度の高いワーカーが 2 つ以上ある場合、他のワーカーはすべて実行されません。このフラグは複数回使用できます。

--worker_sandboxing フラグを渡すと、各ワーカー リクエストですべての入力に対して個別のサンドボックス ディレクトリが使用されます。サンドボックスのセットアップには特に macOS で時間がかかりますが、正確性は保証されます。

--worker_quit_after_build フラグは、主にデバッグとプロファイリングに役立ちます。このフラグは、ビルドが完了するとすべてのワーカーを強制終了します。また、--worker_verbose を渡して、ワーカーの動作に関する出力をさらに取得することもできます。このフラグは WorkRequestverbosity フィールドに反映されるため、ワーカーの実装も詳細になります。

ワーカーは、ログを <outputBase>/bazel-workers ディレクトリ(/tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log など)に保存します。ファイル名にはワーカー ID とニーモニックが含まれています。ニーモニックごとに複数の WorkerKey が存在する可能性があるため、特定のニーモニックに対応するログファイルが worker_max_instances を超えることがあります。

Android ビルドの場合は、Android Build Performance ページで詳細をご覧ください。

永続ワーカーの実装

ワーカーの作成方法の詳細については、永続ワーカーの作成ページをご覧ください。

次の例は、JSON を使用するワーカーの Starlark 構成を示しています。

args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
ctx.actions.write(
    output = args_file,
    content = "\n".join(["-g", "-source", "1.5"] + ctx.files.srcs),
)
ctx.actions.run(
    mnemonic = "SomeCompiler",
    executable = "bin/some_compiler_wrapper",
    inputs = inputs,
    outputs = outputs,
    arguments = [ "-max_mem=4G",  "@%s" % args_file.path],
    execution_requirements = {
        "supports-workers" : "1", "requires-worker-protocol" : "json" }
)

この定義では、このアクションの最初の使用は、コマンドライン /bin/some_compiler -max_mem=4G --persistent_worker の実行から始まります。Foo.java をコンパイルするリクエストは次のようになります。

arguments: [ "-g", "-source", "1.5", "Foo.java" ]
inputs: [
  {path: "symlinkfarm/input1" digest: "d49a..." },
  {path: "symlinkfarm/input2", digest: "093d..."},
]

ワーカーはこれを stdin で改行区切りの JSON 形式で受け取ります(requires-worker-protocol は JSON に設定されているため)。その後、ワーカーはアクションを実行し、JSON 形式の WorkResponse を stdout で Bazel に送信します。Bazel はこのレスポンスを解析し、手動で WorkResponse proto に変換します。JSON の代わりにバイナリ エンコードの protobuf を使用して関連するワーカーと通信するには、requires-worker-protocol を次のように proto に設定します。

  execution_requirements = {
    "supports-workers" : "1" ,
    "requires-worker-protocol" : "proto"
  }

実行要件に requires-worker-protocol を含めない場合、Bazel はデフォルトで protobuf を使用するようにワーカー通信を行います。

Bazel はニーモニックと共有フラグから WorkerKey を導出するため、この構成で max_mem パラメータの変更が許可されている場合は、使用する値ごとに個別のワーカーが生成されます。このため、使用するバリエーションが多すぎると、メモリの過剰消費につながることがあります。

各ワーカーは現在、一度に 1 つのリクエストしか処理できません。試験運用版のマルチプレックス ワーカー機能では、基盤となるツールがマルチスレッドを使用していて、それを理解するためにラッパーが設定されている場合、複数のスレッドを使用できます。

この GitHub リポジトリには、Java と Python で記述されたサンプル ワーカー ラッパーがあります。JavaScript または TypeScript で作業している場合は、@bazel/worker パッケージnodejs ワーカーの例が役に立ちます。

ワーカーはサンドボックスにどう影響しますか?

worker 戦略をデフォルトを使用しても、local 戦略と同様に、アクションはサンドボックスでは実行されません。サンドボックス内ですべてのワーカーを実行するように --worker_sandboxing フラグを設定できます。これにより、ツールを実行するたびに、想定どおりの入力ファイルのみが表示されるようになります。ただし、このツールはキャッシュ内など、リクエスト間の情報を引き続き漏洩する可能性があります。dynamic 戦略を使用するには、ワーカーをサンドボックス化する必要があります

ワーカーでコンパイラ キャッシュを正しく使用できるようにするため、各入力ファイルとともにダイジェストが渡されます。そのため、コンパイラまたはラッパーは、ファイルを読み取ることなく、入力がまだ有効かどうかを確認できます。

入力ダイジェストを使用して、不要なキャッシュから保護する場合でも、サンドボックス ワーカーは純粋なサンドボックスよりも厳密なサンドボックス化を行います。これは、このツールが以前のリクエストの影響を受ける他の内部状態を保持する可能性があるためです。

Multiplex ワーカーをサンドボックス化できるのは、ワーカーの実装がサポートしている場合に限られます。このサンドボックス化は、--experimental_worker_multiplex_sandboxing フラグを使用して個別に有効にする必要があります。詳細については、設計ドキュメントをご覧ください。

関連情報

永続ワーカーの詳細については、以下をご覧ください。