[譯] CockroachDB GC優化總結
幾周前我們分享了一個帖子講述我們為什么選擇Go語言編寫CockroachDB,我們收到一些問題,詢問我們是如何解決Go語言的一些已知問題,特別是關于性能、GC和死鎖的問題。
本文中我們將分享幾個非常有用的優化技巧用以改善許多常見的GC性能問題(接下來還將覆蓋一些有趣的死鎖問題)。我們將重點分享如何通過嵌套結構體、使用 sync.Pool、和復用后端數組減少內存分配和降低GC開銷。
減少內存分配和GC優化
將Go與其他語言(比如java)區別開來的是Go語言能讓你管理內存布局。通過GO語言,你可以合并碎片,而其他垃圾集合語言不能。
讓我們看看CockroachDB中從磁盤讀取數據并解碼的一小段代碼:
metaKey := mvccEncodeMetaKey(key) var meta MVCCMetadata if err := db.GetProto(metaKey, &meta); err != nil { // Handle err } ... valueKey := makeEncodeValueKey(meta) var value MVCCValue if err := db.GetProto(valueKey, &value); err != nil { // Handle err }
為了讀取數據,我們執行了4次內存分配:MVCCMetadata結構體、MVCCValue結構體和metaKey、valueKey。在Go語言中我們可以通過合并結構體和預分配空間給Key把內存分配減少為1次。
type getBuffer struct { meta MVCCMetadata value MVCCValue key [1024]byte } var buf getBuffer metaKey := mvccEncodeKey(buf.key[:0], key) if err := db.GetProto(metaKey, &buf.meta); err != nil { // Handle err } ... valueKey := makeEncodeValueKey(buf.key[:0], meta) if err := db.GetProto(valueKey, &buf.value); err != nil { // Handle err }
我們聲明了一個getBuffer類型,包含兩個不同的結構體:MVCCMetadata和MVCCValue(都是protobuf對象),不同于通常使用的切片,第三個成員使用了一個數組。
不需要額外分配內存,你就可以直接在結構體中定義一個定長的數組(1024 bytes),這允許我們將三個對象放到同一個getBuffer結構體中。這樣我們就把4次內存分配減少為1次。需要注意的的兩個不同的key我們使用了同一個數組,在兩個key不同時使用的情況下是可以正常工作的。稍后我們再來討論數組。
sync.Pool
var getBufferPool = sync.Pool{ New: func () interface{} { return &getBuffer{} }, }
說實話,我們花了一段時間才弄明白為什么 sync.Pool 才是我們我們想要的。在一個GC周期內可以無限制使用同一個對象無需多次內存分配,GC會負責回收。在每次GC啟動的時候都會清除Pool中的對象。
用一個例子來說明如何使用 sync.Pool:
buf := getBufferPool.Get().(*getBuffer) defer getBufferPoolPut(buf) key := append(but.key[0:0], ...)
首先你需要使用一個工廠函數來聲明一個全局的 sync.Pool 對象,在這個列子中我們分配一個 getBuffer結構體并返回。我們不再創建新的 getBuffer 改為從 pool 中獲取。Pool.Get 返回的是一個空接口,我們需要使用類型斷言轉換。使用完成后再放回到 pool 中。最終的結果是我們無需每次獲取 getBuffer時都分配一次內存。
數組和切片
有些事可能不值一提,在Go語言中數組和切片是不同的類型,而且切片和數組幾乎所有操作都一樣。你僅僅通過一個方括號語法 [:0] 就可以從數組得到一個切片。
key := append(bf.key[0:0], ...)
這里使用數組創建了一個長度為0的切片。事實是這個切片已經擁有了一個后端存儲,意思是說對切片的append操作實際上插入到數組中,而并沒有分配新的內存。所以當我們解碼一個key時,我們可以append進一個通過這個 buffer 創建的切片中。只要key的長度小于 1 KB,我們就不需要做任何內存分配。將復用我們給數組分配的內存。
key 的長度超過 1 KB 的情況可能會有但是不常見,在這種情況下,程序可以透明的自動分配新的后端數組,我們的代碼不需要做任何處理。
Gogoprotobuf vs Google protobuf
最后,我們在磁盤上存儲所有的數據都使用了protobuf。然而我們并沒有使用 Google官方的protobuf類庫,我們強烈推薦使用一個叫做 gogoprotobuf的分支。
Gogoprotobuf 遵循了很多我們上面提到的關于避免不必要的內存分配的原則。尤其是,它允許將數據編碼到一個后端使用數組的字節切片以避免多次內存分配。此外,非空注解允許你直接嵌入消息而無需額外的內存分配開銷,這在始終需要嵌入消息時是非常有用的。
最后一點優化是,較基于反射進行編碼和解編碼的Google標準protobuf類庫,gogoprotobuf使用編碼和解編碼協程提供了不錯的性能改善。
總結
通過結合上述技巧,我們已經可以最小化GC的性能開銷和優化更好的性能。當我們接近測試階段,更多地專注于內存分析,我們將在后續的帖子中分享我們的成果。當然,如果你知道其他的Go語言性能優化,我們洗耳恭聽。
原文鏈接: http://www.cockroachlabs.com/blog/how-to-optimize-garbage-collection-in-go/
原文作者:Jessica Edwards
翻譯校對:betty, 龍貓,柚子