Golang 實時垃圾回收理論和實踐

lohc0791 9年前發布 | 11K 次閱讀 Go語言 Haskell OCaml Google Go/Golang開發

Golang實時垃圾回收理論和實踐

每天,Pusher實時發送數十億條消息:從消息源到達目的地控制在100ms內。 我們如何實現這一目標? 一個關鍵因素是Go的低延遲垃圾回收器。

垃圾收集器是實時系統的禍根,因為他們會暫停程序。 因此,在設計我們的新消息總線時,我們仔細選擇了語言。 雖然Go強調低延遲垃圾回收,但我們很警惕:Go真的做到這一點嗎? 如果能做到,效果如何呢?

在這篇博文中,我們會審視Go的垃圾收集器。 我們將看看它是如何工作的(三色算法),為什么它有這樣短的GC暫停,最重要的是,它是否工作(對其進行GC基準測試,并與其他語言進行比較)。

From Haskell to Go

我們一直在構建的系統是一個帶有已發布消息內存存儲的pub / sub消息總線。 Go中的這個版本是Haskell版本的重新實現。在發現GHC的垃圾收集器的延遲問題后,我們在5月停止了在Haskell版本的工作。

我們發布了Haskell版本的細節。基本問題是GHC的暫停時間與工作集的大小(即內存中的對象數量)成正比。在我們的例子中,我們在內存中有很多對象,這導致了幾百毫秒的暫停時間。任何GC在完成收集之前都會阻塞程序。

Go與GHC的STW(stop-the-world)收集器不同,Go的垃圾回收器與程序同時運行,這使得避免更長的停頓時間成為可能。我們對Go的低延遲垃圾回收感到鼓舞,并發現隨著版本改進延遲得到進一步降低。

并發垃圾收集器如何工作?

Go的GC如何實現并發?其 核心是三色標記掃描算法。 它使GC與程序同時運行; 這意味著暫停時間成為調度問題。 調度程序可以配置為僅在短時間內運行GC收集,與程序交叉運行。 這對我們的低延遲要求是個好消息!

GC仍然有兩個暫停階段:對根對象的初始堆棧掃描,以及標記終止階段。 令人興奮的是,這個終止階段最近已經消除。 我們將在后面討論這個優化。 在實踐中,我們發現即使具有非常大的堆,這些階段的暫停時間也可以<1ms。

使用并發GC,也有可能在多個處理器上并行運行GC。

延遲VS吞吐量

如果使用并發GC可以在大堆上得到低得多的延遲,為什么要使用stop-the-world收集器?是不是Go的并發垃圾收集器比GHC的stop-the-world收集器更好嗎?

不必要。低延遲有成本。最重要的成本是減少吞吐量。并發性需要額外的工作來同步和復制,這會減少程序正常運行的時間。 GHC的垃圾收集器針對吞吐量進行了優化,但Go收集器對延遲進行優化。在Pusher,我們關心延遲,所以這是一個對我們來說很好的折衷。

并發垃圾收集器的第二個成本是不可預測的堆增長。程序可以在GC運行時分配任意數量的內存。這意味著GC必須在堆達到目標最大大小之前運行。但是如果GC運行得太快,那么將執行更多的收集工作。這種權衡是棘手的(Austin Clements提供了一個很好的概述)。在Pusher,這種不可預測性不是一個問題;我們的程序傾向于以可預測的恒定速率分配內存。

在實踐中如何?

到目前為止,Go的GC看起來很適合我們的延遲要求。 但它在實踐中如何?

今年早些時候,當調查Haskell實現的暫停時間時,我們為測量暫停創建了一個基準。 基準程序重復地將消息推送到大小受限的緩沖區中。 舊消息不斷地過期并變成垃圾。 堆大小保持很大,這很重要,因為必須遍歷堆才能檢測哪些對象仍被引用。 這就是為什么GC運行時間與它們之間的活對象/指針的數量成正比。

這里是Go中的基準,其中緩沖區被建模為數組:

package main

import ( "fmt" "time" )

const ( windowSize = 200000 msgCount = 1000000 )

type ( message []byte buffer [windowSize]message )

var worst time.Duration

func mkMessage(n int) message { m := make(message, 1024) for i := range m { m[i] = byte(n) } return m }

func pushMsg(b buffer, highID int) { start := time.Now() m := mkMessage(highID) (b)[highID%windowSize] = m elapsed := time.Since(start) if elapsed > worst { worst = elapsed } }

func main() { var b buffer for i := 0; i < msgCount; i++ { pushMsg(&b, i) } fmt.Println("Worst push time: ", worst) }</code></pre>

