首頁 > 科技 > 正文

Golang實現單機百萬長連接服務 - 美圖的三年優化經驗

更新日期:2019-11-10 14:19:17

導讀:美圖長連接服務歷時三年,在內存優化上積累比較豐富的實踐經驗,本文將會介紹我們團隊這些年在內存優化道路上做的一些嘗試。

作者簡介:王鴻佳,系統研發工程師,現任職于美圖公司,主要從事通訊及存儲相關領域的研發。參與了通用長連接通道、美圖推送、分布式數據庫(Titan 已開源)、路由分發器等項目研發。對基礎研發技術及開源項目有濃厚的興趣。

美圖長連接服務簡介

隨著科技的飛速發展,技術的日新月異,長連接的運用場景日益增多。不僅在后端服務中被廣泛運用,比較常見的有數據庫的訪問、服務內部狀態的協調等,而且在 App 端的消息推送、聊天信息、直播彈字幕等場景長連接服務也是優選方案。長連接服務的重要性也在各個場合被業界專家不斷提及,與此同時也引起了更為廣泛地關注和討論,各大公司也開始構建自己的長連接服務。

美圖公司于2016 年初開始構建長連接服務,與此同時, Go 在編程語言領域異軍突起,考慮到其豐富的編程庫,完善的工具鏈,簡單高效的并發模型等優勢,使我們最終選擇 Go 去作為實現長連接服務的語言。在通信協議的選擇上,考慮到 MQTT 協議的輕量、簡單、易于實現的優點,選擇了 MQTT 協議作為數據交互的載體。其整體的架構會在下文中做相應地介紹。

美圖長連接服務(項目內部代號為bifrost )已經歷時三年,在這三年的時間里,長連接服務經過了業務的檢驗,同時也經歷了服務的重構,存儲的升級等,長連接服務從之前支持單機二十幾萬連接到目前可以支撐單機百萬連接。在大多數長連接服務中存在一個共性問題,那就是內存占用過高,我們經常發現單個節點幾十萬的長連接,內存卻占用十幾G 甚至更多,有哪些手段能降低內存呢?

本文將從多個角度介紹長連接服務在內存優化路上的探索,首先會先通過介紹當前服務的架構模型,Go 語言的內存管理,讓大家清晰地了解我們內存優化的方向和關注的重要數據。后面會重點介紹我們在內存優化上做的一些嘗試以及具體的優化手段,希望對大家有一定的借鑒意義。

架構模型

一個好的架構模型設計不僅能讓系統有很好的可擴展性,同時也能在服務能力上有很好的體現。除此之外,在設計上多考慮數據的抽象、模塊的劃分、工具鏈的完善,這樣不僅能讓軟件具有更靈活的擴展能力、服務能力更高,也提高系統的穩定性和健壯性以及可維護性。

在數據抽象層面抽象pubsub 數據集合,用于消息的分發和處理。模塊劃分層面我們將服務一分為三:內部通訊(grpcsrv)、外部服務(mqttsrv)、連接管理(session)。工具鏈的方面我們構建了自動化測試,系統 mock ,壓測工具。美圖長連接服務架構設計如下:

圖一架構圖

從架構圖中我們可以清晰地看到由7 個模塊組成,分別是:conf 、grpcsrv 、mqttsrv、session、pubsub、packet、util ,每個模塊的作用如下:

  • conf :配置管理中心,負責服務配置的初始化,基本字段校驗。

  • grpcsrv :grpc 服務,集群內部信息交互協調。

  • mqttsrv :mqtt 服務,接收客戶端連接,同時支持單進程多端口 MQTT 服務。

  • session :會話模塊,管理客戶端狀態變化,MQTT 信息的收發。

  • pubsub :發布訂閱模塊,按照 Topic 維度保存 session 并發布 Topic 通知給 session。

  • packet:協議解析模塊,負責 MQTT 協議包解析。

  • util :工具包,目前集成監控、日志、grpc 客戶端、調度上報四個子模塊。

Go 的內存管理

