永続ワーカー

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

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

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

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

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

永続ワーカーを使用する

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

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を参照)。 The 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 以外のパッケージング アクションも含まれているため、増分コンパイル時間が長くなる可能性があります。

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を渡すと、ワーカーの動作に関する詳細な出力が得られます。このフラグは verbosity フィールドに WorkRequest反映されるため、ワーカーの実装も 詳細になります。

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

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

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

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

関連情報

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