根據James Fisher的博客,Gabriel Scherer寫了一篇后續博客文章,將原來的Haskell基準與OCaml和Racket的版本進行比較。 他創建了一個包含這些基準的 倉庫 ,Santeri Hiltunen添加了一個 Java版本 。 我決定將基準移植到Go,以便比較它的效果。

不用多說,這里是我的系統上的基準測試結果:

在這里是Java表現很差,OCaml表現非常好。 OCaml的~3ms暫停時間是由于OCaml用舊一代的增量GC算法。 (我們不選擇OCaml的主要原因是它的并發支持很差)。

如你所見,Go執行順利,暫停時間約為7ms。 這達到我們的要求。

一些注意事項

警惕基準!不同的運行時針對不同的用例和不同的平臺進行了優化。然而,由于我們有明確的延遲要求,并且這個基準代表我們的用例,它表明Go對我們來說很好。

map vs array - 最初我們的基準是基于從map中插入和刪除項目。然而,Go的垃圾收集器在處理大map的時候有 bug ,這掩蓋了我們的結果。為此,我們決定切換為可變數組的map。Go Map bug在Go 1.8中已經修復,但是并不是所有的基準都被移植到1.8,這就是為什么我要區分這兩者。盡管如此,沒有理由期望GC時間比map(除了錯誤或不良實現)更糟糕。

手動vs rts計時 - 作為第二個警告,基準在計時方面不同:一些基準使用手動計時器,但其他使用運行時系統統計。存在此差異,因為某些運行時不會使該統計信息可用(例如在Go中)。我們還擔心,打開profiling會對影響一些語言的垃圾收集器。為此,我們將所有基準移植到手動計時。

最后一個警告是基準實現中的最壞情況。有一種情況, insert/delete map操作可能不利地影響定時,這是切換到使用簡單數組的另一個原因。

請為我們的基準貢獻更多的語言!這個簡單的基準是非常通用的,在選擇語言時是一個重要的基準。你想看看$ YOUR_LANGUAGE的GC執行情況,然后請提交PR! :)我會特別感興趣的是知道為什么Java暫停時間是如此糟糕,因為按理論它應該更好。

為什么Go的結果不好?

使用已修復mapbug的編譯器,或使用數組時,我們得到暫停時間~7ms。 這是非常好的,但是根據Go團隊在演示幻燈片標題為“1.5 Garbage Benchmark Latency”的基準測試結果,我們預計我們的堆大小為200MB時暫停大約1m(GC次數往往與 指針數量而不是字節數,但是它們不能提供該信息)。 Twitch團隊使用Go1.7達到約1ms的暫停時間(盡管它們不清楚堆對象的數量)。

我在golang-nuts郵件列表問這個原因。 Rhys Hilter的想法是,這些暫停時間可能是由這個當前未定的錯誤引起的,GC中的空閑標記worker可能會阻止程序,即使有工作要做。 為了嘗試并確認這一點,我啟動了go tool trace [3],它可視化程序運行時行為。

從這個例子可以看出,有一個12ms的周期,其中背景標記woker正在所有四個處理器上運行,阻塞程序。這使我強烈懷疑我遇到上述錯誤。

到目前為止,我很高興看到基準測試的現有暫停時間滿足要求,但我也保持關注,以解決上述問題。

如前所述,Go團隊最近宣布了一項改進措施,導致GC暫停時間小于1ms。它燃起我的希望,但我很快就意識到這個優化是去掉了stw階段,swt階段在我使用的基準下已經<1ms。我們的暫停時間主要是由GC的并發階段引起的。

盡管如此,這是一個值得歡迎的改進,并表明團隊繼續關注并改進GC的延遲。這種優化的技術描述本身是一個有趣的讀物。

結論

這項調查的關鍵是GC是針對更低的延遲或更高的吞吐量進行優化。程序可能執行更好或更差在這取決于您的程序的堆使用率。 (有很多的對象嗎?他們有長或短的生命嗎?)

重要的是要了解底層的GC算法,以決定它是否適合您的用例。在實踐中測試GC實現也很重要。您的基準測試應該與您打算實現的程序具有相同的堆使用率。這將在實踐中驗證GC實現的有效性。正如我們所看到的,Go的實現不是沒有bug的,但在我們的情況下,問題是可以接受的。我想在更多的語言中看到相同的基準,如果你想貢獻的話:)

盡管存在一些問題,Go的GC與其他GC語言相比表現良好。 Go團隊一直在改進延遲,并繼續這樣做。我們對Go語言GC的理論和實踐感到滿意。

 

 

來自:http://mp.weixin.qq.com/s/IX0zO2MliP01ixtLtih6gQ

 

 本文由用戶 lohc0791 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!