眾所周知,Go 是一門自帶垃圾回收機制的語言,內存管理參照 tcmalloc 實現,使用連續虛擬地址,以頁( 8k )為單位、多級緩存進行管理。針對小于16 byte 直接使用Go的上下文P中的mcache分配,大于 32 kb 直接在 mheap 申請,剩下的先使用當前 P 的 mcache 中對應的 size class 分配 ,如果 mcache 對應的 size class 的 span 已經沒有可用的塊,則向 mcentral 請求。如果 mcentral 也沒有可用的塊,則向 mheap 申請,并切分。如果 mheap 也沒有合適的 span,則向操作系統申請。

Go 在內存統計方面做的也是相當出色,提供細粒度的內存分配、GC 回收、goroutine 管理等統計數據。在優化過程中,一些數據能幫助我們發現和分析問題,在介紹優化之前,我們先來看看哪些參數需要關注,其統計參數如下:

  • go_memstats_sys_bytes :進程從操作系統獲得的內存的總字節數 ,其中包含 Go 運行時的堆、棧和其他內部數據結構保留的虛擬地址空間。

  • go_memstats_heap_inuse_bytes:在 spans 中正在使用的字節。其中不包含可能已經返回到操作系統,或者可以重用進行堆分配,或者可以將作為堆棧內存重用的字節。

  • go_memstats_heap_idle_bytes:在 spans 中空閑的字節。

  • go_memstats_stack_sys_bytes:棧內存字節,主要用于 goroutine 棧內存的分配。

在內存監控中根據Go 將堆的虛擬地址空間劃分為 span ,即對內存8K或更大的連續區域進行統計。span 可能處于以下三種狀態之一 :

  1. idle 不包含對象或其他數據,空閑空間的物理內存可以釋放回 OS (但虛擬地址空間永遠不會釋放),或者可以將其轉換為使用中或棧空間;

  2. inuse 至少包含一個堆對象,并且可能有空閑空間來分配更多的堆對象;

  3. stack span 用于 goroutine 棧,棧不被認為是堆的一部分。span 可以在堆和堆棧內存之間更改,但它從來不會同時用于兩者。

此外有一部分統計沒有從堆內存中分配的運行時內部結構(通常因為它們是實現堆的一部分),與堆棧內存不同,分配給這些結構的任何內存都專用于這些結構,這些主要用于調試運行時內存開銷。

雖然Go 擁有了豐富的標準庫、語言層面支持并發、內置runtime,但相比C/C++ 完成相同邏輯的情況下 Go 消耗內存相對增多。在程序的運行過程中,它的 stack 內存會隨著使用而自動擴容,但在 stack 內存回收采用惰性回收方式,一定程度的導致內存消耗增多,此外還有GC 機制也會帶來額外內存的消耗。

Go 提供了三種內存回收機制:定時觸發,按量觸發,手動觸發。在內存垃圾少量的情況下,Go 可以良好的運行。但是無論采用哪種觸發方式,由于在海量用戶服務的情況下造成的垃圾內存是巨大的,在 GC 執行過程中服務都會感覺明顯的卡頓。這些也是目前長連接服務面對的難題,在下文中我將會逐一介紹我們如何減少和解決問題的產生的具體實踐。

優化之路

在了解架構設計、Go 的內存管理、基礎監控后,相信大家已經對當前系統有了一個大致的認識,先給大家展示一下內存優化的成果,下表一是內存優化前后的對比表,在線連接數基本相同的情況下,進程內存占用大幅度降低,其中 stack 申請內存降低約 5.9 G,其次 heap 使用內存降低 0.9 G,other 申請內存也小幅下降。那么我們是如何做到內存降低的呢?那接下來我將會把我們團隊關于進行內存優化的探索和大家聊一聊。


優化前

優化后

在線鏈接數

225 K

225 K

進程占用內存

13.4 G

4.7 G

heap 使用內存

5.2 G

3.4 G

stack 申請內存

7.25 G

1.02 G

other 申請內存

0.9 G

0.37 G

表一內存優化前后的對比表

備注:進程占用內存≈ 虛擬內存- 未歸還內存

在優化前隨機抽取線上一臺機器進行分析內存,通過監控發現當前節點進程占用虛擬內存為22.3 G,堆區使用的內存占用 5.2 G ,堆區未歸還內存為 8.9 G,棧區內存為 7.25 G,其它約占用 0.9 G,連接數為 225 K。

