這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Goim 是毛劍同學寫的 IM 服務,純 Golang 實現,目前應用在 Bilibili 產品線上。最近寫了兩個項目反垃圾和廣告系統,都是內部服務,而長串連 IM 類的對我而言非常陌生。業餘時間研究 goim 代碼,頗受啟發,比如分層設計,資料合併,定時器最佳化,對象最佳化和 RPC 實現最佳化,基本上常用的組件都被 goim 重寫或改造。做個筆記,先分析業務代碼,最佳化代碼放到後幾篇,和大家分享,輕拍,輕虐...
Comet 角色定位
GOIM整體架構
在整個架構中,系統被分成 Comet, Logic, Job, Router 四大模組,各個模組通過 RPC 通訊,參考官方中文文檔,Comet 程式是串連層,暴露給公網,所有的業務處理推給 Logic 模組,通過 RPC 通訊。這樣設計的好處在於,長串連邏輯很少變動,穩定的保持公網串連,而後端 Logic, Router 模組經常變動,重啟不會影響串連層。
幾個重要的結構體
做為典型代碼即注釋的開源項目,goim 基本無太多閱讀障礙,幾個邏輯點梳理下很快就會明白。
Bucket: 每個 Comet 程式擁有若干個 Bucket, 可以理解為 Session Management, 儲存著當前 Comet 服務於哪些 Room 和 Channel. 長串連具體分布在哪個 Bucket 上呢?根據 SubKey
一致性 Hash
來選擇。
Room: 可以理解為房間,群組或是一個 Group. 這個房間內維護 N 個 Channel, 即長串連使用者。在該 Room 內廣播訊息,會發送給房間內的所有 Channel.
Channel: 維護一個長串連使用者,只能對應一個 Room. 推送的訊息可以在 Room 內廣播,也可以推送到指定的 Channel.
Proto: 訊息結構體,存放版本號碼,操作類型,訊息序號和訊息體。
多協議支援
Goim 支援 Tcp, Http, WebSocket, TLS WebSocket. 非常強大,底層原理一樣,下面的分析都是基於 Tcp 協議。
Bucket
先來看看結構體的定義
Bucket結構體
定義很明了,維護當前訊息通道和房間的資訊,方法也很簡單,加減 Channel 和 Room. 一個 Comet Server 預設開啟 1024 Bucket, 這樣做的好處是減少鎖 ( Bucket.cLock ) 爭用,在大並發業務上尤其明顯。
Room
Room結構體
Room 結構體稍顯複雜一些,不但要維護所屬的訊息通道 Channel, 還要訊息廣播的合并寫,即 Batch Write, 如果不合并寫,每來一個小的訊息都通過長串連寫出去,系統 Syscall 調用的開銷會非常大,Pprof 的時候會看到網路 Syscall 是大頭。
訊息廣播
Logic Server 通過 RPC 調用,將廣播的訊息發給 Room.Push, 資料會被暫存在 vers, ops, msgs 裡,每個 Room 在初始化時會開啟一個 groutine 用來處理暫存的訊息,達到 Batch Num 數量或是延遲一定時間後,將訊息批量 Push 到 Channel 訊息通道。
Channel
Channel結構體
總是覺得起名叫 Session 更直觀,並且不和語言層面的 "channel" 衝突。Writer/Reader 就是對網路 Conn 的封裝,SvrProto 是一個 Ring Buffer,儲存 Room 廣播或是直接發送過來的訊息體。
訊息流程轉
這裡只分析 Comet 代碼,所以訊息產生暫時不提
1. Client 串連到 Comet Server, 握手認證
2. 建立當前長串連的 Channel, 由於 Comet 服務不處理商務邏輯,需要 RPC 去 Logic Server 擷取該 Channel 的訂閱資訊。同時 Channel 開啟一個 dispatchTCP groutine, 阻塞等待 Ring Buffer 資料可用,發送到 Client。
3. Logic 服務通過 RPC, 將訊息寫到 Room (廣播)或是直接寫到指定 Channel (單播)。注意這裡,廣播是有寫合并 BatchWrite,
而單播沒有,訊息產生後立刻發送。
4. Room 裡的廣播訊息到達一定數量 Batch Num, 或是延遲等待一定時間後,將訊息寫到 Channel Ring Buffer。
小結
Goim 分層很合理詳情,上面只是對 Comet Server 進行業務層面的解讀。還有很多效能最佳化上的,比如 bytes.Pool, time.Timer, RPC 等最佳化留到最後慢慢分享吧。
大家玩的開心!
補充-來自毛劍
1. bucket按照key的cityhash求餘命中的,沒有用一致性hash,因為這裡不涉及遷移
2. 私信發送其實也有合并的,和room合并不同的是,在ringbuffer取訊息饑餓時候才會真正flush
3. 還有一個最佳化可以改進,因為room有個特點大家訊息可能都一樣,所以在room提前合并成位元組buffer,然後廣播所有人,避免每個人都序列化一次,然後利用gc來處理這個buffer的釋放,這樣可以節省大量cpu,目前這個最佳化還沒做