日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網營銷解決方案
優(yōu)化 Golang 分布式行情推送的性能瓶頸

優(yōu)化 Golang 分布式行情推送的性能瓶頸

作者:峰云就她了 2021-07-05 08:58:17

開發(fā)

前端

分布式 最近一直在優(yōu)化行情推送系統(tǒng),有不少優(yōu)化心得跟大家分享下。性能方面提升最明顯的是時延,在單節(jié)點8萬客戶端時,時延從1500ms優(yōu)化到40ms,這里是內網mock客戶端的得到的壓測數據。

創(chuàng)新互聯(lián)公司基于分布式IDC數據中心構建的平臺為眾多戶提供服務器托管 四川大帶寬租用 成都機柜租用 成都服務器租用。

本文轉載自微信公眾號「碼農桃花源」,作者峰云就她了 。轉載本文請聯(lián)系碼農桃花源公眾號。

最近一直在優(yōu)化行情推送系統(tǒng),有不少優(yōu)化心得跟大家分享下。性能方面提升最明顯的是時延,在單節(jié)點8萬客戶端時,時延從1500ms優(yōu)化到40ms,這里是內網mock客戶端的得到的壓測數據。

對于訂閱客戶端數沒有太執(zhí)著量級的測試,弱網絡下單機8w客戶端是沒問題的。當前采用的是kubenetes部署方案,可靈活地擴展擴容。

架構圖

push-gateway是推送的網關,有這么幾個功能:第一點是為了做鑒權;第二點是為了做接入多協(xié)議,我們這里實現(xiàn)了websocket, grpc, grpc-web,sse的支持;第三點是為了實現(xiàn)策略調度及親和綁定等。

push-server 是推送服務,這里維護了訂閱關系及監(jiān)聽mq的新消息,繼而推送到網關。

問題一:并發(fā)操作map帶來的鎖競爭及時延

推送的服務需要維護訂閱關系,一般是用嵌套的map結構來表示,這樣造成map并發(fā)競爭下帶來的鎖競爭和時延高的問題。

  
 
 
 
  1. // xiaorui.cc  
  2. {"topic1": {"uuid1": client1, "uuid2": client2}, "topic2": {"uuid3": client3,  "uuid4": client4}   ... }  

已經根據業(yè)務拆分了4個map,但是該訂閱關系是嵌套的,直接上鎖會讓其他協(xié)程都阻塞,阻塞就會造成時延高。

加鎖操作map本應該很快,為什么會阻塞?上面我們有說過該map是用來存topic和客戶端列表的訂閱關系,當我進行推送時,必然是需要拿到該topic的所有客戶端,然后進行一個個的send通知。(這里的send不是io.send,而是chan send,每個客戶端都綁定了緩沖的chan)

解決方法:在每個業(yè)務里劃分256個map和讀寫鎖,這樣鎖的粒度降低到1/256。除了該方法,開始有嘗試過把客戶端列表放到一個新的slice里返回,但造成了 GC 的壓力,經過測試不可取。

  
 
 
 
  1. // xiaorui.cc 
  2.  
  3. sync.RWMutex 
  4. map[string]map[string]client 
  5.  
  6. 改成這樣 
  7.  
  8. m *shardMap.shardMap 

分段map的庫已經推到github[1]了,有興趣的可以看看。

問題二:串行消息通知改成并發(fā)模式

簡單說,我們在推送服務維護了某個topic和1w個客戶端chan的映射,當從mq收到該topic消息后,再通知給這1w個客戶端chan。

客戶端的chan本身是有大buffer,另外發(fā)送的函數也使用 select default 來避免阻塞。但事實上這樣串行發(fā)送chan耗時不小。對于channel底層來說,需要goready等待channel的goroutine,推送到runq里。

下面是我寫的benchmark[2],可以對比串行和并發(fā)的耗時對比。在mac下效果不是太明顯,因為mac cpu頻率較高,在服務器里效果明顯。

