goim 架構與定製

goim 文章系列(共5篇):html

有個 slack 頻道, 很多朋友在交流 goim , 歡迎加入slack #goimgit

0. 關於 goim 及文章撰寫動機

goim 官網 goim.iogithub

goim 源碼 github.com/Terry-Mao/g…golang


goim 是 很是成功的 IM (Instance Message) 即時消息平臺, 依賴項爲 kafka ( 消息隊列) + zookeeper ( 擴展/均衡 ) + bilibili/discovery( 在 netflix/eureka上擴展的服務註冊與發現, golang 實現)redis

做爲一個曾經的架構師(2005~2014, Utstarcom IPTV/OTT 事業部) 與當前自由職業者(你懂的~~~~), 時常在 Golang 圈轉轉, 有朋友聊到IM 並提到goim, 我做了一些學習與研究json

中國 B站( BiliBili ) 的技術領軍 毛劍 是我神交以久的技術專家, goim 是一個很是成功的架構示例, 其模塊拆分, 接口設計, 技術選型 ,部署方式 以及持續改進演變, 都是一個互聯網商用項目典範.bash

同時, 另外一位技術專家 Xin.zh 的文章 一套高可用實時消息系統實現 給我很大啓發.cookie

在電信/廣電的幾年經歷, 這一次, 閒來無事, 算是滿懷着在巨人肩頭的感謝與敬意, 嘗試寫一些代碼來加深學習.session

感謝兩位技術專家, 感謝開源社區架構

我的在 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

_

_

_

_

關於文章署名與文章轉載的一些小補充

相關文章被轉載在

studygolang.com/ 保留了個人我的署名/我的介紹, 以及 readhub.cn/tech 幾乎對個人幾篇文章都在次日進行了引用推薦, 並對文章引用署名做者爲 掘金 | tsingson

謝謝了

雖然只是一個小小的署名, 但體現了相互的尊重.

文章發表在互聯網, 發佈在我的博客與掘金, 就是爲了分享, 因此也沒有特別加版權著做權這些聲明瞭, 但轉載文章,保留原做者的署名, 是基本的相互尊重吧.............

readhub.cn/tech / studygolang.com/ 點個贊!!

--------------- tsingson 於 2019/06/04 香港銅鑼灣

總之, 歡迎轉載個人原創文章.

能夠的話, 請保留個人原創文章署名, 固然, 不署名, 留空也行, 只要別把誰誰的名字替換掉原做者名稱, 或聲稱爲某某本身的原創文章, 也是挺好的...

就這樣, 祝愉快.

相關文章
相關標籤/搜索