這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
幾周前我們分享了一個文章講述我們為什麼選擇Go語言編寫CockroachDB,我們收到一些問題,詢問我們是如何解決Go語言的一些已知問題,特別是關於效能、GC和死結的問題。
本文中我們將分享幾個非常有用的最佳化技巧用以改善許多常見的GC效能問題(接下來還將覆蓋一些有趣的死結問題)。我們將重點分享如何通過嵌套結構體、使用 sync.Pool、和複用後端數組減少記憶體配置和降低GC開銷。
減少記憶體配置和GC最佳化
將Go與其他語言(比如java)區別開來的是Go語言能讓你管理記憶體布局。通過GO語言,你可以合并片段,而其他垃圾集合語言不能。
讓我們看看CockroachDB中從磁碟讀取資料並解碼的一小段代碼:
metaKey := mvccEncodeMetaKey(key)var meta MVCCMetadataif err := db.GetProto(metaKey, &meta); err != nil { // Handle err}...valueKey := makeEncodeValueKey(meta)var value MVCCValueif 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 getBuffermetaKey := 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, 龍貓,柚子