本頁說明如何使用持續性工作站、優點、需求,以及工作站如何影響沙箱。
永久工作者是 Bazel 伺服器啟動的長時間執行程序,可做為實際工具 (通常是編譯器) 的包裝函式,或做為工具本身。為了讓持續性工作者發揮效益,工具必須支援執行一系列編譯作業,而包裝函式需要在工具的 API 和下方所述要求/回應格式之間進行轉譯。系統可能會在同一個版本中使用和沒有 --persistent_worker
旗標呼叫相同的 worker,並負責啟動和通訊工具,以及結束時關閉工作站。每個 worker 例項都會指派 (但不會以 chroot 指派) <outputBase>/bazel-workers
底下的獨立工作目錄。
使用持續性工作者是一種執行策略,可減少啟動額外負擔、允許更多 JIT 編譯,並啟用動作執行作業中的抽象語法樹狀結構快取。這項策略會向長時間執行的程序傳送多個要求,進而達成這些改善。
持久性工作者已針對多種語言實作,包括 Java、Scala、Kotlin 等。
使用 NodeJS 執行階段的程式可使用 @bazel/worker 輔助程式庫來實作 worker 通訊協定。
使用持續性 worker
Bazel 0.27 以上版本在執行建構作業時預設會使用永久工作站,但會優先使用遠端執行。如果動作不支援永續性工作站,Bazel 會改為為每個動作啟動工具例項。您可以設定適用工具記憶的 worker
策略,明確將版本設為使用永久工作站。為了提供最佳做法,這個範例會將 local
指定為 worker
策略的備用方案:
bazel build //my:target --strategy=Javac=worker,local
使用 worker 策略而非本機策略,可大幅提升編譯速度 (視實作方式而定)。對於 Java,建構作業的速度可加快 2 至 4 倍,漸進式編譯的速度則可能更快。使用 worker 編譯 Bazel 的速度大約是 2.5 倍。詳情請參閱「選擇 worker 數量」一節。
如果您還有與本機建構環境相符的遠端建構環境,則可使用實驗性動態策略,這會與遠端執行和工作站執行作業競爭。如要啟用動態策略,請傳遞 --experimental_spawn_scheduler 標記。這項策略會自動啟用 worker,因此您不需要指定 worker
策略,但仍可使用 local
或 sandboxed
做為備用。
選擇工作站數量
每個助憶法的工作站執行個體預設數量為 4,但可以使用 worker_max_instances
標記進行調整。在充分利用可用 CPU 與取得的 JIT 編譯與快取命中之間存在取捨。工作站越多,執行非即時編譯程式碼和命中冷快取的目標就會支付越多啟動成本。如果要建構的目標數量不多,單一工作站可能會在編譯速度和資源使用之間取得最佳取捨 (例如請參閱問題 #8586)。worker_max_instances
標記會設定每個助記符和標記集的 worker 執行個體上限數量 (請參閱下方說明),因此在混合系統中,如果您保留預設值,可能會使用大量記憶體。對於漸進式建構作業,多個工作站執行個體的效益更低。
這張圖表顯示在 6 核心超執行緒 Intel Xeon 3.5 GHz Linux 工作站 (64 GB RAM) 上,Bazel (目標 //src:bazel
) 從頭開始編譯的時間。針對每個 worker 設定,系統會執行五次清除建構作業,並取最後四次的平均值。
圖 1. 清理建構作業的效能改善圖表。
針對這項設定,兩個工作站的編譯速度最快,但與一個工作站相比只提升 14%。如果想要更少記憶體,建議您使用一個工作站。
漸進式編譯通常能更完善。清除建構作業相對較少,但在編譯之間變更單一檔案的情況相當常見,特別是在以測試為導向的開發作業中。上述範例也有一些非 Java 封裝動作,可能會覆蓋漸進式編譯時間。
在 AbstractContainerizingSandboxedSpawn.java 中變更內部字串常數後,只重新編譯 Java 來源 (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
) 可加快 3 倍 (平均 20 次增量建構作業,其中 1 次為預熱建構作業):
圖 2. 漸進式建構作業的效能改善圖表。
速度取決於變更。在上述情況下,當常用的常數發生變化時,系統會以 6 倍的速度進行測量。
修改持續性 worker
您可以傳遞 --worker_extra_flag
標記,為工作站指定啟動標記,並以助憶法做為索引。舉例來說,傳遞 --worker_extra_flag=javac=--debug
只會開啟 Javac 的偵錯功能。每次使用此標記時,只能設定一個 worker 標記,且只能針對一個 mnemonic 設定。系統不僅會為每個助憶法分別建立工作站,也會為其啟動旗標的變化建立工作站。每個助憶鍵和啟動旗標的組合會合併為 WorkerKey
,每個 WorkerKey
最多可以建立 worker_max_instances
個工作站。請參閱下一節,瞭解動作設定如何指定設定標記。
您可以使用 --high_priority_workers
標記,指定應優先執行的助憶法,而非一般優先順序的助憶法。這麼做有助於將一律位於重要路徑中的動作設為優先。如果有兩個或更多高優先順序的 worker 執行要求,系統會禁止所有其他 worker 執行。這個標記可重複使用。
傳遞 --worker_sandboxing
標記後,每個工作站要求都會使用獨立的沙箱目錄處理其所有輸入內容。設定沙箱需要額外時間,尤其是在 macOS 上,但可提供更準確的正確性保證。
--worker_quit_after_build
標記主要用於偵錯和剖析。此標記會在建構作業完成後,強制所有工作者離開。您也可以傳遞 --worker_verbose
,取得更多關於 worker 執行作業的輸出內容。這個標記會反映在 WorkRequest
的 verbosity
欄位中,讓 worker 實作項目也能更詳細。
工作站會將記錄儲存在 <outputBase>/bazel-workers
目錄中,例如 /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
。檔案名稱包含工作站 ID 和記憶法。由於每個助憶法可能會有多個 WorkerKey
,因此您可能會看到特定助憶法的 worker_max_instances
記錄檔。
如要進一步瞭解 Android 版本,請參閱 Android 版本效能頁面。
實作永久工作站
如要進一步瞭解如何建立工作站,請參閱建立永久工作站頁面。
以下範例為使用 JSON 的 worker 顯示 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
的要求如下所示:
注意:雖然通訊協定緩衝區規格使用的是「Sake case」 (request_id
),但 JSON 通訊協定會使用「駝峰式大小寫」(requestId
)。在本文件中,我們會在 JSON 範例中使用駝峰式大小寫,但無論通訊協定為何,在討論欄位時,則會使用蛇形大小寫。
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
工作者會以 JSON (以換行符號分隔) 格式 (因為 requires-worker-protocol
已設為 JSON) 在 stdin
上接收此資料。接著,worker 會執行動作,並透過 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 的 worker 通訊。
Bazel 會從助憶法和共用標記衍生 WorkerKey
,因此如果這個設定允許變更 max_mem
參數,系統會為每個使用的值產生個別的工作站。如果使用太多變化版本,可能會導致記憶體用量過多。
每個 worker 目前一次只能處理一個要求。如果基礎工具是多執行緒,且包裝函式已設定為瞭解這項功能,實驗性的多工工作站功能可讓您使用多個執行緒。
在這個 GitHub 存放區中,您可以看到使用 Java 和 Python 編寫的工作站包裝函式範例。如果您使用的是 JavaScript 或 TypeScript,@bazel/worker 套件和 nodejs 工作站範例可能會有所幫助。
worker 對沙箱有何影響?
根據預設,使用 worker
策略並不會在沙箱中執行該動作,與 local
策略類似。您可以設定 --worker_sandboxing
標記,讓所有 worker 在沙箱中執行,確保每次執行工具時,只會看到應有的輸入檔案。該工具仍可能會在內部 (例如透過快取) 之間洩漏資訊。使用 dynamic
策略時,必須將 worker 置於沙箱中。
為了讓工作站正確使用編譯器快取,系統會連同每個輸入檔案傳送摘要。因此,編譯器或包裝函式可以檢查輸入內容是否仍有效,而無須讀取檔案。
即使使用輸入摘要來防止不必要的快取,採用沙箱機制的 worker 仍可提供比純沙箱更嚴格的沙箱機制,因為工具可能會保留受先前要求影響的其他內部狀態。
只有在 worker 實作支援的情況下,才能將 Multiplex worker 置入沙箱,且必須使用 --experimental_worker_multiplex_sandboxing
標記分別啟用此沙箱。詳情請參閱設計文件。
延伸閱讀
如要進一步瞭解持續性工作站,請參閱:
- 原始永久員工網誌文章
- Haskell 實作說明 {: .external}
- Mike Morearty 的網誌文章 {: .external}
- 使用 Bazel 進行前端開發:使用 Asana 的 Angular/TypeScript 和永久工作站 {: .external}
- Bazel 策略說明 {: .external}
- 在 bazel-discuss 電子郵件討論群組中討論資訊工作站策略 {: .external}