Uber:大規模、半自動化 Go GC 調優
Uber 是國外大規模使用 Go 的公司之一,在 GitHub 上,他們開源了不少 Go 相關項目。最出名的有以下幾個:
-
zap
-
fx、dig
-
guide
其中 guide 是他們內部的 Go 編碼規範,目前已經被翻譯成了多國語言,其中包括簡體中文版本:https://github.com/xxjwxc/uber_go_guide_cn。
Uber 更多內容開源項目可以訪問他們的 GitHub 首頁:https://github.com/uber-go。
此外,https://github.com/jaegertracing/jaeger 也是 Uber 開發的,之後捐贈給 CNCF,這是一個分佈式追蹤平臺,用於監控基於微服務的分佈式系統。
因此他們在 Go 上有很多經驗。本文介紹 Uber 如何在 30 個關鍵任務服務中節省 7 萬個內核。
本文作者是 Cristian Velazquez,他是 Uber Maps Production Engineering 團隊的 Sr Production Engineer II。他負責跨多個組織的多個效率計劃,其中最相關的是 Java 和 Go 的垃圾收集調優。
1、介紹
實現盈利的方式有開源和節流,對於 Uber 技術團隊(其他公司技術團隊其實也類似)來說,提升資源利用率,進而減少服務器數量,這是減少成本的一種方式。有些公司通過換語言實現,比如 Python 換爲 Go 等。而對 Go 服務來說,可能最有效的工作是針對 GOGC 的優化。在本文中,我們將分享在高效、低風險、大規模、半自動化的 Go GC 調優機制方面的經驗。
Uber 有數千個微服務,並由基於雲原生和基於調度程序的基礎設施提供支持,這些服務大部分是用 Go 編寫的。我們的 Maps Production Engineering 團隊之前在 Java 微服務 GC 調優方面有很多經驗,也取得了很好的效果,現在這些經驗在 Go GC 方面也發揮了重要的作用。
2021 年初,我們探索了對 Go 服務進行 GC 調優的可能性。我們運行了幾個 CPU 配置文件來評估當前的事務狀態,我們發現 GC 是絕大多數關鍵任務服務的 CPU 最大消耗者。以下是一些 CPU 配置文件的表示,其中 GC(由 runtime.scanobject 方法標識)消耗了分配的計算資源的很大一部分。
示例服務 #1:
圖 1:示例服務 #1 的 GC CPU 成本
示例服務 #2
圖 2:示例服務 #2 的 GC CPU 成本
受到這一發現的啓發,我們開始爲相關服務調整 GC。令我們高興的是,Go 的 GC 實現和調整的簡單性使我們能夠自動化大部分檢測和調整機制。我們將在以下部分詳細介紹我們的方法及其效果。
2、GOGC Tuner
除了觸發事件,Go 運行時會定期調用併發垃圾收集器,其中觸發事件是基於內存值的。因此, 更多內存對 Go 服務來說更有利,因爲它減少了 GC 必須運行的時間。此外,我們意識到我們的主機 CPU 與內存的比例是 1:5(1 核:5 GB RAM),而大多數 Go 服務的配置比例是 1:1 ~ 1:2。因此,我們有信心可以利用更多內存來減少 GC 的 CPU 影響。這是一種與服務無關的機制,如果應用得當,會產生很大的影響。
深入研究 Go 的垃圾收集超出了本文的範圍,但以下是這項工作的相關部分:Go 中的垃圾收集是併發的,涉及分析所有對象以確定哪些對象仍然可以訪問。我們將可到達對象稱爲 “實時數據集”。Go 僅提供一個選項:GOGC, 以實時數據集的百分比表示,用於控制垃圾收集。GOGC 值充當數據集的乘數。GOGC 的默認值爲 100%,這意味着 Go 運行時將爲新分配保留與實時數據集相同的內存量。例如:
hard_target = live_dataset + live_dataset * (GOGC / 100).
然後,pacer 負責預測觸發 GC 的最佳時間,以避免命中硬目標(軟目標)。
圖 3:具有默認配置的示例堆
3、動態多樣:一個值無法適應所有場景
我們發現固定的 GOGC 值的調整不適合 Uber 的服務。以下是可能的挑戰:
-
它不知道分配給容器的最大內存,並可能導致內存不足問題。
-
我們的微服務有各種內存利用率組合。例如,分片系統可以有非常不同的實時數據集。我們在其中一項服務中遇到了這種情況,其中 p99 利用率爲 1G 但 p1 爲 100MB,因此 100MB 實例具有巨大的 GC 影響。
4、自動化案例
GOGCTuner 是一個庫,它簡化了爲服務所有者調整垃圾收集的過程,並在其之上添加了一個可靠層。
GOGCTuner 根據容器的內存限制(或服務所有者的上限)動態計算正確的 GOGC 值,並使用 Go 的運行時 API 設置它。以下是 GOGCTuner 庫功能的詳細信息:
-
簡化配置,便於推理和確定性計算。對於初學者來說,GOGC=100% 的確定性不足,因爲它仍然依賴於實時數據集。另一方面,70% 的限制可確保服務始終使用 70% 的堆空間。
-
防止 OOM(內存不足):該庫從 cgroup 讀取內存限制並使用 70% 的默認硬限制,根據我們的經驗,這是一個安全值。
-
需要注意的是,這種保護是有限制的。Tuner 只能調整緩衝區分配,因此如果你的服務活動對象高於限制,則 Tuner 將設置默認下限爲 1.25X 你的活動對象利用率。
-
對於極端情況允許更高的 GOGC 值,例如:
-
正如我們上面提到的,手動 GOGC 不是確定性的。我們仍然依賴實時數據集的大小。如果 live_dataset 將我們的最後一個峯值翻倍了怎麼辦?GOGCTuner 將以更多 CPU 爲代價強制執行相同的內存限制。相反,手動調整可能會導致 OOM。因此,服務所有者過去常常爲這些類型的場景提供足夠的緩衝。請參見下面的示例:
正常流量(實時數據集爲 150M)
圖 4:正常操作。左側爲默認配置,右側爲手動調整
流量增加了 2 倍(實時數據集爲 300M)
圖 5:雙倍負載。左側爲默認配置,右側爲手動調整
GOGCTuner 達到 70% 時流量增加了 2 倍(實時數據集爲 300M)
圖 6:將負載加倍,但使用調諧器。左邊是默認配置,右邊是 GOGCTuner 調優
- 使用 MADV_FREE[1] 內存策略的服務會導致錯誤的內存指標。例如,我們的可觀察性指標顯示 50% 的內存利用率(實際上它已經釋放了 50% 中的 20%)。然後服務所有者只是使用這個 “不準確” 的指標來調整 GOGC。
5、可觀察性
我們發現缺乏一些關鍵指標,這些指標可以讓我們更深入地瞭解每個服務的垃圾收集。
- 垃圾收集之間的間隔:瞭解我們是否仍然可以調整很有用。例如,Go 強制每 2 分鐘進行一次垃圾收集。如果你的服務仍然具有較高的 GC 影響,但你已經看到此圖的 120 秒,這意味着你不能再使用 GOGC 進行調優。在這種情況下,你需要優化分配。
圖 7:GC 之間的間隔圖表
- GC CPU 影響:知道哪些服務受 GC 影響最大。
圖 8:p99 GC CPU 成本圖表
- 實時數據集(Live dataset)大小:幫助我們識別內存泄漏。服務所有者注意到的問題是他們看到內存利用率有所增加。爲了向他們展示沒有內存泄漏,我們添加了 “實時使用” 指標,該指標顯示了穩定的利用率。
圖 9:估計的 p99 實時數據集圖表
- GOGC 值:有助於瞭解調諧器的反應。
圖 10:調諧器分配給應用程序的 min、p50、p99 GOGC 值圖表
6、實現
我們最初的方法是每秒運行一次代碼來監控堆指標,然後相應地調整 GOGC 值。這種方法的缺點是開銷開始變得相當大,因爲爲了讀取堆指標,Go 需要執行 STW(ReadMemStats[2]),並且它有點不準確,因爲我們每秒可以進行多次垃圾收集。
幸運的是,我們找到一個不錯的方法。Go 有終結器(SetFinalizer[3]),它們是在對象將被垃圾收集時運行的函數。它們主要用於清理 C 代碼或其他一些資源的內存。我們能夠使用一個自引用終結器,它會在每次 GC 調用時自行重置。這使得我們能夠減少 CPU 開銷。例如:
圖 11:GC 觸發事件的示例代碼
調用 runtime.SetFinalizer(f, finalizerHandler)
代替直接調用 finalizerHandler
以允許處理程序在每次 GC 上運行;它基本上不會讓引用消失,因爲它不是保活的昂貴資源(它只是一個指針)。
7、影響
在我們的幾十個服務中部署了 GOGCTuner 之後,我們深入研究了其中一些顯著的、CPU 利用率提高到兩位數的服務。僅這些服務就累計節省了大約 70K 個內核。以下是 2 個這樣的示例:
圖 12:可觀察性服務在數千個計算內核上運行,live_dataset 具有高標準偏差(最大值是最小值的 10 倍),顯示 p99 CPU 利用率降低了約 65%
圖 13:任務關鍵型 Uber 喫掉在數千個計算核心上運行的服務,顯示 p99 CPU 利用率降低了約 30%
由此產生的 CPU 利用率降低在戰術上改善了 p99 延遲(以及相關的 SLA、用戶體驗),並在戰略上改善了容器成本(因爲服務是根據其利用率進行擴展的)。
8、總結
垃圾收集(GC)是應用程序中最難以捉摸,同時也是被低估的性能影響因素之一。Go 強大的 GC 機制和簡化的調優,加之我們大規模的 Go 服務以及強大的內部平臺(如 Go、計算、可觀察性),使我們能夠產生如此大規模的影響。由於技術和能力的變化,同時問題空間本身的發展,我們希望繼續改進我們調整 GC 的方式。
最後再次重申我們在開頭提到的內容:沒有一個適合所有場景的 GOGC 值。由於公有云和運行在其中的容器化工作負載的性能高度可變,我們認爲 GC 性能在雲原生設置中將保持可變。再加上我們使用的絕大多數 CNCF 可觀測項目都是用 Go 編寫的(如 Kubernetes、Prometheus、Jaeger 等),這意味着任何外部的大規模部署也可以從這種努力中受益。
比較可惜的是,目前沒看到 Uber 開源了這個工具。
來自公衆號:幽鬼
原文鏈接:https://eng.uber.com/how-we-saved-70k-cores-across-30-mission-critical-services/
參考資料
[1]
MADV_FREE: https://man7.org/linux/man-pages/man2/madvise.2.html
[2]
ReadMemStats: https://golang.org/pkg/runtime/#ReadMemStats
[3]
SetFinalizer: https://golang.org/pkg/runtime/#SetFinalizer
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/yMBFHH_4KMxeSsWPGJjCRw