このページでは、永続ワーカーの使用方法、メリット、要件、ワーカーがサンドボックス化に与える影響について説明します。
永続ワーカーは、Bazel サーバーによって開始される長時間実行プロセスです。これは、実際のツール(通常はコンパイラ)のラッパーとして機能するか、
ツール自体として機能します。永続ワーカーのメリットを得るには、ツールがコンパイルのシーケンスの実行をサポートしている必要があります。また、ラッパーはツールの API
と、後述のリクエスト/レスポンス形式の間で変換を行う必要があります。同じビルド内で --persistent_worker
フラグの有無にかかわらず同じワーカーが呼び出されることがあります。ワーカーは、ツールの適切な起動と通信、終了時のワーカーのシャットダウンを行います。各ワーカーインスタンスには、
`<outputBase>/bazel-workers`<outputBase>/bazel-workersの下に個別の作業ディレクトリが割り当てられます(chroot はされません)。
永続ワーカーの使用は、起動時のオーバーヘッドを削減し、JIT コンパイルを増やし、アクション実行で抽象構文木などのキャッシュを有効にする実行戦略です。この戦略では、長時間実行プロセスに複数のリクエストを送信することで、これらの改善を実現します。
永続ワーカーは、Java、 Scala、 Kotlin など、複数の言語で実装されています。
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 ですが、
with the
worker_max_instances
フラグで調整できます。利用可能な CPU の有効活用と、JIT
コンパイルの量とキャッシュヒットの量の間にはトレードオフがあります。ワーカーが多いほど、JIT
コンパイルされていないコードを実行してコールド キャッシュにヒットする起動コストが発生するターゲットが増えます。ビルドするターゲットの数が少ない場合は、1 つのワーカーでコンパイル速度とリソース使用量の最適なトレードオフを実現できます(例:
問題 #8586を参照)。
worker_max_instances
フラグは、ニーモニックとフラグセットあたりのワーカー インスタンスの最大数を設定します(下記を参照)。そのため、混合システムでは、デフォルト値を維持すると、かなりの量のメモリを使用する可能性があります。増分ビルドの場合、複数のワーカー
インスタンスのメリットはさらに小さくなります。
このグラフは、6 コアのハイパースレッディング Intel Xeon 3.5 GHz Linux ワークステーション(64 GB の RAM)での
Bazel(ターゲット //src:bazel)のゼロからのコンパイル時間を示しています。ワーカー構成ごとに、5
つのクリーンビルドが実行され、最後の 4 つの平均が取得されます。

図 1.クリーンビルドのパフォーマンス改善のグラフ。
この構成では、2 つのワーカーで最速のコンパイルが実現しますが、1 つのワーカーと比較して改善率は 14% にすぎません。使用するメモリを減らしたい場合は、1 つのワーカーが適しています。
通常、増分コンパイルではさらにメリットがあります。クリーンビルドは比較的まれですが、コンパイル間で 1 つのファイルを変更することは一般的です。特にテスト駆動開発では一般的です。上記の例では、Java 以外のパッケージング アクションも含まれているため、増分コンパイル時間が長くなる可能性があります。

図 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を渡すと、ワーカーの動作に関する詳細な出力が得られます。このフラグは WorkRequest の verbosity
フィールドに反映されるため、ワーカーの実装も詳細にできます。
ワーカーは、<outputBase>/bazel-workers ディレクトリにログを保存します(
例
/tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log)。ファイル名には、ワーカー
ID とニーモニックが含まれます。ニーモニックごとに複数の WorkerKey が存在するため、特定のニーモニックに対して
worker_max_instances 個を超えるログファイルが表示されることがあります。
Android ビルドについては、 Android ビルドのパフォーマンス ページで詳細をご覧ください。
永続ワーカーを実装する
ワーカーの作成方法については、永続ワーカーを作成するページで 詳細をご覧ください。
次の例は、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
をコンパイルするリクエストは次のようになります。
注: プロトコル バッファ仕様では「スネークケース」(request_id)が使用されますが、JSON プロトコルでは「キャメルケース」(requestId)が使用されます。このドキュメントでは、JSON の例ではキャメルケースを使用しますが、プロトコルに関係なくフィールドについて説明する場合はスネークケースを使用します。
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
ワーカーは、改行区切り JSON 形式で stdin にこれを受け取ります(requires-worker-protocol が JSON
に設定されているため)。次に、ワーカーはアクションを実行し、JSON 形式の WorkResponse を stdout に Bazel
に送信します。Bazel はこのレスポンスを解析し、手動で WorkResponse
プロトに変換します。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戦略を使用するには、ワーカーをサンドボックス化する必要があります
。
ワーカーでコンパイラ キャッシュを正しく使用できるように、各入力ファイルとともにダイジェストが渡されます。これにより、コンパイラまたはラッパーは、ファイルを読み取らずに入力が有効かどうかを確認できます。
入力ダイジェストを使用して不要なキャッシュを防ぐ場合でも、サンドボックス化されたワーカーは、純粋なサンドボックスよりも厳格なサンドボックス化を提供しません。これは、ツールが以前のリクエストの影響を受けた他の内部状態を保持している可能性があるためです。
マルチプレックス ワーカーをサンドボックス化できるのは、ワーカーの実装がサポートしている場合のみです。このサンドボックス化は、--experimental_worker_multiplex_sandboxing
フラグを使用して個別に有効にする必要があります。詳細については、
設計ドキュメントをご覧ください。
関連情報
永続ワーカーの詳細については、以下をご覧ください。