goim 架構與定製

tsingson發表於2019-04-20

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站 的彈幕業務解決方案的開源實現, 所以, 業務場景, 想想彈幕

原圖在這裡

original architecture

我重繪了這張圖, 把各網元, 及外部網元關係標示清晰一些

說明: 下圖右側 http client 是 goim push message 介面, 我標註了 backend 只是個人習慣, 事實上這只是個即時訊息傳送介面, 無所謂前後臺

original architecture

注意要點:

  1. comet / job / logic 支援多例項部署, 這是 goim 分散式架構設計的精粹. 同時, push message 訊息釋出介面從 comet 拆分也有一定的考量, 畢竟多數IM 尤其是 bilibili 的業務場景上來說, 傳送量少, 而閱讀量多, 想想彈幕的業務場景就明白了.

  2. goim 採用 bilibili/discovery 實現註冊/服務發現, 從而實現分散式路由與動態排程, 相關細節參看 bilibili/discovery 文件, 以及 Netflix/eureka 原始設計文件

  3. 配置 discovery 時, 注意 region / zone / env 的相互匹配對應關係

  4. 測試部署請注意 redis-server 儘量只要部署一個例項或一個叢集(相當於單例項), kafka / zookeeper 相對簡單, 部署多少都行, 配置對接上就行

2. 架構細節(內部邏輯元件與介面關係)

goim 原始碼不多, 閱讀簡單也算是 golang 語言的特點, 在 goim 尤其如此. ( 推薦用 goland 閱讀程式碼) 下圖中 goim 各網元的內部邏輯元件(邏輯單元), 以及各邏輯介面的相互關係, 可以對照原始碼自行閱讀, 擴充套件

請注意各網元的連線線, 箭頭標示了資料/信令的流向

architecture degail ( original )

3. 如何定製擴充套件

在學習過程中, 網上問到比較多的定製問題有幾個, 分別如下

  1. 離線訊息如何儲存
  2. 使用者如何認證, 或如何與自有業務系統對接
  3. kafka 建議可更換, 比如 nats (我作了這個嘗試)
  4. bilibili/discovery 分離

下面畫出 goim 定製擴充套件, 或優化的一個可行方式

architecture degail ( original )

  1. 在 comet 上定製擴充套件, client 端增加訊息傳送, 可雙向流式傳送/接收即時訊息
  2. 在 logic 上定製, 增加使用者管理介面, 會話管理介面, room 管理介面, 以及
  3. 在 logic 上增加即時訊息儲存或處理介面, 比如離線訊息儲存, 使用者上線後獲取離線訊息(後臺觸發傳送)
  4. 當然了, 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, &params); 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 )

tsingson 寫於中國深圳 小羅號口琴音樂中心, 2019/04/21

相關文章