goim 文章系列:
0. 關於 goim 及文章撰寫動機
goim 官網 goim.io
goim 原始碼 github.com/Terry-Mao/g…
goim 是 非常成功的 IM (Instance Message) 即時訊息平臺, 依賴項為 kafka ( 訊息佇列) + zookeeper ( 擴充套件/均衡 ) + bilibili/discovery( 在 netflix/eureka上擴充套件的服務註冊與發現, golang 實現)
作為一個曾經的架構師(2005~2014, Utstarcom IPTV/OTT 事業部) 與當前自由職業者(你懂的~~~~), 時常在 Golang 圈轉轉, 有朋友聊到IM 並提到goim, 我作了一些學習與研究
中國 B站( BiliBili ) 的技術領軍 毛劍 是我神交以久的技術專家, goim 是一個非常成功的架構示例, 其模組拆分, 介面設計, 技術選型 ,部署方式 以及持續改進演變, 都是一個網際網路商用專案典範.
同時, 另一位技術專家 Xin.zh 的文章 一套高可用實時訊息系統實現 給我很大啟發.
在電信/廣電的幾年經歷, 這一次, 閒來無事, 算是滿懷著在巨人肩頭的感謝與敬意, 嘗試寫一些程式碼來加深學習.
感謝兩位技術專家, 感謝開源社群
個人在 Utstarcom 以業務平臺架構師/解決方案工程師/ IPTV播控產品線 release manager 角色折騰過比較長一些時間, 除了技術方案的原型程式碼撰寫與現場應急幫忙修bug 以外, 甚少參撰寫商用專案中的程式碼, 這次寫寫程式碼也是有趣的練習 :P
歡迎指點/交流....
1. goim 業務場景與架構設計
goim 是 bilibili, 簡稱B站 的彈幕業務解決方案的開源實現, 所以, 業務場景, 想想彈幕
原圖在這裡
我重繪了這張圖, 把各網元, 及外部網元關係標示清晰一些
說明: 下圖右側 http client 是 goim push message 介面, 我標註了 backend 只是個人習慣, 事實上這只是個即時訊息傳送介面, 無所謂前後臺
注意要點:
comet / job / logic 支援多例項部署, 這是 goim 分散式架構設計的精粹. 同時, push message 訊息釋出介面從 comet 拆分也有一定的考量, 畢竟多數IM 尤其是 bilibili 的業務場景上來說, 傳送量少, 而閱讀量多, 想想彈幕的業務場景就明白了.
goim 採用 bilibili/discovery 實現註冊/服務發現, 從而實現分散式路由與動態排程, 相關細節參看 bilibili/discovery 文件, 以及 Netflix/eureka 原始設計文件
配置 discovery 時, 注意 region / zone / env 的相互匹配對應關係
測試部署請注意 redis-server 儘量只要部署一個例項或一個叢集(相當於單例項), kafka / zookeeper 相對簡單, 部署多少都行, 配置對接上就行
2. 架構細節(內部邏輯元件與介面關係)
goim 原始碼不多, 閱讀簡單也算是 golang 語言的特點, 在 goim 尤其如此. ( 推薦用 goland 閱讀程式碼) 下圖中 goim 各網元的內部邏輯元件(邏輯單元), 以及各邏輯介面的相互關係, 可以對照原始碼自行閱讀, 擴充套件
請注意各網元的連線線, 箭頭標示了資料/信令的流向
3. 如何定製擴充套件
在學習過程中, 網上問到比較多的定製問題有幾個, 分別如下
- 離線訊息如何儲存
- 使用者如何認證, 或如何與自有業務系統對接
- kafka 建議可更換, 比如 nats (我作了這個嘗試)
- bilibili/discovery 分離
下面畫出 goim 定製擴充套件, 或優化的一個可行方式
- 在 comet 上定製擴充套件, client 端增加訊息傳送, 可雙向流式傳送/接收即時訊息
- 在 logic 上定製, 增加使用者管理介面, 會話管理介面, room 管理介面, 以及
- 在 logic 上增加即時訊息儲存或處理介面, 比如離線訊息儲存, 使用者上線後獲取離線訊息(後臺觸發傳送)
- 當然了, logic 上原有的 http client 傳送介面保留
0. 請注意
下面的原始碼標記出處在 github.com/Terry-Mao/g…
與我的 repo github.com/tsingson/go… 並不相同!!!
我 fork 的程式碼庫中, 訊息佇列抽象成為golang 的 interface , 並且 discovery 正在抽離處理中
1. 即時訊息的儲存鉤子
原始碼在檔案 /internal/logic/dao/kafka.go 中
// PushMsg push a message to databus.
func (d *Dao) PushMsg(c context.Context, op int32, server string, keys []string, msg []byte) (err error) {
pushMsg := &pb.PushMsg{
Type: pb.PushMsg_PUSH,
Operation: op,
Server: server,
Keys: keys,
Msg: msg,
}
//
// 即時訊息儲存擴充套件 HOOKS:
// 在這裡增加即時訊息儲存擴充套件
// 如果需要只儲存離線訊息, 可以先檢查當前使用者是否線上, 依據使用者線上情況處理儲存
//
b, err := proto.Marshal(pushMsg)
if err != nil {
return
}
m := &sarama.ProducerMessage{
Key: sarama.StringEncoder(keys[0]),
Topic: d.c.Kafka.Topic,
Value: sarama.ByteEncoder(b),
}
if _, _, err = d.kafkaPub.SendMessage(m); err != nil {
log.Errorf("PushMsg.send(push pushMsg:%v) error(%v)", pushMsg, err)
}
return
}
複製程式碼
2. 使用者管理與會話管理
原始碼在 /internal/logic/conn.go
// Connect connected a conn.
func (l *Logic) Connect(c context.Context, server, cookie string, token []byte) (mid int64, key, roomID string, accepts []int32, hb int64, err error) {
var params struct {
Mid int64 `json:"mid"`
Key string `json:"key"`
RoomID string `json:"room_id"`
Platform string `json:"platform"`
Accepts []int32 `json:"accepts"`
}
if err = json.Unmarshal(token, ¶ms); err != nil {
log.Errorf("json.Unmarshal(%s) error(%v)", token, err)
return
}
mid = params.Mid
roomID = params.RoomID
accepts = params.Accepts
hb = int64(l.c.Node.Heartbeat) * int64(l.c.Node.HeartbeatMax)
//
// 使用者管理 HOOKS
// 這裡增加使用者管理邏輯程式碼, 比如:
// 1. 呼叫使用者管理模組( 比如 UMS) 檢查 mid ( 會員ID / 使用者 ID ) 是否存在
// 2. 檢查使用者與 room 的許可權關係
//
// 補充: 一般來說, goim 就作為一個即時訊息服務, 使用者註冊/使用者認證等業務應該由 goim 以外的網元或子系統完成
// 這裡的 HOOKS 只要提供一個與使用者管理子系統/會話管理子系統的相應介面呼叫就可以了
//
// 會話管理 HOOKS
// key 是會話ID ( session ID) , 在這裡增加會話管理邏輯程式碼, 比如:
// 1. 檢查會話 ID 是否合法
// 2. 如果不合法, 為授權使用者建立會活ID
//
// 下面這個 if 程式碼段, 是一個簡化掉的例子:
if key = params.Key; key == "" {
keyUuid, _ := uuid.NewV4()
key = keyUuid.String()
}
// 這裡是儲存使用者會話
if err = l.dao.AddMapping(c, mid, key, server); err != nil {
log.Errorf("l.dao.AddMapping(%d,%s,%s) error(%v)", mid, key, server, err)
return
}
//
log.Infof("conn connected key:%s server:%s mid:%d token:%s", key, server, mid, token)
return
}
複製程式碼
4. 我的定製擴充套件
由於學習目的, 及深入閱讀原始碼的需求, 簡化了 kafka / zk 的複雜部署引數配置與 jvm 依賴, 我 fork 了 goim 並修改為 nats + liftbridge, 由 nats 實現 簡化掉 kafka 佇列功能 + zookeeper , 由 liftbridge 實現 nats 訊息的持久化
原始碼見這裡 github.com/tsingson/go… 上作一些擴充套件學習
關於我
網名 tsingson (三明智, 江湖人稱3爺)
原 ustarcom IPTV/OTT 事業部播控產品線技術架構溼/解決方案工程溼角色(8年), 自由職業者,
喜歡音樂(口琴,是第三/四/五屆廣東國際口琴嘉年華的主策劃人之一), 攝影與越野,
喜歡 golang 語言 (商用專案中主要用 postgres + golang )