我們簡單進行換算,可以看出平均一個鏈接占用的內存分別為:堆:23K,棧:32K。通過對比業內長連接服務的數據可以看出單個鏈接占用的內存偏大,根據監控數據和內存分配原理分析主要原因在:goroutine 占用、session 狀態信息、pubsub 模塊占用,我們打算從業務、程序、網絡模式三個方面進行優化。

業務優化

上文中提到 session 模塊主要是用于處理消息的收發,在實現時考慮到在通常場景中業務的消息生產大于客戶端消息的消費速度的情況,為了緩解這種狀況,設計時引入消息的緩沖隊列,這種做法同樣也有助于做客戶端消息的流控。

緩沖消息隊列借助chan 實現 ,chan 大小根據經驗將初始化默認配置為 128 。但在目前線上推送的場景中,我們發現,消息的生產一般小于消費的速度,128 緩沖大小明顯偏大,因此我們把長度調整為 16 ,減少內存的分配。

在設計中按照topic 對客戶端進行分組管理的算法中,采用空間換時間的方式,組合 map 和 list 兩種數據結構對于客戶端集合操作提供O(1)的刪除、O(1)的添加、O(n)的遍歷。數據的刪除采用標記刪除方式,使用輔助 slice 結構進行記錄,只有到達預設閾值才會進行真正的刪除。雖然標記刪除提高了遍歷和添加的性能,但也同樣帶來了內存損耗問題。

大家一定好奇什么樣的場景需要提供這樣的復雜度,在實際中其場景有以下兩種情況:

  1. 在實際的網絡場景中,客戶端隨時都可能由于網絡的不穩定斷開或者重新建聯,因此集合的增加和刪除需要在常數范圍內。

  2. 在消息發布的流程中,采用遍歷集合逐一發布通知方式,但隨著單個topic 上的用戶量的增加,經常會出現單個 topic 用戶集合消息過熱的問題,耗時太久導致消息擠壓,因此針對集合的遍歷當然也要求盡量快。

通過benchamrk 數據分析,在標記回收 slice 長度在 1000 時,可以提供最佳的性能,因此默認配置閾值為 1000。在線上服務中,無特殊情況都是采用默認配置。但在當前推送服務的使用中,發現標記刪除和延遲回收機制好處甚微,主要是因為 topic 和客戶端為 1 : 1 方式,也就是不存在客戶端集合,因此調整回收閾值大小為 2,減少無效內存占用。

上述所有優化,只要簡單調整配置后服務灰度上線即可,在設計實現時通過conf 模塊動態配置,降低了服務的開發和維護成本。通過監控對比優化效果如下表,在優化后在線連接數比優化的在線連接更多的情況下, heap 使用內存使用數量由原來的 4.16G 下降到了 3.5G ,降低了約 0.66 G。

golang 代碼優化

在實現上面展示的架構的時候發現在session 模塊 和 mqttsrv 模塊之間存在很多共享變量,目前實現方式都是采用指針或者值拷貝的,由于 session的數量和客戶端數據量成正比也就導致消耗大量內存用于共享數據,這不僅僅增加 GC 壓力,同樣對于內存的消耗也是巨大的。就此問題思考再三,參考系統的庫 context 的設計在架構中也抽象 context 包負責模塊之間交互信息傳遞,統一分配內存。此外還參考他人減少臨時變量的分配的優化方式,提高系統運行效率。主要優化角度參考如下:

  • 在頻繁申請內存的地方,使用pool 方式進行內存管理

  • 小對象合并成結構體一次分配,減少內存分配次數

  • 緩存區內容一次分配足夠大小空間,并適當復用

  • slice 和 map 采 make 創建時,預估大小指定容量

  • 調用棧避免申請較多的臨時對象

  • 減少byte 與 string 之間轉換,盡量采用 byte 來字符串處理

目前系統具被完備的單元測試、集成測試,因此經過一周的快速的開發重構后灰度上線監控數據對比如下表:在基本相同的連接數上,heap 使用內存約占用降低 0.27G,stack 申請內存占用降低 3.81G。為什么 stack 會大幅度降低呢?

