[譯] CockroachDB GC優化總結

幾周前咱們分享了一個帖子講述咱們爲何選擇Go語言編寫CockroachDB,咱們收到一些問題,詢問咱們是如何解決Go語言的一些已知問題,特別是關於性能、GC和死鎖的問題。java

本文中咱們將分享幾個很是有用的優化技巧用以改善許多常見的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次。mvc

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對象),不一樣於一般使用的切片,第三個成員使用了一個數組。app

不須要額外分配內存,你就能夠直接在結構體中定義一個定長的數組(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, 龍貓,柚子

相關文章
相關標籤/搜索