用Go开发支持百万级数据量的高性能缓存服务
最近,我們的團隊負責編寫一個高性能的緩存服務。目標很明確,但可以通過多種方式實現。最后,我們決定嘗試新的技術使用Go實現該服務。
目錄:
需求
根據需求,我們的服務應該:
- 使用HTTP協議來處理請求
- 處理10k rps(寫入為5k,讀取為5k)
- 緩存數據至少10分鐘
- 響應時間(不包括在網絡上花費的時間)低于
- 5ms - 平均
- 10ms -?99.9%滿足
- 400ms - 99.999%滿足
- 處理包含JSON消息的POST請求,其中每條消息:
- 包含一個條目及其ID
- 不大于500字節
- 在通過POST請求添加條目后立即通過GET請求檢索條目并返回int(一致性)
簡單來說,我們的任務是編寫一個帶有過期和REST接口的快速字典。
為什么用GO
我們公司的大多數微服務都是用Java或其他基于JVM的語言編寫的,有些是用Python編寫的。我們還有一個用PHP編寫的單一的遺留平臺,但除非必須,否則我們不會觸摸它。我們已經了解這些技術,但我們愿意探索新技術。我們的任務可以用任何語言實現,因此我們決定在Go中編寫它。
Go已經有一段時間了,有大公司和不斷增長的用戶社區支持。它被宣傳為編譯的,并發的,命令式的,結構化的編程語言。它還具有托管內存,因此它看起來比C / C ++更安全,更容易使用。我們對使用Go編寫的工具有很好的經驗,并決定在這里使用它。我們在Go有一個開源項目,現在我們想知道Go如何處理大流量。使用Go我們相信整個項目只需要不到100行代碼,并且足夠快可以滿足我們的要求。
緩存
為了滿足要求,緩存本身需要:
- 即使有數百萬條目,也要非常快
- 提供并發訪問
- 過期后清除
考慮到第一點,我們決定放棄外部緩存,如Redis,Memcached或Couchbase,主要是因為網絡需要額外的時間。因此,我們專注于內存緩存。在Go中已經存在這種類型的緩存,即LRU組緩存,?go-cache,ttlcache,freecache。只有freecache滿足了我們的需求。接下來的子章節揭示了為什么我們決定自己推銷自己,并描述如何實現上述特征。
并發
我們的服務會同時收到許多請求,因此我們需要提供對緩存的并發訪問。實現這一目標的簡單方法是放在sync.RWMutex緩存訪問功能之前,以確保一次只能修改一個goroutine。然而,其他想要對其進行修改的goroutine也會被阻止,從而成為瓶頸。為了消除這個問題,可以使用切片。切片背后的想法很簡單。創建N個切片的數組,每個切片包含其自己的具有鎖的緩存實例。當需要緩存具有唯一鍵的項時,首先由該函數選擇它的切片hash(key) % N。在獲取緩存鎖并發生對緩存的寫入之后。項目讀數是類似的。當切片的數量相對較高并且哈希函數返回唯一鍵的正確分布的數字時,則鎖競爭幾乎可以最小化為零。這就是我們決定在緩存中使用切片的原因。
過期
從緩存中刪除過期元素的最簡單方法是將它與FIFO隊列一起使用。將條目添加到緩存時,會發生另外兩個操作:
由于已經獲取了鎖,因此在寫入緩存期間執行刪除。
省略垃圾收集器
在Go中,如果使用Map,垃圾收集器(GC)將在標記和掃描階段查詢該Map的每個元素。當Map足夠大(包含數百萬個對象)時,這會對應用程序性能產生巨大影響。
我們對我們的服務進行了一些測試,我們在其中為數百萬條目提供緩存,之后我們開始向一些不相關的REST端點發送請求,只執行靜態JSON序列化(它根本沒有觸及緩存)。對于空緩存,此端點的最大響應延遲為10k rps,為10ms。當緩存填滿時,它有超過第99%的延遲。度量標準表明堆中有超過40萬個對象,GC標記和掃描階段耗時超過4秒。測試結果表明,如果我們想要滿足與響應時間相關的要求,我們需要跳過GC以獲取緩存條目。我們該如何做?有下面三種解決辦法。
GC僅限于堆,所以第一種就是堆外。有一個項目可以幫助解決這個問題,稱為offheap。它提供自定義功能Malloc()并Free()管理堆外部的內存。但是,需要實現依賴于這些功能的緩存。
第二種方法是使用freecache。Freecache通過減少指針數來實現零GC開銷的映射。它將鍵和值保存在環形緩沖區中,并使用索引切片查找條目。
省略GC用于緩存條目的第三種方法與Go 1.5中提供的優化有關。此優化表明,如果您在鍵和值中使用沒有指針的映射,則GC將省略其內容。這是一種保持堆積并省略GC以獲取Map中條目的方法。但是,它不是最終解決方案,因為Go中的所有內容基本上都是基于指針構建的:結構,切片,甚至是固定數組。只有原函數喜歡int或bool不接觸指針。那么我們可以用map[int]int做些什么呢?因為我們已經生成了哈希鍵以便從緩存中選擇正確的切片(在并發中描述),所以我們將它們重用為我們的密鑰map[int]int。但是int類型的價值呢?我們可以保留哪些信息做為int?我們可以保留條目的偏移量。另一個問題是,為了再次省略GC,可以保留這些條目嗎?可以分配大量字節,并且可以將條目序列化為字節并保留在其中。在這方面,值map[int]int可以指向一個條目,其中條目在建議的數組中開始。并且由于FIFO隊列用于保存條目并控制它們的刪除(在Eviction中描述),因此可以重建它并基于巨大的字節數組,該映射的值也將指向該數組。
在所有呈現的場景中,都需要進入(de)序列化。最后,我們決定嘗試第三種解決方案,因為我們很好奇它是否能夠工作并且我們已經擁有大多數元素 - 哈希鍵(在切片選擇階段計算)和條目隊列。
BigCache
為了滿足本章開頭提出的要求,我們實現了自己的緩存并將其命名為BigCache。BigCache提供切片,過期刪除,并省略了GC用于緩存條目。因此,即使對于大量數據,它也是非常快速的緩存。
Freecache是??Go中唯一可用的內存緩存,它提供了這種功能。Bigcache是??它的替代解決方案,并以不同的方式減少GC開銷,因此我們決定與它共享:bigcache。有關freecache和bigcache之間比較的更多信息,請訪問github。
HTTP服務器
內存分析器向我們顯示在請求處理期間分配了一些對象。我們知道HTTP處理程序將成為我們系統的熱點。我們的API非常簡單。我們只接受POST和GET來上傳和下載緩存中的元素。我們實際上只支持一個URL模板,因此不需要功能齊全的路由器。我們通過剪切前7個字母從URL中提取ID,它運行的很好。
當我們開始開發時,Go 1.6在RC中。我們減少請求處理時間的第一個努力是更新到最新的RC版本。在我們的案例中,表現幾乎相同。我們開始尋找更高效的東西,我們找到了?fasthttp。它是一個提供零分配HTTP服務器的庫。根據文檔,它在合成測試中比標準HTTP處理程序快10倍。在我們的測試中,結果發現它只快了1.5倍,但仍然更好!
fasthttp通過減少HTTP Go包的工作來提高其性能。例如:
- 它將請求生命周期限制在實際處理的時間
- 請求頭是懶惰解析(我們真的不需要請求頭)
不幸的是,fasthttp并不是標準http的真正替代品。它不支持路由或HTTP / 2并聲稱不支持所有HTTP邊緣技術。它適用于具有簡單API的小型項目,因此我們會堅持使用默認HTTP進行正常(非超級性能)項目。
JSON反序列化
在分析我們的應用程序時,我們發現該程序在JSON反序列化上花費了大量時間。內存分析器還報告說,處理了大量數據json.Marshal。它并沒有讓我們感到驚訝。對于10k rps,每個請求350個字節可能是任何應用程序的重要負載。然而,我們的目標是速度,所以我們研究了它。
我們聽說Go JSON序列化程序沒有其他語言那么快。大多數基準測試都是在2013年完成的,所以在1.3版之前。當我們看到問題-5683聲稱Go比Python慢??3倍并且?郵件列表說它比Python?simplejson慢5倍時,我們開始尋找更好的解決方案。
如果您需要速度,JSON over HTTP絕對不是最佳選擇。不幸的是,我們所有的服務都以JSON相互通信,因此合并新協議超出了此任務的范圍(但我們正在考慮使用avro,就像我們為Kafka所做的那樣)。我們決定堅持使用JSON。快速搜索為我們提供了一個名為ffjson的解決方案。
ffjson文檔聲稱它比標準快2-3倍json.Unmarshal,并且使用更少的內存來完成它。
| JSON | 16154 ns / op | 1875年B / op | 37 allocs / op |
| ffjson | 8417 ns / op | 1555 B / op | 31 allocs / op |
我們的測試證實,ffjson比內置的解組器快了近2倍并且執行的分配更少。怎么可能實現這個目標?
首先,為了從ffjson的所有功能中受益,我們需要為struct生成一個unmarshaller。生成的代碼實際上是一個掃描字節的解析器,并用數據填充對象。如果你看一下JSON語法,你會發現它非常簡單。ffjson利用了解結構的確切內容,只解析結構中指定的字段,并在發生錯誤時快速失敗。標準編組程序使用昂貴的反射調用來在運行時獲取結構定義。另一個優化是減少不必要的錯誤檢查。json.Unmarshal將更快地執行更少的alloc,并跳過反射調用。
| json(無效的json) | 1027 ns / op | 384 B / op | 9 allocs / op |
| ffjson(無效的json) | 2598 ns / op | 528 B / op | 13 allocs / op |
有關ffjson如何工作的更多信息,請點擊此處。基準測試可在此處獲得
結論
最后,我們將應用程序從2.5秒以上加速到不到250毫秒,以獲得最長的請求。這些時間只發生在我們的用例中。我們相信,對于更多的寫入或更長的過期時間,訪問標準緩存可能需要更多的時間,但是使用bigcache或freecache它可以保持毫秒級別,因為消除了長GC暫停的問題。
下圖顯示了優化服務之前和之后的響應時間的比較。在測試期間,我們發送了10k rps,其中5k是寫入,另外5k是讀取。過期時間設定為10分鐘。測試時間為35分鐘。
最終結果是隔離的,具有與上述相同的設置。
概要
如果您不需要高性能,請堅持使用標準庫。它們保證可以維護,并且具有向后兼容性,因此升級Go版本應該是順暢的。
我們用Go編寫的緩存服務終于滿足了我們的要求。我們花費大部分時間來確定GC停頓會對應用程序響應能力產生巨大影響,因為它控制著數百萬個對象。幸運的是,像bigcache或freecache這樣的緩存解決了這個問題。
總結
以上是生活随笔為你收集整理的用Go开发支持百万级数据量的高性能缓存服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 各互联网公司面试题整理
- 下一篇: 各品牌交换机常用命令整理