Uber:大規模、半自動化 Go GC 調優

Uber 是國外大規模使用 Go 的公司之一,在 GitHub 上,他們開源了不少 Go 相關項目。最出名的有以下幾個:

其中 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 的服務。以下是可能的挑戰:

4、自動化案例

GOGCTuner 是一個庫,它簡化了爲服務所有者調整垃圾收集的過程,並在其之上添加了一個可靠層。

GOGCTuner 根據容器的內存限制(或服務所有者的上限)動態計算正確的 GOGC 值,並使用 Go 的運行時 API 設置它。以下是 GOGCTuner 庫功能的詳細信息:

正常流量(實時數據集爲 150M)

圖片

圖 4:正常操作。左側爲默認配置,右側爲手動調整

流量增加了 2 倍(實時數據集爲 300M)

圖片

圖 5:雙倍負載。左側爲默認配置,右側爲手動調整

GOGCTuner 達到 70% 時流量增加了 2 倍(實時數據集爲 300M)

圖片

圖 6:將負載加倍,但使用調諧器。左邊是默認配置,右邊是 GOGCTuner 調優

5、可觀察性

我們發現缺乏一些關鍵指標,這些指標可以讓我們更深入地瞭解每個服務的垃圾收集。

圖片

圖 7:GC 之間的間隔圖表

圖片

圖 8:p99 GC CPU 成本圖表

圖片

圖 9:估計的 p99 實時數據集圖表

圖片

圖 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