永久工作站

回報問題 查看來源

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

永久工作站是指由 Bazel 伺服器啟動的長時間執行程序,該程序可做為實際工具 (通常是編譯器) 周圍的「包裝函式」,或者本身就是工具本身。為了受益於永久工作站,這項工具必須支援執行一系列編譯,且包裝函式需要在工具的 API 和下述的要求/回應格式之間進行轉譯。系統可能會在同一個版本中使用和沒有 --persistent_worker 旗標呼叫相同的 worker,並負責啟動和通訊工具,以及結束時關閉工作站。每個工作站執行個體都會獲派 (但不分支) 位於 <outputBase>/bazel-workers 下的獨立工作目錄。

使用永久工作站是一種執行策略,可減少啟動成本、增加更多 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 策略,但您仍可使用 localsandboxed 做為備用方案。

選擇工作站數量

每記憶體的工作站執行個體預設數量為 4 個,但您可以使用 worker_max_instances 標記進行調整。在充分利用可用 CPU 與取得的 JIT 編譯與快取命中之間存在取捨。隨著工作站數量增加,更多的目標將支付啟動成本,包括執行非 JITted 程式碼及進入冷快取。如果要建構的目標數量不多,單一工作站可能會在編譯速度和資源使用之間取得最佳取捨 (例如請參閱問題 #8586)。worker_max_instances 旗標會設定每個記憶和旗標的工作站執行個體數量上限 (請見下方),因此如果您保留預設值,在混合的系統中可能會耗用大量記憶體。對於漸進式建構,多個工作站執行個體的優點就更小。

此圖表顯示在搭載 64 GB RAM 的 6 核心超執行緒 Intel Xeon 3.5 GHz Linux 工作站上,Bazel (目標 //src:bazel) 的 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,取得 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 的要求隨後會如下所示:

注意:雖然通訊協定緩衝區規格使用的是「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..." },
  ],
}

工作站會在 stdin 上以換行符號分隔的 JSON 格式接收此屬性 (因為 requires-worker-protocol 已設為 JSON)。工作站接著會執行該動作,並將 JSON 格式的 WorkResponse 傳送到其 stdout 上的 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 策略時,必須對工作站進行沙箱機制

為了讓工作站正確使用編譯器快取,系統會連同每個輸入檔案傳送摘要。因此,編譯器或包裝函式可以在不讀取檔案的情況下,檢查輸入是否仍然有效。

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

只有在工作站實作支援的情況下,才能啟用多重廣告工作站的沙箱機制,而且此沙箱作業必須另外透過 --experimental_worker_multiplex_sandboxing 標記啟用。詳情請參閱設計文件)。

其他資訊

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