永久工作站

本頁面說明如何使用永久工作站、優點、需求,以及工作站對沙箱作業的影響。

永久工作站是指由 Bazel 伺服器啟動的長時間執行程序,其運作方式是在實際工具 (通常是編譯器) 周圍的「包裝函式」,或是由「工具」本身運作。為受惠於永久性工作站,工具必須支援一系列的編譯,且包裝函式必須在工具的 API 與下方所述的要求/回應格式之間進行轉譯。同一工作站可能會在相同建構作業中使用及不使用 --persistent_worker 旗標,且必須負責妥善啟動工具並與其通訊,以及在結束時關閉工作站。每個工作站執行個體都會在 <outputBase>/bazel-workers 下指派 (但不會分離) 一個獨立的工作目錄。

使用永久工作站是一種執行策略,能減少啟動負擔、允許更多 JIT 編譯,還能快取執行動作中的抽象語法樹狀結構。此策略會將多個要求傳送至長時間執行的程序,以達到這些改善。

Persistent Worker 可針對多種語言實作,包括 Java、ScalaKotlin 等。

使用 NodeJS 執行階段的程式可以使用 @bazel/worker 輔助程式庫實作工作站通訊協定。

使用永久工作站

Bazel 0.27 以上版本會在執行建構作業時使用永久工作站,但遠端執行的工作的優先順序較高。針對不支援持續性工作站的動作,Bazel 會改回針對各個動作啟動工具執行個體。您可以為適用的工具設定採用worker 策略,將建構明確設為使用永久工作站。以下範例包含指定 local 做為 worker 策略的備用方案:

bazel build //my:target --strategy=Javac=worker,local

視實作而定,採用工作站策略而非本機策略,將可大幅加快編譯速度。以 Java 來說,建構速度可加快 2 至 4 倍,有時則可以多完成漸進式編譯。處理 Bazel 的速度比使用工作站快 2.5 倍。詳情請參閱「選擇工作站數量」一節。

如果您也有與本機建構環境相符的遠端建構環境,則可使用實驗性的動態策略,這會競爭遠端執行和工作站執行作業。如要啟用動態策略,請傳送 --experimental_spawn_scheduler 標記。這項策略會自動啟用 worker,因此您不需要指定 worker 策略,但還是可以使用 localsandboxed 做為備用方案。

選擇工作站數量

每個助學器的預設工作站執行個體數量為 4 個,但可使用 worker_max_instances 標記進行調整。請權衡使用可用的 CPU,以及您取得的 JIT 編譯和快取命中數。隨著工作站越多,執行非 JITted 程式碼及碰到冷快取會產生啟動費用,越多目標就會產生費用。如果建構的目標數量不多,單一工作站可能會在編譯速度和資源用量之間取得最佳平衡,例如問題 #8586worker_max_instances 旗標會設定每個助測和旗標設定的工作站執行個體數量上限 (詳見下文),因此在混合的系統中,如果您保留預設值,最後可能會使用許多記憶體。如果是漸進式建構,多個工作站執行個體的優點甚至更小。

此圖表顯示在搭載 64 GB RAM 的 6 核心超執行緒 Intel Xeon 3.5 GHz Linux 工作站上,Bazel (目標 //src:bazel) 的當機編譯時間。每個工作站設定都會執行五個乾淨版本,其中平均值是最後四個版本。

顯示乾淨版本效能提升效能的圖表

圖 1:顯示乾淨版本效能提升效能的圖表。

就這項設定而言,兩個工作站的編譯速度最快,但比單一工作站的改善幅度只有 14%。如果希望使用較少記憶體,建議使用工作站。

一般來說,漸進式編譯組合能獲得更大效益。乾淨建構作業相對較少,但在編譯之間變更單一檔案是很常見的做法,尤其是在測試導向的開發作業中。上述範例也有一些非 Java 封裝動作,這可能會覆蓋漸進式的編譯時間。

在變更 AbstractContainerizingSandboxedSpawn.java 中的內部字串常數後,才會重新編譯 Java 來源 (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar),速度可加快 3 倍 (平均有 20 個漸進式建構捨棄一個暖機建構):

漸進式建構效能改善圖

圖 2. 漸進式建構效能改善圖表。

時間長短則視正在進行的變更而定。當常用常數發生變更時,系統就會在上述情況下測量因數 6 的速度加快速度。

修改永久工作站

您可以傳遞 --worker_extra_flag 標記,指定啟動標記給工作站,該標記由助眠輸入。舉例來說,傳遞 --worker_extra_flag=javac=--debug 只會開啟 Javac 的偵錯功能。使用此標記時,只能設定一個工作站旗標,且只能設定一個記憶。工作站並非只是為每項記憶內個別建立,還會在其啟動標記中使用不同的變化。每個助聽器和啟動標記的組合都會合併為 WorkerKey,且每個 WorkerKey 最多可以建立 worker_max_instances 個工作站。如要瞭解動作設定也能如何指定設定標記,請參閱下一節。

您可以使用 --high_priority_workers 標記指定應以一般優先順序記憶法執行的助力。這有助於優先處理總是在關鍵路徑中的動作。如果有兩個以上的高優先順序工作站在執行要求,系統會禁止其他所有工作站執行。這個標記可以多次使用。

傳遞 --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 和記憶法。由於每個助眠可能會超過一個 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。如要透過二進位編碼的 protobuf (而非 JSON) 與相關工作站通訊,requires-worker-protocol 會設為 proto,如下所示:

  execution_requirements = {
    "supports-workers" : "1" ,
    "requires-worker-protocol" : "proto"
  }

如果您未在執行要求中加入 requires-worker-protocol,Bazel 會預設工作站通訊使用 protobuf。

Bazel 會從記憶法和共用標記中衍生 WorkerKey,因此如果這項設定允許變更 max_mem 參數,就會為每個使用的值產生獨立的工作站。如果使用過多變化版本,這可能會導致記憶體消耗過多。

每個工作站目前一次只能處理一項要求。如果基礎工具為多執行緒,且包裝函式已設定瞭解這一點,則實驗性的 Multix worker 功能允許使用多個執行緒。

在這個 GitHub 存放區中,您可以看到以 Java 和 Python 編寫的工作站包裝函式範例。如果您是使用 JavaScript 或 TypeScript,請參考 @bazel/worker 套件nodejs 工作站範例

工作站對沙箱功能有何影響?

根據預設使用 worker 策略,系統不會在sandbox中執行動作,這點與 local 策略類似。您可以將 --worker_sandboxing 標記設為執行沙箱中的所有工作站,確保工具每次執行時都只會看到應有的輸入檔案。這項工具仍可能會在內部要求之間洩露資訊 (例如透過快取)。使用 dynamic 策略會要求工作站採用沙箱機制

為了允許在 worker 中正確使用編譯器快取,系統會隨每個輸入檔案一起傳送摘要。因此,編譯器或包裝函式可以檢查輸入內容是否仍然有效,而不必讀取檔案。

即使使用輸入摘要來防止不必要的快取,沙箱工作站仍提供的沙箱機制比純沙箱更嚴格,因為工具可能會保留先前要求影響的其他內部狀態。

只有在工作站實作支援多工工作站時,才能採用沙箱機制,而且您必須使用 --experimental_worker_multiplex_sandboxing 標記另外啟用這項沙箱機制。詳情請參閱設計文件)。

延伸閱讀

如要進一步瞭解永久工作站,請參閱: