永久工作站

回報問題 查看來源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

本頁說明如何使用持續性工作站、優點、需求,以及工作站對沙箱的影響。

永久工作站是由 Bazel 伺服器啟動的長期執行程序,可做為實際工具 (通常是編譯器) 的封裝函式,或本身就是工具。如要使用持續性工作站,工具必須支援執行一連串的編譯作業,而包裝函式則需在工具的 API 與下述要求/回應格式之間進行轉換。在同一個建構作業中,可能會使用 --persistent_worker 旗標呼叫同一個工作站,也可能不使用該旗標,而工作站負責適當啟動及與工具通訊,並在結束時關閉工作站。每個工作站執行個體都會指派 (但不會 chroot 至) <outputBase>/bazel-workers 下的個別工作目錄。

使用持續性工作者是執行策略,可減少啟動負擔、允許更多 JIT 編譯,並啟用快取 (例如動作執行中的抽象語法樹狀結構)。這項策略會向長時間執行的程序傳送多個要求,藉此達成上述改善目標。

我們為多種語言實作了持續性工作人員,包括 Java、ScalaKotlin 等。

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

使用永久工作站

Bazel 0.27 以上版本在執行建構作業時,預設會使用持續性工作站,但遠端執行作業的優先順序較高。如果動作不支援永續性工作站,Bazel 會改為為每個動作啟動工具執行個體。您可以為適用的工具助記符設定 worker strategy,明確將建構作業設為使用持續性工作站。最佳做法是在這個範例中指定 local 做為 worker 策略的回退:

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

視實作方式而定,使用工作人員策略而非本機策略,可大幅提升編譯速度。以 Java 來說,建構速度可提升 2 到 4 倍,有時漸進式編譯速度還會更快。使用工作站編譯 Bazel 的速度約快 2.5 倍。詳情請參閱「選擇工作人員人數」一節。

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

選擇工作站數量

每個助記符的預設工作站執行個體數量為 4,但可使用 worker_max_instances 標記調整。充分運用可用 CPU 與您獲得的 JIT 編譯和快取命中次數之間,存在取捨關係。工作站越多,更多目標就會支付執行非 JIT 編譯程式碼和命中冷快取的啟動費用。如果建構的目標數量不多,單一工作站或許能在編譯速度和資源用量之間取得最佳平衡 (例如,請參閱問題 #8586)。worker_max_instances 標記會為每個助記符和標記集設定工作站執行個體數上限 (請參閱下文),因此在混合系統中,如果您保留預設值,最終可能會使用大量記憶體。對於漸進式建構,多個工作站執行個體的優勢甚至更小。

這張圖表顯示在 6 核心超執行緒 Intel Xeon 3.5 GHz Linux 工作站上,從頭開始編譯 Bazel (目標 //src:bazel) 的時間,該工作站配備 64 GB 的 RAM。針對每項工作站設定,系統會執行五次乾淨的建構作業,並取最後四次的平均值。

清理建構作業的效能提升圖表

圖 1. 清理建構作業的效能提升圖表。

就這項設定而言,兩個工作站的編譯速度最快,但與一個工作站相比,速度只提升了 14%。如要減少記憶體用量,建議使用一個 worker。

增量編譯通常能帶來更多好處。清除建構作業相對少見,但編譯之間變更單一檔案很常見,尤其是在測試驅動開發中。上述範例也有一些非 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 旗標可讓每個工作站要求都使用獨立的沙箱目錄,處理所有輸入內容。設定沙箱需要額外時間,尤其是在 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 與相關聯的 Worker 通訊,而非 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 範例可能會有幫助。

Worker 對沙箱有何影響?

預設使用 worker 策略不會在沙箱中執行動作,與 local 策略類似。您可以設定 --worker_sandboxing 標記,在沙箱中執行所有工作人員,確保工具的每次執行只會看到應有的輸入檔案。工具仍可能在內部洩漏要求之間的資訊,例如透過快取。使用 dynamic 策略 需要將工作站設為沙箱

為確保編譯器快取能與工作站正確搭配使用,系統會連同每個輸入檔案傳遞摘要。因此,編譯器或包裝函式可以檢查輸入內容是否仍然有效,而不必讀取檔案。

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

只有在工作站實作支援的情況下,才能將多工工作站設為沙箱,且必須使用 --experimental_worker_multiplex_sandboxing 旗標分別啟用這項沙箱功能。詳情請參閱設計文件

延伸閱讀

如要進一步瞭解持續性工作站,請參閱: