<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 4744字,需瀏覽 10分鐘

           ·

          2021-07-07 21:16

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

          對(duì)于訂閱客戶端數(shù)沒有太執(zhí)著量級(jí)的測(cè)試,弱網(wǎng)絡(luò)下單機(jī)8w客戶端是沒問題的。當(dāng)前采用的是kubenetes部署方案,可靈活地?cái)U(kuò)展擴(kuò)容。

          架構(gòu)圖

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

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

          問題一:并發(fā)操作map帶來的鎖競(jìng)爭(zhēng)及時(shí)延

          推送的服務(wù)需要維護(hù)訂閱關(guān)系,一般是用嵌套的map結(jié)構(gòu)來表示,這樣造成map并發(fā)競(jìng)爭(zhēng)下帶來的鎖競(jìng)爭(zhēng)和時(shí)延高的問題。

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

          已經(jīng)根據(jù)業(yè)務(wù)拆分了4個(gè)map,但是該訂閱關(guān)系是嵌套的,直接上鎖會(huì)讓其他協(xié)程都阻塞,阻塞就會(huì)造成時(shí)延高。

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

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

          // xiaorui.cc

          sync.RWMutex
          map[string]map[string]client

          改成這樣

          m *shardMap.shardMap

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

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

          簡(jiǎn)單說,我們?cè)谕扑头?wù)維護(hù)了某個(gè)topic和1w個(gè)客戶端chan的映射,當(dāng)從mq收到該topic消息后,再通知給這1w個(gè)客戶端chan。

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

          下面是我寫的benchmark[2],可以對(duì)比串行和并發(fā)的耗時(shí)對(duì)比。在mac下效果不是太明顯,因?yàn)閙ac cpu頻率較高,在服務(wù)器里效果明顯。

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

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

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

          // xiaorui.cc

          notifiers := []*mapping.StreamNotifier{}
          // conv slice
          for _, notifier := range notifierMap {
              notifiers = append(notifiers, notifier)
          }


          // optimize: direct map struct
          taskChunks := b.splitChunks(notifiers, batchChunkSize)


          // concurrent send chan
          wg := sync.WaitGroup{}
          for _, chunk := range taskChunks {
              chunkCopy := chunk // slice replica
              wg.Add(1)
              b.SubmitBlock(
                  func() {
                      for _, notifier := range chunkCopy {
                          b.directSendMesg(notifier, mesg)
                      }
                      wg.Done()
                  },
              )
          }
          wg.Wait()

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

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

          行情推送里有大量的心跳檢測(cè),及任務(wù)時(shí)間控速,這些都依賴于定時(shí)器。go在1.9之后把單個(gè)timerproc改成多個(gè)timerproc,減少了鎖競(jìng)爭(zhēng),但四叉堆數(shù)據(jù)結(jié)構(gòu)的時(shí)間復(fù)雜度依舊復(fù)雜,高精度引起的樹和鎖的操作也依然頻繁。

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

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

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

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

          // xiaorui.cc

          // 觸發(fā)器的結(jié)構(gòu)
          type StreamNotifier struct {
              Guid  string
              Queue chan interface{}


              closed int32
              ctx    context.Context
              cancel context.CancelFunc
          }


          func (sc *StreamNotifier) IsClosed() bool {
              if sc.ctx.Err() == nil {
                  return false
              }
              return true
          }

          ...

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

          grpc是基于http2協(xié)議來實(shí)現(xiàn)的,http2本身實(shí)現(xiàn)流的多路復(fù)用。通常來說,內(nèi)網(wǎng)的兩個(gè)節(jié)點(diǎn)使用單連接就可以跑滿網(wǎng)絡(luò)帶寬,無性能問題。但在golang里實(shí)現(xiàn)的grpc會(huì)有各種鎖競(jìng)爭(zhēng)的問題。

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

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

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

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

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

          問題七:GC 問題

          對(duì)于頻繁創(chuàng)建的結(jié)構(gòu)體采用sync.Pool進(jìn)行緩存。有些業(yè)務(wù)的緩存先前使用list鏈表來存儲(chǔ),在不斷更新新數(shù)據(jù)時(shí),會(huì)不斷的創(chuàng)建新對(duì)象,對(duì) GC 造成影響,所以改用可復(fù)用的循環(huán)數(shù)組來實(shí)現(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]

          測(cè)試: https://github.com/rfyiamcool/grpc_batch_test



          瀏覽 57
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天综合网7799精品视频 | 亚洲视频久久久 | av乱伦网址 | 国产精品 码一本A片 | A A A片免费看视频 |