通過設置stackDebug 重新編譯程序追查程序運行過程,優化前 goroutine 棧的大多數在內存為 16K,通過減少臨時變量的分配,拆分大函數處理邏輯,有效的減少觸發棧的內存擴容,優化后 goroutine 棧內存降低到 8 K。一個連接需要啟動兩個 goroutine 負責數據的讀和寫,粗略計算一個連接減少約 16 K 的內存,23 w 連接約降低 3.68 G 內存。

網絡模型優化

在Go 語言的網絡編程中經典的實現都是采用同步處理方式,啟動兩個 goroutine 分別處理讀和寫請求,goroutine 也不像 thread ,它是輕量級的。但對于一百萬連接的情況,這種設計模式至少要啟動兩百萬的 goroutine,其中一個 goroutine 使用棧的大小在 2 KB 到 8KB, 對于資源的消耗也是極大的。在大多數場景中,只有少數連接是有數據處理,大部分 goroutine 阻塞 IO 處理中。在因此可以借鑒 C 語言的設計,在程序中使用 epoll 模型做事件分發,只有活躍連接才會啟動 goroutine 處理業務,基于這種思想修改網絡處理流程。

網絡模型修改測試完成后開始灰度上線,通過監控數據對比如下表:在優化后比優化前的連接數多10 K的情況下,heap 使用內存降低 0.33 G,stack 申請內存降低 2.34 G,優化效果顯著。

總結

在經過業務優化,臨時內存優化,網絡模型優化操作后,線上服務保證21w 長連接在線實際內存占用約為 5.1 G。簡單進行壓測 100w 連接只完成建立連接,不進行其他操作約占用 10 G。長連接服務內存優化已經取得階段性的成功,但是這僅僅是我們團隊的一小步,未來還有更多的工作要做:網絡鏈路、服務能力,存儲優化等,這些都是亟待探索的方向。如果大家有什么好的想法,歡迎與我們團隊分享,共同探討。

bifrost項目目前我們有開源計劃,敬請大家期待。

參考文章

go tool pprof 使用介紹 :https://segmentfault.com/a/1190000016412013

Go 內存監控介紹:https://golang.org/src/runtime/mstats.go

Go 內存優化介紹:https://blog.golang.org/profiling-go-programs

高性能Go服務內存分配:https://segment.com/blog/allocation-efficiency-in-high-performance-go-services

Go stack 優化分析:https://studygolang.com/articles/10597

參考閱讀:

  • 正式支持多線程!Redis 6.0與老版性能對比評測

  • 你真的了解性能壓測中的SLA嗎?

  • 一個Netflix開發的微服務編排引擎,支持可視化工作流定義

  • 你真的了解壓測嗎?實戰講述性能測試場景設計和實現

  • 關于Golang GC的一些誤解--真的比Java算法更領先嗎?

高可用架構

改變互聯網的構建方式


相關:

中國乳制品市場非常有活力中國青年報客戶端訊(中國青年報·中國青年網記者 張均斌)“我16個月之前來到中國,幾乎每天都能看到中國市場上的新變化,這讓我很驚訝。中國的乳制品市場,我可以簡單地用三個關鍵詞概括:非常有活力、創新力,..

中東歐進出口商品交易會在河北滄州舉行這是交易會上展出的飛機(11月9日無人機拍攝)。當日,中東歐進出口商品交易會在河北滄州舉行,共有來自20余個國家和地區的300余種產品參展。 新華社記者 金良快 攝11月9日,人們參觀中國與捷克聯合研發的“天..

民眾深受觸動:4K直播電影《大閱兵·2019》在希臘首播IT之家11月9日消息 據央視新聞報道,當地時間8日由中央廣播電視總臺制作的4K電影《大閱兵·2019》在希臘雅典大學正式上映。希臘大學師生一同觀看了這一節目。據報道,這也是4K電影《大閱兵·2019》希臘語版的全球..

相關熱詞搜索:新托福 新挑戰 新捷達 青山佑香 青山光青山遮不住 畢竟東流去

上一篇: 國家市場監管局副局長田世宏會見特斯拉董事長
下一篇: 巴基斯坦總理顧問:寶石金礦將是中巴合作的新領域

排列3开奖直播链接