一行降低 100000kg 碳排放量的代碼!

文|張稀虹(花名:止語 )

螞蟻集團技術專家

負責螞蟻集團雲原生架構下的高可用能力的建設 主要技術領域包括 ServiceMesh、Serverless 等

本文 3631 字 閱讀 8 分鐘

PART. 1 故事背景

今年雙十一大促後,按照慣例我們對大促期間的系統運行數據進行了詳細的分析,對比去年同期的性能數據發現,MOSN 的 CPU 使用率有大約 1% 的上漲。

爲什麼增加了?

是合理的嗎?

可以優化嗎?

是不可避免的熵增,還是人爲的浪費?

帶着這一些列靈魂拷問我們對系統進行了分析

圖片

PART. 2 問題定位

我們從監控上發現,這部分額外的開銷是在系統空閒時已有,並且不會隨着壓測流量增加而降低,CPU 總消耗增加 1.2%,其中 0.8% 是由 cpu_sys 帶來。

通過 perf 分析發現新版本的 MOSN 相較於老版本, syscall 有明顯的增加。

舊版本

新版本

圖片

經過層層分析,發現其中一部分原因是 MOSN 依賴的 sentinel-golang 中的一個StartTimeTicker 的 func 中的 Sleep 產生了大量的系統調用,這是個什麼邏輯?

PART. 3 理論分析

圖片

查看源碼發現有一個毫秒級別的時間戳緩存邏輯,設計的目的是爲了降低高調用頻率下的性能開銷,但空閒狀態下頻繁的獲取時間戳和 Sleep 會產生大量的系統調用,導致 cpu sys util 上漲。我們先從理論上分析一下爲什麼這部分優化在工程上通常是無效的,先來看看 Sentinel 的代碼:

package util

import (
  "sync/atomic"
  "time"
)

var nowInMs = uint64(0)

// StartTimeTicker starts a background task that caches current timestamp per millisecond,
// which may provide better performance in high-concurrency scenarios.
func StartTimeTicker() {
  atomic.StoreUint64(&nowInMs, uint64(time.Now().UnixNano())/UnixTimeUnitOffset)
  go func() {
    for {
      now := uint64(time.Now().UnixNano()) / UnixTimeUnitOffset
      atomic.StoreUint64(&nowInMs, now)
      time.Sleep(time.Millisecond)
    }
  }()
}

func CurrentTimeMillsWithTicker() uint64 {
  return atomic.LoadUint64(&nowInMs)
}

從上面的代碼可以看到,Sentinel 內部用了一個 goroutine 循環的獲取時間戳存到 atomic 變量裏,然後調用 Sleep 休眠 1ms,通過這種方式緩存了毫秒級別的時間戳。外部有一個開關控制這段邏輯是否要啓用,默認情況下是啓用的。從這段代碼上看,性能開銷最大的應該是 Sleep,因爲 Sleep 會產生 syscall,衆所周知 syscall 的代價是比較高的。

time.Sleep 和 time.Now 對比開銷到底大多少呢?

查證資料(1)後我發現一個反直覺的事實,由於 Golang 特殊的調度機制,在 Golang 中一次 time.Sleep 可能會產生 7 次 syscall,而 time.Now 則是 vDSO 實現的,那麼問題來了 vDSO 和 7 次系統調用相比提升應該是多少呢?

我找到了可以佐證的資料,恰好有一個 Golang 的優化(2),其中提到在老版本的 Golang 中(golang 1.9-),Linux/386 下沒有這個 vDSO 的優化,此時會有 2 次 syscall,新版本經過優化後理論性能提高 5~7x+,可以約等於一次 time.Now <= 0.3 次 syscall 的開銷。

Cache 設計的目的是爲了減少 time.Now 的調用,所以理論上這裏調用量足夠大的情況下可能會有收益,按照上面的分析,假設 time.Now 和 Sleep 系統調用的開銷比是 0.3:7.3(7+0.3),Sleep 每秒會執行 1000 次(不考慮系統精度損失的情況下),這意味着一秒內 CurrentTimeMillsWithTicker 的調用總次數要超過 2.4W 纔會有收益。

所以我們再分析一下 CurrentTimeMillsWithTicker 的調用次數,我在這個地方加了一個 counter 進行驗證,然後模擬請求調用 Sentinel 的 Entry,經過測試發現:

  1. 當首次創建資源點時,Entry 和 CurrentTimeMillsWithTicker 的放大比爲 20,這主要是因爲創建底層滑動窗口時需要大量的時間戳計算

  2. 當相同的 resource 調用 Entry 時,調用的放大比⁰爲 5:1

|注 0: 內部使用的 MOSN 版本基於原版 Sentinel 做了一些定製化,社區版本放大比理論上低於該比值。

考慮到創建資源點是低頻的,我們可以近似認爲此處調用放大比爲 5。所以理論上當單機 QPS 至少超過 4800 以上纔可能會取得收益......我們動輒聽說什麼 C10K、C100K、C1000K 問題,這個值看上去似乎並不很高?但在實際業務系統中,這實際上是一個很高的量。

我隨機抽取了多個日常請求量相對大的應用查看 QPS(這裏的 QPS 包含所有類型的資源點,入口/出口調用以及子資源點等,總之就是所有會經過 Sentinel Entry 調用的請求量),日常峯值也未超過 4800QPS,可見實際的業務系統中,單機請求量超過這個值的場景是非常罕見的。¹