串行通知,拿到所有客戶端的chan,然后進行send發(fā)送。

  
 
 
 
  1. for _, notifier := range notifiers { 
  2.     s.directSendMesg(notifier, mesg) 

并發(fā)send,這里使用協(xié)程池來規(guī)避morestack的消耗,另外使用sync.waitgroup里實現(xiàn)異步下的等待。

  
 
 
 
  1. // xiaorui.cc 
  2.  
  3. notifiers := []*mapping.StreamNotifier{} 
  4. // conv slice 
  5. for _, notifier := range notifierMap { 
  6.     notifiers = append(notifiers, notifier) 
  7.  
  8.  
  9. // optimize: direct map struct 
  10. taskChunks := b.splitChunks(notifiers, batchChunkSize) 
  11.  
  12.  
  13. // concurrent send chan 
  14. wg := sync.WaitGroup{} 
  15. for _, chunk := range taskChunks { 
  16.     chunkCopy := chunk // slice replica 
  17.     wg.Add(1) 
  18.     b.SubmitBlock( 
  19.         func() { 
  20.             for _, notifier := range chunkCopy { 
  21.                 b.directSendMesg(notifier, mesg) 
  22.             } 
  23.             wg.Done() 
  24.         }, 
  25.     ) 
  26. wg.Wait() 

按線上的監(jiān)控表現(xiàn)來看,時延從200ms降到30ms。這里可以做一個更深入的優(yōu)化,對于少于5000的客戶端,可直接串行調用,反之可并發(fā)調用。

問題三:過多的定時器造成cpu開銷加大

行情推送里有大量的心跳檢測,及任務時間控速,這些都依賴于定時器。go在1.9之后把單個timerproc改成多個timerproc,減少了鎖競爭,但四叉堆數據結構的時間復雜度依舊復雜,高精度引起的樹和鎖的操作也依然頻繁。

所以,這里改用時間輪解決上述的問題。數據結構改用簡單的循環(huán)數組和map,時間的精度弱化到秒的級別,業(yè)務上對于時間差是可以接受的。

Golang時間輪的代碼已經推到github[3]了,時間輪很多方法都兼容了golang time原生庫。有興趣的可以看下。

問題四:多協(xié)程讀寫chan會出現(xiàn)send closed panic的問題

解決的方法很簡單,就是不要直接使用channel,而是封裝一個觸發(fā)器,當客戶端關閉時,不主動去close chan,而是關閉觸發(fā)器里的ctx,然后直接刪除topic跟觸發(fā)器的映射。

  
 
 
 
  1. // xiaorui.cc 
  2.  
  3. // 觸發(fā)器的結構 
  4. type StreamNotifier struct { 
  5.     Guid  string 
  6.     Queue chan interface{} 
  7.  
  8.  
  9.     closed int32 
  10.     ctx    context.Context 
  11.     cancel context.CancelFunc 
  12.  
  13.  
  14. func (sc *StreamNotifier) IsClosed() bool { 
  15.     if sc.ctx.Err() == nil { 
  16.         return false 
  17.     } 
  18.     return true 
  19.  
  20. ... 

問題五:提高grpc的吞吐性能

grpc是基于http2協(xié)議來實現(xiàn)的,http2本身實現(xiàn)流的多路復用。通常來說,內網的兩個節(jié)點使用單連接就可以跑滿網絡帶寬,無性能問題。但在golang里實現(xiàn)的grpc會有各種鎖競爭的問題。

如何優(yōu)化?多開grpc客戶端,規(guī)避鎖競爭的沖突概率。測試下來qps提升很明顯,從8w可以提到20w左右。

可參考以前寫過的grpc性能測試[4]。

問題六:減少協(xié)程數量

有朋友認為等待事件的協(xié)程多了無所謂,只是占內存,協(xié)程拿不到調度,不會對runtime性能產生消耗。這個說法是錯誤的。雖然拿不到調度,看起來只是占內存,但是會對 GC 有很大的開銷。所以,不要開太多的空閑的協(xié)程,比如協(xié)程池開的很大。

在推送的架構里,push-gateway到push-server不僅幾個連接就可以,且?guī)资畟€stream就可以。我們自己實現(xiàn)大量消息在十幾個stream里跑,然后調度通知。在golang grpc streaming的實現(xiàn)里,每個streaming請求都需要一個協(xié)程去等待事件。所以,共享stream通道也能減少協(xié)程的數量。

問題七:GC 問題

對于頻繁創(chuàng)建的結構體采用sync.Pool進行緩存。有些業(yè)務的緩存先前使用list鏈表來存儲,在不斷更新新數據時,會不斷的創(chuàng)建新對象,對 GC 造成影響,所以改用可復用的循環(huán)數組來實現(xiàn)熱緩存。

后記

有坑不怕,填上就可以了。

參考資料

[1]github: https://github.com/rfyiamcool/ccmap/blob/master/syncmap.go

[2]benchmark: https://github.com/rfyiamcool/go-benchmark/tree/master/batch_notify_channel

[3]github: https://github.com/rfyiamcool/go-timewheel

[4]測試: https://github.com/rfyiamcool/grpc_batch_test


本文題目:優(yōu)化 Golang 分布式行情推送的性能瓶頸
網頁URL:http://www.dlmjj.cn/article/cdgddgs.html