このページでは、永続ワーカーを使用する方法、そのメリット、要件、ワーカーがサンドボックスに及ぼす影響について説明します。
永続ワーカーは、Bazel サーバーによって開始される長時間実行プロセスで、実際のツール(通常はコンパイラ)のラッパーとして機能するか、ツール自体として機能します。永続ワーカーを利用するには、ツールで一連のコンパイルをサポートし、ラッパーでツールの API と以下で説明するリクエスト/レスポンス形式を変換する必要があります。同じビルド内で --persistent_worker
フラグの有無にかかわらず同じワーカーが呼び出される場合、ツールの適切な起動および通信、終了時にワーカーをシャットダウンする役割を担います。各ワーカー インスタンスには、<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 ですが、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 倍になります(平均 20 回の増分ビルドで 1 つのウォームアップ ビルドが破棄されます)。
図 2. 増分ビルドのパフォーマンス改善のグラフ。
処理の迅速化は、加える変更によって異なります。上記の状況では、よく使用される定数が変更されると、係数 6 の高速化が測定されます。
永続ワーカーの変更
--worker_extra_flag
フラグを渡して、ニーモニックをキーとしてワーカーに起動フラグを指定できます。たとえば、--worker_extra_flag=javac=--debug
を渡すと、Javac のデバッグのみが有効になります。このフラグの使用ごとに設定できるワーカーフラグは 1 つのみで、1 つのニーモニックに対してしか設定できません。ワーカーはニーモニックごとに個別に作成されるだけでなく、起動フラグのバリエーションに対しても作成されます。ニーモニックと起動フラグの各組み合わせは WorkerKey
に結合され、WorkerKey
ごとに最大 worker_max_instances
個のワーカーを作成できます。アクション構成でセットアップ フラグも指定する方法については、次のセクションをご覧ください。
--high_priority_workers
フラグを使用して、通常の優先度のニーモニックよりも優先して実行するニーモニックを指定できます。これにより、常にクリティカル パスにあるアクションを優先できます。リクエストを実行している優先度の高いワーカーが 2 つ以上ある場合、他のすべてのワーカーは実行されません。このフラグは複数回使用できます。
--worker_sandboxing
フラグを渡すと、各ワーカー リクエストですべての入力に個別のサンドボックス ディレクトリが使用されます。sandboxのセットアップは、特に macOS の場合に余分な時間がかかりますが、正確性の保証が向上します。
--worker_quit_after_build
フラグは、主にデバッグとプロファイリングに役立ちます。このフラグを指定すると、ビルドの完了時にすべてのワーカーが強制的に終了します。また、--worker_verbose
を渡して、ワーカーの動作に関するより多くの出力を取得することもできます。このフラグは WorkRequest
の verbosity
フィールドに反映され、ワーカーの実装でも詳細になります。
ワーカーは、ログを <outputBase>/bazel-workers
ディレクトリ(例: /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
)に保存します。ファイル名にはワーカー ID とニーモニックが含まれます。1 つのニーモニックに複数の 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..." },
],
}
ワーカーは stdin
で、改行区切りの JSON 形式でこれを受信します(requires-worker-protocol
が JSON に設定されているため)。ワーカーはアクションを実行し、stdout で JSON 形式の WorkResponse
を 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 つのリクエストしか処理できません。試験運用版の Multiplex ワーカー機能では、基盤となるツールがマルチスレッドであり、ラッパーがこれを理解するように設定されていれば、複数のスレッドを使用できます。
この GitHub リポジトリでは、Java と Python で記述されたワーカー ラッパーの例を確認できます。JavaScript または TypeScript で作業している場合は、@bazel/worker パッケージと nodejs ワーカーの例が役立つ可能性があります。
ワーカーがサンドボックス化に与える影響
local
戦略と同様に、デフォルトでは worker
戦略を使用しても、sandboxではアクションは実行されません。サンドボックス内ですべてのワーカーを実行するように --worker_sandboxing
フラグを設定すると、ツールを実行するたびに、本来あるべき入力ファイルのみが認識されるようになります。このツールは、キャッシュなどを通じて、リクエスト間で内部的に情報を漏洩する可能性があります。dynamic
戦略を使用するには、ワーカーをサンドボックス化する必要があります。
ワーカーでコンパイラ キャッシュを正しく使用できるようにするため、各入力ファイルとともにダイジェストが渡されます。したがって、コンパイラまたはラッパーは、ファイルを読み取ることなく、入力がまだ有効かどうかを確認できます。
入力ダイジェストを使用して不要なキャッシュから保護している場合でも、サンドボックス ワーカーでは、以前のリクエストの影響を受けた他の内部状態を保持する可能性があるため、サンドボックス ワーカーでは純粋なサンドボックスよりも厳密なサンドボックス化は行われません。
Multiplex ワーカーをサンドボックス化できるのは、ワーカーの実装でサポートされている場合のみです。このサンドボックス化は、--experimental_worker_multiplex_sandboxing
フラグを使用して個別に有効にする必要があります。詳しくは、設計ドキュメントをご覧ください。
関連情報
永続ワーカーの詳細については、以下をご覧ください。
- 元の永続ワーカーに関するブログ投稿
- Haskell の実装の説明 {: .external}
- Mike Morearty によるブログ投稿 {: .external}
- Bazel を使用したフロントエンド開発: Angular / TypeScript と Asana を使用した永続ワーカー {: .external}
- Bazel の戦略の説明 {: .external}
- bazel-discuss メーリング リストにおける有益なワーカー戦略のディスカッション {: .external}