このページでは、永続ワーカーの使用方法、メリット、要件、ワーカーがサンドボックス化に与える影響について説明します。
永続ワーカーとは、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 つのワーカーをおすすめします。
通常、増分コンパイルはさらに効果的です。クリーンビルドは比較的まれですが、特にテスト駆動開発では、コンパイルの合間に単一のファイルを変更することが一般的です。上記の例には、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
フラグを渡すと、各ワーカー リクエストはすべての入力に個別のサンドボックス ディレクトリを使用します。サンドボックスのセットアップは、特に 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..." },
],
}
ワーカーは、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
戦略を使用するには、ワーカーをサンドボックス化する必要があります。
ワーカーでコンパイラ キャッシュを正しく使用できるように、各入力ファイルとともにダイジェストが渡されます。したがって、コンパイラまたはラッパーは、ファイルを読み取ることなく入力がまだ有効かどうかを確認できます。
入力ダイジェストを使用して不要なキャッシュを防ぐ場合でも、サンドボックス化されたワーカーは、ツールが以前のリクエストの影響を受けた他の内部状態を保持する可能性があるため、純粋なサンドボックスよりも厳格なサンドボックス化を提供しません。
マルチプレックス ワーカーをサンドボックス化できるのは、ワーカーの実装がサンドボックス化をサポートしている場合のみです。このサンドボックス化は、--experimental_worker_multiplex_sandboxing
フラグで個別に有効にする必要があります。詳細については、設計ドキュメントをご覧ください。
関連情報
永続ワーカーの詳細については、以下をご覧ください。
- 永続的なワーカーに関する元のブログ投稿
- Haskell 実装の説明 {: .external}
- Mike Morearty によるブログ投稿{: .external}
- Bazel を使用したフロントエンド開発: Asana を使用した Angular / TypeScript と永続ワーカー {: .external}
- Bazel 戦略の説明{: .external}
- bazel-discuss メーリング リストにおける、情報量の多い従業員戦略に関する議論 {: .external}