效能敏感場景下,Go 三方庫的選型思路和案例分析

語言: CN / TW / HK

Go 原生自帶的庫很豐富,基本上我們常規用到的功能都有對應的原生庫支援。不過,Go 自帶的庫是儘可能滿足大多數的需求,並不是所有的庫都是官方的好。

我們做技術選型的時候,要對Go 的原生庫有所取捨,比如 crypto 這個庫據說是專門請的專精加密演算法的團隊開發的,易用又專業,那加密相關的功能肯定是原生庫。

再比如 Go 的encoding/json 庫,平時用肯定沒問題,但是效能敏感的場景下可能就不太行,今天聊一下除了Go自帶的庫外,JSON序列化還有哪些可以選擇的優秀三方庫,以及另外一個常用的 Cache 庫的選型調研。藉此分析一下我們在做技術選型時的取捨。

JSON

基本上從以下兩種角度進行分析

  1. 效能方面,如是否使用反射;

  2. 是否支援 Unmarshal 到 map 或 struct,未涉及靈活性與擴充套件性方面,下面報告中只考慮最簡單的反序列化,不會提及每個庫的靈活性,如提供的一些定製化抽取的 API;

相關庫

GO 1.14 標準庫 JSON大量使用反射獲取值,首先 go 的反射本身效能較差 ,其次 頻繁分配物件 ,也會帶來記憶體分配和 GC 的開銷;

valyala/fastjsonstar: 1.4k

  1. 通過遍歷 json 字串找到 key 所對應的 value,返回其值 []byte,由業務方自行處理。同時可以返回一個 parse 物件用於多次解析;

  2. 只提供了簡單的 get 介面,不提供 Unmarshal 到結構體或 map 的介面;

tidwall/gjsonstar: 9.5k

  1. 原理與 fastjson 類似,但不會像 fastjson 一樣將解析的內容儲存在一個 parse 物件中,後續可以反覆的利用,所以當呼叫 GetMany 想要返回多個值的時候,需要遍歷 JSON 串多次,因此效率會比較低;

  2. 提供了 get 介面和 Unmarshal 到 map 的介面,但沒有提供 Unmarshal 到 struct 的介面;

buger/jsonparserstar: 4.4k

  1. 原理與 gjson 類似,有一些更靈活的 api;

  2. 只提供了簡單的 get 介面,不提供 Unmarshal 到結構體或 map 的介面;

json-iteratorstar: 10.3k

  1. 相容標準庫;

  2. 其之所以快,一個是 儘量減少不必要的記憶體複製 ,另一個是減少 reflect 的使用—— 同一型別的物件,jsoniter 只調用 reflect 解析一次之後即快取下來。

  3. 不過隨著 go 版本的迭代,原生 json 庫的效能也越來越高,jsonter 的效能優勢也越來越窄, 但仍有明顯優勢

sonicstar: 2k

  1. 相容標準庫;

  2. 通過JIT(即時編譯)和SIMD(單指令-多資料)加速;需要 go 1.15 及以上的版本,提供完成的 json 操作的 API, 是一個比 json-iterator 更優的選擇。

  3. 已經在抖音內部大範圍使用,且 github 庫維護給力,issues 解決積極,安全性有保證。

  4. sonic :基於 JIT 技術的開源全場景高效能 JSON 庫

easyjsonstar: 3.5k

  1. 支援序列化和反序列化;

  2. 通過程式碼生成的方式,達到不使用反射的目的;

相關壓測資料可見參考文章;

選型案例

業務場景

  1. 需要 Unmarshal map;

  2. json 導致的 GC 與 CPU 壓力較大;

  3. 業務較為重要,需要一個穩定的序列化庫;

