永続ワーカー

問題を報告 ソースを表示

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

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

永続ワーカーの使用は、起動時のオーバーヘッドを削減し、より多くの 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 フラグを渡すと、各ワーカー リクエストはすべての入力に個別のサンドボックス ディレクトリを使用します。sandboxをセットアップすると、(特に macOS の場合)多少の時間がかかりますが、正確性の保証が向上します。

--worker_quit_after_build フラグは、主にデバッグとプロファイリングに役立ちます。このフラグは、ビルドの完了時にすべてのワーカーを強制的に終了します。--worker_verbose を渡して、ワーカーの処理内容に関する追加の出力を取得することもできます。このフラグは WorkRequestverbosity フィールドに反映され、ワーカーの実装も冗長化されます。

ワーカーはログを <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-protocolproto に設定します。

  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 戦略と同様に、sandboxではアクションは実行されません。--worker_sandboxing フラグを設定すると、サンドボックス内のすべてのワーカーを実行できます。これにより、ツールを実行するたびに必要な入力ファイルのみを参照できます。ただし、このツールは、キャッシュを通じてなど、リクエスト間で内部で情報をリークする可能性があります。dynamic 戦略を使用するには、ワーカーをサンドボックス化する必要があります

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

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

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

参考資料

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