Go 部落格
容器感知的 GOMAXPROCS
Go 1.25 引入了新的容器感知 GOMAXPROCS
預設值,為許多容器工作負載提供了更合理的預設行為,避免了可能影響尾部延遲的節流,並提高了 Go 的開箱即用生產就緒性。在這篇文章中,我們將深入探討 Go 如何排程 goroutine,這種排程如何與容器級 CPU 控制互動,以及 Go 如何透過感知容器 CPU 控制來更好地執行。
GOMAXPROCS
Go 的優勢之一是其透過 goroutine 實現的內建且易於使用的併發性。從語義角度來看,goroutine 與作業系統執行緒非常相似,使我們能夠編寫簡單的阻塞程式碼。另一方面,goroutine 比作業系統執行緒更輕量,這使得即時建立和銷燬它們變得更加便宜。
雖然 Go 的實現可以將每個 goroutine 對映到一個專用的作業系統執行緒,但 Go 透過執行時排程器使執行緒具有可替代性,從而保持 goroutine 的輕量級。任何 Go 管理的執行緒都可以執行任何 goroutine,因此建立新的 goroutine 不需要建立新的執行緒,喚醒 goroutine 也不一定需要喚醒另一個執行緒。
話雖如此,伴隨排程器而來的還有排程問題。例如,我們到底應該使用多少個執行緒來執行 goroutine?如果 1,000 個 goroutine 可執行,我們應該將它們排程到 1,000 個不同的執行緒上嗎?
這就是 GOMAXPROCS
的用武之地。從語義上講,GOMAXPROCS
告訴 Go 執行時 Go 應該使用的“可用並行度”。更具體地說,GOMAXPROCS
是同時執行 goroutine 的最大執行緒數。
因此,如果 GOMAXPROCS=8
並且有 1,000 個可執行的 goroutine,Go 將使用 8 個執行緒同時執行 8 個 goroutine。通常,goroutine 執行時間很短然後阻塞,此時 Go 將切換到在同一個執行緒上執行另一個 goroutine。Go 還會搶佔那些不自行阻塞的 goroutine,確保所有 goroutine 都有機會執行。
從 Go 1.5 到 Go 1.24,GOMAXPROCS
預設設定為機器上的 CPU 核心總數。請注意,在這篇文章中,“核心”更精確地表示“邏輯 CPU”。例如,一臺具有 4 個物理 CPU 並啟用了超執行緒的機器有 8 個邏輯 CPU。
這通常是一個很好的“可用並行度”預設值,因為它自然地匹配了硬體的可用並行度。也就是說,如果有 8 個核心,Go 同時執行超過 8 個執行緒,作業系統將不得不將這些執行緒多路複用到 8 個核心上,就像 Go 將 goroutine 多路複用到執行緒上一樣。這額外的排程層並非總是問題,但它是不必要的開銷。
容器編排
Go 的另一個核心優勢是透過容器部署應用程式的便利性,在容器編排平臺中部署應用程式時,管理 Go 使用的核心數尤其重要。像 Kubernetes 這樣的容器編排平臺獲取一組機器資源,並根據請求的資源在可用資源內排程容器。在叢集資源中儘可能多地打包容器需要平臺能夠預測每個排程容器的資源使用情況。我們希望 Go 遵守容器編排平臺設定的資源利用率限制。
讓我們以 Kubernetes 為例,探討 GOMAXPROCS
設定在 Kubernetes 環境中的影響。像 Kubernetes 這樣的平臺提供了一種機制來限制容器消耗的資源。Kubernetes 具有 CPU 資源限制的概念,它向底層作業系統發出訊號,說明特定容器或一組容器將分配多少核心資源。設定 CPU 限制意味著建立 Linux 控制組 CPU 頻寬限制。
在 Go 1.25 之前,Go 不瞭解編排平臺設定的 CPU 限制。相反,它會將 GOMAXPROCS
設定為部署機器上的核心數。如果存在 CPU 限制,應用程式可能會嘗試使用遠超限制的 CPU。為了防止應用程式超出其限制,Linux 核心將 限制 應用程式。
節流是一種粗暴的機制,用於限制那些會超出其 CPU 限制的容器:它會在節流期剩餘時間內完全暫停應用程式執行。節流期通常為 100 毫秒,因此與較低 GOMAXPROCS
設定的較柔和的排程多路複用效應相比,節流可能會對尾部延遲產生實質性影響。即使應用程式從未有過太多的並行度,Go 執行時執行的任務(例如垃圾回收)仍然可能導致 CPU 峰值,從而觸發節流。
新預設值
我們希望 Go 在可能的情況下提供高效可靠的預設值,因此在 Go 1.25 中,我們已將 GOMAXPROCS
預設考慮其容器環境。如果 Go 程序在具有 CPU 限制的容器中執行,則 GOMAXPROCS
將預設為 CPU 限制(如果它小於核心數)。
容器編排系統可能會動態調整容器 CPU 限制,因此 Go 1.25 也會定期檢查 CPU 限制,並在限制發生變化時自動調整 GOMAXPROCS
。
所有這些預設值僅在未另行指定 GOMAXPROCS
時適用。設定 GOMAXPROCS
環境變數或呼叫 runtime.GOMAXPROCS
的行為與以前一樣。runtime.GOMAXPROCS
文件涵蓋了新行為的詳細資訊。
略有不同的模型
GOMAXPROCS
和容器 CPU 限制都對程序可以使用的最大 CPU 量設定了限制,但它們的模型略有不同。
GOMAXPROCS
是一個並行度限制。如果 GOMAXPROCS=8
,Go 將永遠不會同時執行超過 8 個 goroutine。
相比之下,CPU 限制是吞吐量限制。也就是說,它們限制了在某個實際時間段內使用的總 CPU 時間。預設週期為 100 毫秒。因此,“8 CPU 限制”實際上是每 100 毫秒實際時間限制 800 毫秒的 CPU 時間。
這個限制可以透過在整個 100 毫秒內連續執行 8 個執行緒來滿足,這等同於 GOMAXPROCS=8
。另一方面,這個限制也可以透過執行 16 個執行緒,每個執行緒執行 50 毫秒,而每個執行緒在另外 50 毫秒內處於空閒或阻塞狀態來滿足。
換句話說,CPU 限制不限制容器可以執行的總 CPU 數量。它只限制總 CPU 時間。
大多數應用程式在 100 毫秒週期內 CPU 使用率相當一致,因此新的 GOMAXPROCS
預設值與 CPU 限制非常匹配,肯定比總核心數更好!然而,值得注意的是,由於 GOMAXPROCS
阻止了超出 CPU 限制平均值的額外執行緒的短暫峰值,特別是在峰值工作負載中,這種變化可能會導致延遲增加。
此外,由於 CPU 限制是吞吐量限制,它們可以具有小數部分(例如 2.5 CPU)。另一方面,GOMAXPROCS
必須是正整數。因此,Go 必須將限制四捨五入到有效的 GOMAXPROCS
值。Go 總是向上取整,以充分利用 CPU 限制。
CPU 請求
Go 的新 GOMAXPROCS
預設值基於容器的 CPU 限制,但容器編排系統也提供“CPU 請求”控制。CPU 限制指定容器可能使用的最大 CPU,而 CPU 請求指定容器在任何時候都保證可用的最小 CPU。
通常會建立具有 CPU 請求但沒有 CPU 限制的容器,因為這允許容器利用超出 CPU 請求的機器 CPU 資源,這些資源否則會因為其他容器缺乏負載而空閒。不幸的是,這意味著 Go 無法根據 CPU 請求設定 GOMAXPROCS
,這會阻止利用額外的空閒資源。
如果機器繁忙,具有 CPU 請求的容器在超出其請求時仍然會受到 約束。超出請求的基於權重的約束比 CPU 限制的基於硬時間段的節流“更柔和”,但高 GOMAXPROCS
引起的 CPU 峰值仍然可能對應用程式行為產生不利影響。
我應該設定 CPU 限制嗎?
我們已經瞭解了 GOMAXPROCS
過高導致的問題,以及設定容器 CPU 限制允許 Go 自動設定適當的 GOMAXPROCS
,因此下一步自然會想到所有容器是否都應該設定 CPU 限制。
雖然這可能是自動獲得合理 GOMAXPROCS
預設值的好建議,但在決定是否設定 CPU 限制時還有許多其他因素需要考慮,例如透過避免限制來優先利用空閒資源,或者透過設定限制來優先實現可預測的延遲。
GOMAXPROCS
和有效 CPU 限制之間不匹配的最糟糕行為發生在 GOMAXPROCS
顯著高於有效 CPU 限制時。例如,一個接收 2 個 CPU 的小型容器執行在 128 核機器上。在這些情況下,考慮設定明確的 CPU 限制,或者明確設定 GOMAXPROCS
是最有價值的。
結論
Go 1.25 透過根據容器 CPU 限制設定 GOMAXPROCS
,為許多容器工作負載提供了更合理的預設行為。這樣做避免了可能影響尾部延遲的節流,提高了效率,並通常試圖確保 Go 開箱即用即生產就緒。您只需在 go.mod
中將 Go 版本設定為 1.25.0 或更高版本即可獲得新預設值。
感謝社群中所有為實現這一目標做出了 長期 討論 貢獻的人,特別是來自 Uber 的 go.uber.org/automaxprocs
維護者的反饋,他們長期以來一直為其使用者提供類似的行為。
下一篇文章:測試時間(及其他非同步性)
上一篇文章:Go 1.25 釋出
部落格索引