永久工作站

回報問題 查看來源

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

永久工作站是一種由 Bazel 伺服器啟動的長時間執行程序,可做為實際工具 (通常是編譯器) 的包裝函式,或做為工具本身。為了受益於永久工作站,這項工具必須支援執行一系列編譯作業,且包裝函式必須在工具的 API 和以下要求/回應格式之間進行轉換。可能會在相同版本中使用或沒有 --persistent_worker 旗標來呼叫相同的工作站,且必須負責妥善啟動工具並與其通訊,以及在結束時關閉工作站。每個工作站執行個體都會指派給 <outputBase>/bazel-workers 下的獨立工作目錄 (但無法啟用 Root 權限)。

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

永久工作站已實作多種語言,包括 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 編譯和快取命中量的優勢。隨著工作站數量增加,更多目標必須支付啟動費用,也就是執行未 JIT 處理的程式碼,以及存取冷快取。如果您只須建構少量目標,則單一工作站可能會在編譯速度和資源用量之間取得最佳平衡 (例如請參閱問題 #8586)。worker_max_instances 旗標會設定每個記憶和旗標集的工作站執行個體數量上限 (如下所示),因此如果您保留預設值,在混合系統中可能會耗用大量記憶體。對漸進式建構而言,多個工作站執行個體的優勢甚至更小。

這張圖表顯示 Bazel (目標 //src:bazel) 在 6 核心超執行緒的 Intel Xeon 3.5 GHz Linux 工作站 (搭載 64 GB RAM) 上的擷取時間編譯時間。針對每個工作站設定,系統會執行五個清理建構作業,並採用最後四種建構作業的平均值。

顯示乾淨版本效能改善項目的圖表

圖 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,取得 worker 正在執行作業的更多輸出內容。這個旗標會反映在 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 的要求會如下所示:

注意:雖然通訊協定緩衝區規格使用「snake case」(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 參數,則每個使用的值都會產生不同的工作站。如果使用的變化版本過多,則可能導致記憶體用量過高。

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

您可以在這個 GitHub 存放區中查看以 Java 和 Python 編寫的工作站包裝函式範例。如果您使用 JavaScript 或 TypeScript,@bazel/worker 套件nodejs 工作站範例可能會有幫助。

工作站對沙箱作業有何影響?

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

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

即使使用輸入摘要來防範不必要的快取,沙箱工作站的沙箱強度也會低於純沙箱,因為工具可能會保留受先前要求影響的其他內部狀態。

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

其他資訊

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