選型思路

  1. easyjson 需要生成程式碼,喪失了 json 的靈活性,增加維護成本,因此不予考慮;

  2. sonic 需要 go 1.15 及以上的版本,且業務場景無 Unmarshal 到結構體的操作,因此暫時不做選擇;

  3. json-iterator 的優勢在於相容標準庫介面,但因為使用到了反射,效能相對較差,且業務場景沒有反序列化結構體的場景,因此不予考慮;

  4. fastjson、gjson、jsonparser 由於沒有用到反射,因此效能要高於 json-iterator。所以著重在這三個中選擇;

  5. fastjson 實現了 0 分配的開銷,但是 star 數較少,不予考慮;

  6. gjson 與 jsonparser 類似,速度及記憶體分配上各擅勝場,靈活性上也各有長處,比較難抉擇,但業務場景下不需要使用到其提供的靈活 API,而有 json 序列化到 map 的場景,所以 gjson 會有一些優勢,再結合 star 數後選擇 gjson;

結論

綜上所述,選用 gjson。

參考

  • 深入 Go 中各個高效能 JSON 解析庫 [1]

  • Go 語言原生的 json 包有什麼問題?如何更好地處理 JSON 資料? [2]

Cache

基本上從以下四種角度進行分析

  1. GC 方面,是否有針對性優化;

  2. 是否需要限制記憶體大小,如果限制,命中率與淘汰策略如何;

  3. TTL 支援程度:全域性、單個 key、不支援;

  4. 其他特性;

此處因業務場景原因未詳細探討淘汰策略方面,但這是本地快取中一個較為重要的部分;各 cache 的命中率,可以看 Introducing Ristretto: A High-Performance Go Cache [3] 的命中率報告部分;但正如《A large scale analysis of hundreds of in-memory cache clusters at Twitter》論文所提到,Under reasonable cache sizes, FIFO often shows similar performance as LRU, and LRU often exhibits advantages only when the cache size is severely limited. 因此在本地快取的場景下,淘汰策略的選擇對於快取命中率的影響當較為重要;

相關庫

go-cachestar: 5.7k

  1. 最簡單的 cache,可以直接儲存指標,下面的部分 Cache 都需要先把物件序列化為 []byte,會引入一定的序列化開銷,但可以用高效的序列化庫減少開銷;

  2. 可以對每個 key 設定 TTL;

  3. 無淘汰機制;

freecachestar: 3.6k

  1. 0 GC;

  2. 可以對每個 key 設定 TTL;

  3. 近 LRU 淘汰;

  4. 參考 深入理解Freecache [4]

bigcachestar: 5.4k

  1. 0 GC;

  2. 只有全域性 TTL,不能對每個 key 設定 TTL;

  3. 如果超過記憶體最大值(也可以不設定,記憶體使用無上限),採用的是 FIFO 策略;

  4. 產生 hash 衝突會導致舊值被覆蓋;

  5. 會在記憶體中分配大陣列用以達到 0 GC 的目的,一定程度上會影響到 GC 頻率;

  6. 參考 妙到顛毫: bigcache優化技巧 [5]

fastcachestar: 1.3k

  1. 0 GC;

  2. 不支援 TTL;

  3. 如果超過設定最大值,底層是 ring buffer,快取會被覆蓋掉, 採用的是 FIFO 策略;

  4. 呼叫 mmap 分配堆外記憶體,因此不會影響到 gc 頻率;

groupcachestar: 11k

  1. 一個較為複雜的 cache 實現,本質上是個 LRU cache;

  2. 是一個lib庫形式的程序內的分散式快取,也可以認為是本地快取,但不是簡單的單機快取,不過也可以作為單機快取;

  3. 參考 一個有趣的分散式快取實現 — groupcache

  4. 特性如下:單機快取和基於HTTP的分散式快取;最近最少訪問(LRU,Least Recently Used)快取策略;使用Golang鎖機制防止快取擊穿;使用一致性雜湊選擇節點以實現負載均衡;使用Protobuf優化節點間二進位制通訊;