|注 1: 此處監控爲分鐘級的數據監控,可能與秒級監控存在一定的出入,僅用於指導日常請求量評估。

圖片

考慮到這個優化還有一個好處,是可以降低同步請求時間戳時的耗時,所以我們可以再對比一下直接從 atomic 變量讀取緩存值和通過 time.Now() 讀取時間戳的速度。

圖片

可以看到單次直接獲取時間戳確實比從內存讀取開銷大很多,但是仍然是 ns 級別的,這種級別的耗時增長對於一筆請求而言是可以忽略不計的。

圖片

大概是 0.06 微秒,即使乘以 5,也就是 0.3 微秒的增加。在 4000QPS 這個流量檔位下我們也可以看一下 MOSN 實際 RT。

圖片

兩臺機器的 MOSN RT 也沒有明顯的差異,畢竟只有 0.3 微秒...

PART. 4 測試結論

同時我們也找了兩臺機器,分別禁用/啓用這個 Cache 進行測試,測試結果佐證了上述分析的結論。

圖片

從上圖的數據可以看出來,啓用 Cache 的情況下 cpu sys util 始終比不啓用 Cache 的版本要大,隨着請求量增加,性能差距在逐步縮小,但是直至 4000QPS 仍然沒有正向的收益。

經過測試和理論分析可得知,在常規的場景下,Sentinel 的這個 Cache 特性是沒有收益的,反而對性能造成了損耗,尤其是在低負載的情況下。即使在高負載的情況下,也可以推論出:沒有這個 Cache 不會對系統造成太大的影響。

這次性能分析也讓我們意識到了幾個問題:

  1. 不要過早優化,正所謂過早優化是萬惡之源;

  2. 一定要用客觀數據證明優化結果是正向的,而不是憑藉直覺;

  3. 要結合實際場景進行分析,而不應該優先考慮一些小概率場景;

  4. 不同語言間底層實現可能存在區別,移植時應該仔細評估。

PART. 5 有必要嗎?

你上面不是說了,不要過早優化,那這個算不算過早優化呢,你是不是雙標?

“過早優化是萬惡之源”實際上被誤用了,它是有上下文的。

We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. —— Donald Knuth

Donald Knuth 認爲許多優化是沒必要的,我們可能花費了大量的時間去做了投入產出比不高的事情,但他同時強調了一些關鍵性優化的必要性。簡而言之就是要考慮性價比,不能盲目地、沒有數據支撐地去做性能優化,premature 似乎翻譯成“不成熟、盲目的”更爲貼切,因此這句話的本意是“盲目的優化是萬惡之源”。這裏只需要一行代碼的改動,即可省下這部分不必要的開銷,性價比極高,何樂而不爲呢?

從數據上看,這個優化只是降低了 0.7% 的 cpu sys util,我們缺這 0.7% 嗎?

從系統水位的角度思考或許還好,畢竟我們爲了保險起見預備了比實際需求更多的資源,這 0.7% 並不會成爲壓垮我們系統的最後一顆稻草。但從環保的角度,很有必要!今年我們強調的是綠色環保,提效降本。這區區一行代碼,作爲 Sidecar 跑在數十萬的業務 Pod 中,背後對應的是上萬臺的服務器。

圖片

用不太嚴謹的一種方式進行粗略的估算,以常規的服務器 CPU Xeon E5 爲例,TDP² 爲 120W,0.7% * 120W * 24 * 365 / 1000 = 73584 度電,每一萬臺機器一年 7 萬度電,這還不包括爲了保持機房溫度而帶來的更大的熱交換能耗損失(簡單說就是空調費,常規機房 PUE 大概 1.5),按照不知道靠譜不靠譜的專家估算,節約 1 度電=減排 0.997 千克二氧化碳,這四捨五入算下來大概減少了 100000kg 的二氧化碳吧。

同時這也是一行開源社區的代碼,社區已經採納我們的建議(3)將該特性默認設置爲關閉,或許有上千家公司數以萬計的服務器也將得到收益。

圖片

|注 2: TDP 即熱功耗設計,不能等價於電能功耗,熱設計功耗是指處理器在運行實際應用程序時,可產生的最大熱量。TDP 主要用於和處理器相匹配時,散熱器能夠有效地冷卻處理器的依據。處理器的 TDP 功耗並不代表處理器的真正功耗,更沒有算術關係,但通常可以認爲實際功耗會大於 TDP。

「擴展閱讀」

(1)查證資料:https://github.com/golang/go/issues/25471

(2)Golang 的優化:https://go-review.googlesource.com/c/go/+/69390

(3)我們的建議:https://github.com/alibaba/sentinel-golang/issues/441

感謝藝剛、茂修、浩也、永鵬、卓與等同學對問題定位做出的貢獻,本文部分引用了 MOSN 大促版本性能對比文檔提供的數據。同時感謝宿何等 Sentinel 社區的同學對相關 issue 和 PR 的積極支持。

本週推薦閱讀

技術風口上的限流

深入 HTTP/3(一)|從 QUIC 鏈接的建立與關閉看協議的演進

網商雙十一基於 ServiceMesh 技術的業務鏈路隔離技術及實踐

降本提效!註冊中心在螞蟻集團的蛻變之路

img

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章