go post 参数_用 Go 编写能存数百万条记录仍非常快的缓存服务
點擊上方藍色“Go語言中文網”關注我們,領全套Go資料,每天學習?Go?語言
本文發布于 2016 年 3 月,但其中的設計技巧仍然有效。
我們的團隊需要編寫非常快速的緩存服務。目標非常明確,但可以通過多種方式實現。最后,我們決定嘗試一些新的東西,并在 Go 中實現該服務。本文描述了我們是如何做到的以及由此產生的價值。
目錄
需求
為什么用 Go
緩存
HTTP 服務器
JSON 反序列化
最終結果
總結
需求
根據需求,我們的服務應:
- 使用 HTTP 協議處理請求
- 支持 10K RPS (5k 寫,5k 讀)
- cache 對象至少保持 10 分鐘
- 響應時間平均 5ms,99.9% 以外 10ms, 99.999% 以外 400ms
- 處理包含 JSON 消息的 POST 請求,消息包含:
- 包含 ID 和內容
- 不超過 500 字節
- 通過 POST 請求添加記錄后,立即檢索記錄并通過 GET 請求返回 int,也就是保證一致性
簡而言之,我們的任務是編寫帶有到期時間和 REST 接口的快速字典。
為什么是 Go?
我們公司中的大多數微服務都是用 Java 或另一種基于 JVM 的語言編寫的,有些是用 Python 編寫的。我們也有一個用 PHP 編寫的單體式舊平臺,但是除非有必要,否則我們不會碰它。我們已經知道這些技術,但是我們愿意探索一種新技術。我們的任務可以用任何語言實現,因此我們決定用 Go 編寫。
在一家大公司(谷歌)和一個不斷增長的用戶社區的支持下,Go 發布已有一段時間了。它是一門編譯,并發,命令式,結構化的編程語言。它還具有自動內存管理,因此比 C/C++ 看起來更安全,更容易使用。我們在用 Go 編寫工具方面擁有相當豐富的經驗,因此決定在此處使用它。我們已經有了一個 Go 的開源項目[1],現在我們想知道 Go 如何處理大流量。我們相信整個項目用 Go 僅需不到 100 行的代碼即可完成任務,并且速度足以滿足我們的需求。
緩存
為了滿足需求,緩存本身需要:
- 即使有數以百萬計的記錄也非常快
- 提供并發訪問
- 在預定的時間后移除記錄
基于第一點,我們決定放棄外部緩存,例如 Redis,Memcached 或 Couchbase,主要是因為網絡上需要更多時間。因此,我們專注于內存中的緩存。在 Go 中已經有這種類型的緩存,即 LRU groupcache[2],go-cache[3],ttlcache[4],freecache[5] 等。只有 freecache 滿足我們的需求。接下來的子章節會揭示為什么我們決定還是堅持自己實現,并描述了如何實現上述需求。
并發
我們的服務將同時接收許多請求,因此我們需要提供對緩存的并發訪問。最簡單的方法是將 sync.RWMutex 放在緩存訪問功能的前面,以確保一次只能通過一個 goroutine 對其進行修改。但是,其他想對其進行修改的 goroutine 將被阻塞,使其成為瓶頸。為了消除此問題,可以應用 shards(分片)技術。分片背后的想法很簡單。創建 N 個分片的數組,每個分片包含自己的帶有鎖的緩存實例。當需要緩存具有唯一 key 的記錄時,首先通過函數 hash(key)%N 選擇一個分片。在此之后,獲取緩存鎖并對緩存進行寫入。記錄讀取是類似的。當分片的數量相對較多并且哈希函數返回的數字較分散時,鎖爭用幾乎可以降至零。這就是我們決定在緩存中使用分片的原因。
移除(Eviction)
從緩存中移除元素的最簡單方法是將其與 FIFO 隊列一起使用。將記錄添加到高速緩存后,將執行兩個附加操作:
- 包含 key 和創建時間戳的記錄被添加到隊列的末尾。
- 從隊列中讀取最早的記錄。將其創建時間戳與當前時間進行比較。如果晚于收回時間,則將隊列中的元素及其在緩存中的對應記錄一起刪除。
由于在寫入緩存期間已獲取了鎖,因此順帶執行移除操作。
避免 GC
在 Go 中,對于 map,垃圾收集器(GC)將在標記(mark)和掃描(scan)階段遍歷該 map 的每個條目。當 map 足夠大(包含數百萬個對象)時,這可能會對應用程序性能產生巨大影響。
我們對服務進行了一些測試,在該服務中向緩存添加了數百萬個記錄,然后我們開始將請求發送到一些不相關的 REST 端點(endpoint),僅執行靜態 JSON 序列化(根本不涉及緩存)。緩存為空時,此端點的最大響應延遲為 10ms,10k rps。當緩存被填滿時,它在 99 的百分位上有超過一秒鐘的延遲。度量標準表明,堆中有超過 4000 萬個對象,GC 標記和掃描階段花費了四秒鐘。該測試向我們表明,如果要滿足與響應時間有關的要求,則需要跳過 GC 以獲取緩存項。我們該怎么做?好吧,有三個選擇。
GC 僅限于堆,因此第一個選擇是堆外。有一個項目可以幫助解決這個問題,稱為 offheap[6]。它提供了自定義函數 Malloc()和 Free() 來管理堆外部的內存。但是,將需要實現依賴于那些功能的緩存。
第二種方法是使用 freecache[7]。Freecache 通過減少指針數量實現了具有零 GC 開銷的 map。它將鍵和值保留在環形緩沖區中,并使用索引切片查找條目。
為緩存條目避免 GC 的第三種方式與 Go 1.5 版本修復的一個 issue 有關(issue 9477[8])。此優化表明,如果你使用的是 key 和 value 中沒有指針的 map,則 GC 將忽略其內容。這是用堆,但為 map 中的條目避免 GC 的一種方法。但是,這并不是最終的解決方案,因為 Go 中的所有內容基本上是基于指針構建的:結構,切片甚至數組。只有諸如 int 或 bool 之類的基本類型才不是指針。那我們可以用 map[int]int 做什么?由于我們已經生成了 hashed key,以便從緩存中選擇適當的分片(在并發中進行了描述),因此我們可以將它們重新用作 map[int]int 中的鍵。但是 int 類型的值呢?我們可以將哪些信息保留為 int?我們可以保留條目的偏移量。另一個問題是如何保留這些條目以便再次避免 GC?可以分配大量的字節數組,并且可以將條目序列化為字節并保留在其中。為此,map[int]int 中的值可能指向一個偏移量,該偏移量是條目在目標數組中開始的位置。而且由于 FIFO 隊列用于保留條目并控制其移除(在 Eviction 中進行了描述),因此可以基于一個巨大的字節數組重新構建它,該 map 中的值也將指向該數組。
在所有以上指出的方案中,都需要條目(反)序列化。最終,我們決定嘗試第三種解決方案,因為我們想知道它是否會起作用,并且實際中我們會有大量元素—hashed key(在分片選擇階段計算)和條目隊列。
BigCache
為了滿足本章開頭提出的需求,我們實現了自己的緩存并將其命名為 BigCache。BigCache 提供分片,移除(即淘汰),并且避免了緩存條目的 GC 問題。結果,即使對于大量記錄,它也是非常快的緩存。
Freecache 是 Go 中唯一提供這種功能的可用內存中緩存之一。Bigcache 是它的替代解決方案,并以不同的方式減少了 GC 開銷,因此我們決定分享它:bigcache[9]。有關 freecache 和 bigcache 比較的更多信息,請參見 GitHub[10]。
HTTP Server
內存探查器(profiler)向我們展示了在請求處理期間分配了一些對象。我們知道 HTTP 處理程序將成為我們系統的熱點。我們的 API 非常簡單。我們僅接受 POST 和 GET 從緩存中上傳和下載元素。我們實際上僅支持一個 URL 模板,因此不需要功能齊全的路由器。我們通過剪切前 7 個字母從 URL 中提取了 ID,它對我們來說很好用。
當我們開始開發時,Go 1.6 發布了 RC 版。我們減少請求處理時間的第一個嘗試是將其更新到最新的 RC 版本。在我們的場景中,性能幾乎相同。我們開始尋找更有效的方法,然后找到了 fasthttp[11]。它是一個提供零分配 HTTP 服務器的庫。根據文檔,在綜合測試中,它通常比標準 HTTP 處理程序快 10 倍。在我們的測試過程中,結果證明它僅快 1.5 倍,但還是更好!
fasthttp 通過減少 HTTP Go 程序包完成的工作來提升其性能。例如:
- 將請求生存期限制為實際處理的時間
- header 是延遲解析的(我們真的不需要 header)
不幸的是,fasthttp 并不是標準 http 的真正替代。它不支持路由或 HTTP/2,并聲稱不能支持所有 HTTP 邊緣情況。對于具有簡單 API 的小型項目而言,這很好,因此對于正常(非超級性能)項目,我們會堅持使用默認 HTTP。
fasthttp vs nethttp
JSON 反序列化
在對應用程序進行性能分析時,我們發現該程序在 JSON 反序列化上花費了大量時間。內存探查器還報告說 json.Marshal 處理了大量數據。這并不令我們感到驚訝。對于 10k rps,每個請求 350 個字節對于任何應用程序來說都是重要的有效負載(payload)。盡管如此,我們的目標是速度,所以我們對其進行了調研。
我們聽說 Go JSON 序列化程序的速度不如其他語言快。大多數基準測試是在 2013 年完成的,因此這是在 1.3 版之前做的測試。當我們看到 issue-5683[12] 聲稱 Go JSON 比 Python 慢 3 倍,而郵件列表[13]說它比 Python simplejson[14] 慢 5 倍時,我們開始尋找更好的解決方案。
如果需要速度,基于 HTTP 的 JSON 絕對不是最佳選擇。不幸的是,我們所有的服務都以 JSON 相互通信,因此采用新協議超出了此任務的范圍(但我們正在考慮使用 avro[15],就像我們對 Kafka[16] 所做的那樣)。我們決定堅持使用 JSON。通過快速搜索找到了一個名為 ffjson 的解決方案。(polaris 注:此文是 2016 年寫的,一方面標準庫性能更好了,另一方面,也有更多 JSON 庫可供選擇。)
ffjson 文檔聲稱它比標準 json.Unmarshal 快 2-3 倍,并且使用的內存更少。
| ffjson | 8417 ns/op | 1555 B/op | 31 allocs/op |
我們的測試證實 ffjson 比內置 unmarshaler 快了近 2 倍,并且內存分配的次數更少。它是如何做到這一點的?
首先,為了從 ffjson 的所有功能中受益,我們需要為我們的結構生成一個 unmarshaller。生成的代碼實際上是一個解析器,它掃描字節并用數據填充對象。如果您看一下 JSON 語法,您會發現它確實很簡單。ffjson 充分利用了結構的外觀,僅解析結構中指定的字段,并在發生錯誤時快速失敗。標準庫的 marshaler 使用昂貴的反射調用來在運行時獲取結構定義。另一個優化是減少不必要的錯誤檢查。json.Unmarshal 將無法更快地執行較少的分配,并跳過反射調用。
| ffjson (invalid json) | 2598 ns/op | 528 B/op | 13 allocs/op |
有關 ffjson 如何工作的更多信息,請參見此處[17]。基準測試在這里[18]。
最終結果
最后,對于最耗時的請求,我們將時間從 2.5 秒以上縮短到了 250 毫秒以下。這些時間僅發生在我們的場景中。我們相信,對于大量的寫入操作或更長的移除(淘汰)周期,對標準緩存的訪問可能會花費更多的時間,但是對于 bigcache 或 freecache 來說,訪問時間可以保持在毫秒級,因為消除了長時間 GC 暫停這個根源。
下表比較了優化服務前后的響應時間。在測試期間,我們發送了 10k rps,從中寫入 5k,讀取另外 5k。移除時間設置為 10 分鐘。測試時間為 35 分鐘。
response times before and after optimizations使用上述相同的設置進行單獨測試(讀寫單獨)的最終結果如下。
final results總結
如果您不需要高性能,請使用標準庫。確保維護性,因為它們具有向后兼容性,因此升級 Go 版本會很順利。
我們用 Go 編寫的緩存服務終于滿足了我們的需求。大多數時候,我們花時間弄清楚 GC 暫停可能會對應用程序的響應性產生巨大影響,因為它控制著數百萬個對象。幸運的是,像 bigcache 或 freecache 這樣的緩存可以解決此問題。
原文鏈接:https://allegro.tech/2016/03/writing-fast-cache-service-in-go.html
作者:?ukasz Drumiński、Tomasz Janiszewski
參考資料
[1]開源項目: https://github.com/allegro/marathon-consul/#marathon-consul-
[2]LRU groupcache: https://github.com/golang/groupcache/tree/master/lru
[3]go-cache: https://github.com/patrickmn/go-cache
[4]ttlcache: https://github.com/diegobernardes/ttlcache
[5]freecache: https://github.com/coocood/freecache
[6]offheap: https://godoc.org/github.com/glycerine/offheap
[7]freecache: https://github.com/coocood/freecache
[8]issue 9477: https://github.com/golang/go/issues/9477
[9]bigcache: https://github.com/allegro/bigcache
[10]GitHub: https://github.com/allegro/bigcache#bigcache-vs-freecache
[11]fasthttp: https://github.com/valyala/fasthttp
[12]issue-5683: https://github.com/golang/go/issues/5683
[13]郵件列表: https://groups.google.com/forum/#!topic/golang-nuts/zCBUEB_MfVs
[14]simplejson: https://pypi.org/project/simplejson/
[15]avro: https://avro.apache.org/
[16]Kafka: https://allegro.tech/2015/08/spark-kafka-integration.html
[17]此處: https://journal.paul.querna.org/articles/2014/03/31/ffjson-faster-json-in-go/
[18]在這里: https://gist.github.com/janisz/8b20eaa1197728e09d6a
推薦閱讀
官方不推薦使用 Goroutine ID,但它自己卻使用了:原來是這么做的
【每日一庫】一個零 GC 的緩存庫:freecache
從Go開源項目BigCache學習加速并發訪問和避免高額的GC開銷
妙到顛毫:你應該學會的 bigcache 優化技巧
喜歡本文的朋友,歡迎關注“Go語言中文網”:
Go語言中文網啟用微信學習交流群,歡迎加微信:274768166,投稿亦歡迎
總結
以上是生活随笔為你收集整理的go post 参数_用 Go 编写能存数百万条记录仍非常快的缓存服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何缓解Golang大型游戏服务器的GC
- 下一篇: 为什么程序员工位上总会摆着小黄鸭?