goburrowstar: 468

  1. Go 中 Guava Cache 的部分實現;

  2. 沒有對 GC 做優化,內部使用 sync.map;

  3. 支援淘汰策略:LRU、Segmented LRU (default)、TinyLFU (experimental);

ristrettostar: 3.6k

  1. 在 GC 方面做了少量優化;

  2. 可以對每個 key 設定 TTL;

  3. 在吞吐方面做了較多優化,使得在複雜的淘汰策略下仍具有較好的吞吐水平;

  4. 在命中率方面,具備出色的准入政策和 SampledLFU 驅逐政策,因此高於其他 cache 庫;

  5. 參考 Introducing Ristretto: A High-Performance Go Cache [6]

選型案例

業務場景 - Feature 服務

  1. key 分鐘固定視窗失效,且 key 中自帶分鐘級時間戳;

  2. 記憶體容量足夠,有全域性 TTL 即可,不需要額外的淘汰機制;

  3. 快取 Key 數量較多,對 GC 壓力較大;

  4. Value 是 string,另外可以通過不安全方式無開銷轉換為 []byte;

  5. 業務較為重要,需要一個穩定的 cache 庫;

選型思路

  1. goburrow、ristretto 兩個 cache 的主打的是固定記憶體情況下的命中率,對 GC 無優化,且 Feature 服務的 Cache 是分鐘固定視窗失效,機器記憶體容量遠大於視窗內的快取 value 之和,因此不需要用到更好的淘汰機制,而且 Feature 服務本次更換 cahce 要解決的是快取中物件數量太多,導致的 GC 問題,因此不考慮這兩種;

  2. groupcache 是一個 LRU Cache,且功能較重,Feature 服務只需要一個本地 Cache 庫,不需要用到這些特性,因此不考慮這個 Cahce;

  3. fastcache 最大的問題是不支援 TTL,這個是 Feature 服務所不能接受的,因此不考慮這個Cahce;

  4. go-cache 類似於 Feature 服務中的 beego/cache 庫,最簡單的 Cache 庫,對 GC 無優化,且 Feature 服務的 value 本身就為 string 型別,不會引入序列化開銷,且可以通過不安全的方式實現 string 與 []byte 之間 0 開銷轉換;

  5. freecache、bigcache 比較適合 Feature 服務,freecache 的優勢在於近 LRU 的淘汰,並且可以對每個 Key 設定 TTL,但 Feature 服務記憶體空間足夠無需進行快取淘汰,且 key 名中自帶分鐘級時間戳,key 有效期都為 1min,因此無需使用 freecache;

  6. bigcache 相對於 freecache 的優勢之一是您不需要提前知道快取的大小,因為當 bigcache 已滿時,它可以為新條目分配額外的記憶體,而不是像 freecache 當前那樣覆蓋現有的。摘自: bigcache [7]

結論

綜上所述,bigcache 的序列化開銷、無法為每個 key 設定 TTL、快取淘汰效率差與命中率低等問題 Feature 服務都可以優雅避免,所以 bigcache 最適合作為當前場景下的本地 Cache。

參考資料

[1]

深入 Go 中各個高效能 JSON 解析庫: https://www.luozhiyun.com/archives/535

[2]

Go 語言原生的 json 包有什麼問題?如何更好地處理 JSON 資料?: https://segmentfault.com/a/1190000039957766

[3]

Introducing Ristretto: A High-Performance Go Cache: https://dgraph.io/blog/post/introducing-ristretto-high-perf-go-cache/

[4]

深入理解Freecache: https://blog.csdn.net/chizhenlian/article/details/108435024

[5]

妙到顛毫: bigcache優化技巧: https://colobu.com/2019/11/18/how-is-the-bigcache-is-fast/

[6]

Introducing Ristretto: A High-Performance Go Cache: https://dgraph.io/blog/post/introducing-ristretto-high-perf-go-cache/

[7]

bigcache: https://github.com/allegro/bigcache/blob/master/README.md