跟着源碼學IM(十):基於Netty,搭建高性能IM集羣

本文原題“搭建高性能的IM系統”,作者“劉蒞”,內容有修訂和改動。爲了尊重原創,如需轉載,請聯繫作者獲得授權。

1、引言

相信很多朋友對微信、QQ等聊天軟件的實現原理都非常感興趣,筆者同樣對這些軟件有着深厚的興趣。而且筆者在公司也是做IM的,公司的IM每天承載着上億條消息的發送!

正好有這樣的技術資源和條件,所以前段時間,筆者利用業餘時間,基於Netty開發了一套基本功能比較完善的IM系統。該系統支持私聊、羣聊、會話管理、心跳檢測,支持服務註冊、負載均衡,支持任意節點水平擴容。

這段時間,網上的一些讀者,也希望筆者分享一些Netty或者IM相關的知識,所以今天筆者把開發的這套IM系統分享給大家。

本文將根據筆者這次的業餘技術實踐,爲你講述如何基於Netty+Zk+Redis來搭建一套高性能IM集羣,包括本次實現IM集羣的技術原理和實例代碼,希望能帶給你啓發。

2、本文源碼

主地址:https://github.com/nicoliuli/chat

備地址:https://github.com/52im/chat

源碼的目錄結構,如下圖所示:

3、知識準備

* 重要提示:本文不是一篇即時通訊理論文章,文章內容來自代碼實戰,如果你對即時通訊(IM)技術理論瞭解的太少,建議先詳細閱讀:《新手入門一篇就夠:從零開發移動端IM》。

可能有人不知道 Netty 是什麼,這裏簡單介紹下:

Netty 是一個 Java 開源框架。Netty 提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。

也就是說,Netty 是一個基於 NIO 的客戶、服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。

Netty 相當簡化和流線化了網絡應用的編程開發過程,例如,TCP 和 UDP 的 Socket 服務開發。

以下是有關Netty的入門文章:

Netty源碼和API的在線查閱地址:

4、系統架構

系統的架構如上圖所示:整個系統是一個C/S系統,客戶端沒有做複雜的圖形化界面而是用Java終端開發的(黑窗口),服務端IM實例是Netty寫的socket服務。

ZK作爲服務註冊中心,Redis用來做分佈式會話的緩存,並保存用戶信息和輕量級的消息隊列。

對於整個系統架構中各部分的工作原理,我們將在接下來的各章節中一一介紹。

5、服務端的工作原理

在上述架構中:NettyServer啓動,每啓動一臺Server節點,都會把自身的節點信息,如:ip、port等信息註冊到ZK上(臨時節點)。

正如上節架構圖上啓動了兩臺NettyServer,所以ZK上會保存兩個Server的信息。

同時ZK將監聽每臺Server節點,如果Server宕機ZK就會刪除當前機器所註冊的信息(把臨時節點刪除),這樣就完成了簡單的服務註冊的功能。

6、客戶端的工作原理

Client啓動時,會先從ZK上隨機選擇一個可用的NettyServer(隨機表示可以實現負載均衡),拿到NettyServer的信息(IP和port)後與NettyServer建立鏈接。

鏈接建立起來後,NettyServer端會生成一個Session(即會話),用來把當前客戶端的Channel等信息組裝成一個Session對象,保存在一個SessionMap裏,同時也會把這個Session保存在Redis中。

這個會話特別重要,通過會話,我們能獲取當前Client和NettyServer的Channel等信息。

7、Session的作用

我們啓動多個Client,由於每個Client啓動,都會先從ZK上隨機獲取NettyServer的的信息,所以如果啓動多個Client,就會連接到不同的NettyServer上。

熟悉Netty的朋友都知道,Client與Server建立接連後會產生一個Channel,通過Channel,Client和Server才能進行正常的網絡數據傳輸。

如果Client1和Client2連接在同一個Server上:那麼Server通過SessionMap分別拿到Client1和Client2的會話,會話中包含Channel信息,有了兩個Client的Channel,Client1和Client2便可完成消息通信。

如果Client1和Client2連接到不同的NettyServer上:Client1和Client2要進行通信,該怎麼辦?這個問題放在後面解答。

8、高效的數據傳輸

無論是IM系統,還是分佈式的RPC框架,高效的網絡數據傳輸,無疑會極大的提升系統的性能。

數據通過網絡傳輸時,一般把對象通序列化成二進制字節流數組,然後將數據通過socket傳給對方服務器,對方服務器拿到二進制字節流後再反序列化成對象,達到遠程通信的目的。

在Java領域,Java序列化對象的方式有嚴重的性能問題,業界常用谷歌的protobuf來實現序列化反序列化(見《Protobuf通信協議詳解:代碼演示、詳細原理介紹等)。

protobuf支持不同的編程語言,可以實現跨語言的系統調用,並且有着極高的序列化反序列化性能,本系統也採用protobuf來做數據的序列化。

關於Protobuf的基本認之,下面這幾篇可以深入讀一讀:

  1. 強列建議將Protobuf作爲你的即時通訊應用數據傳輸格式
  2. 全方位評測:Protobuf性能到底有沒有比JSON快5倍?
  3. 金蝶隨手記團隊分享:還在用JSON? Protobuf讓數據傳輸更省更快(原理篇)

另外:一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》一文中,“3、協議設計”這一節有關於protobuf在IM中的實戰設計和使用,可以一併學習一下。

9、聊天協議定義

我們在使用各種聊天APP時,會發各種各樣的消息,每種消息都會對應不同的消息格式(即“聊天協議”)。

聊天協議中主要包含幾種重要的信息:

  • 1)消息類型;
  • 2)發送時間;
  • 3)消息的收發人;
  • 4)聊天類型(羣聊或私聊)。

我的這套IM系統中,聊天協議定義如下:

syntax = "proto3";

option java_package = "model.chat";

option java_outer_classname = "RpcMsg";

message Msg{

    string msg_id = 1;

    int64 from_uid = 2;

    int64 to_uid = 3;

    int32 format = 4;

    int32 msg_type = 5;

    int32 chat_type = 6;

    int64 timestamp = 7;

    string body = 8;

    repeated int64 to_uid_list = 9;

}

如上面的protobuf代碼,字段的具體含義如下:

  • 1)msg_id:表示消息的唯一id,可以用UUID表示;
  • 2)from_uid:消息發送者的uid;
  • 3)to_uid:消息接收者的uid;
  • 4)format:消息格式,我們使用各種聊天軟件時,會發送文字消息,語音消息,圖片消息等等等等,每種消息有不同的消息格式,我們用format來表示(由於本系統是java終端,format字段沒有太大含義,可有可無);
  • 5)msg_type:消息類型,比如登錄消息、聊天消息、ack消息、ping、pong消息;
  • 6)chat_type:聊天類型,如羣聊、私聊;
  • 7)timestamp:發送消息的時間戳;
  • 8)body:消息的具體內容,載體;
  • 9)to_uid_list:這個字段用戶羣聊消息提高羣聊消息的性能,具體作用會在羣聊原理部分詳細解釋。

10、私聊消息發送原理

Client1給Client2發消息時,我們需要構建上節中的消息體。

具體就是:from_uid是Client1的uid、to_uid是Client2的uid。

NettyServer收到消息後的處理邏輯是:

  • 1)解析到to_uid字段;
  • 2)從SessionMap或者Redis中保存的Session集合中獲取to_uid即Client2的Session;
  • 3)從Session中取出Client2的Channel;
  • 4)然後將消息通過Client2的Channel發給Client2。

11、羣聊消息發送原理

羣聊消息的分發通常有兩種技術實現方式,我們一一來看看。

方式一:假設一個羣有100人,如果Client1給一個羣的所有人發消息,其實相當於Client1分別給其餘99人分別發一條消息。我們可以直接在Client端,通過循環,分別給羣裏的99人發消息即可,相當於Client發送給NettyServer發送了99次相同的消息(除了to_uid不同)。

上述方案有很嚴重的性能問題:Client1通過循環99次,分別把消息發給NettyServer,NettyServer收到這99條消息後,分別將消息發給羣內其餘的用戶。先拋開移動端的特殊性(比如循環還沒完成手機就有可能退到後臺被系統掛起),顯然Client1到NettyServer的99次循環存在明顯不合理地方。

方式二:上節的消息體中to_uid_list字段就是爲了解決這個方式一的性能問題的。Client1把羣內其餘99個Client的uid保存在to_uid_list中,然後NettyServer只發一條消息,NettyServer收到這一條消息後,通過to_uid_list字段解析羣內其餘99的Client的uid,再通過循環把消息分別發送給羣內其餘的Client。

可以看到:方式二的羣聊時,Client1與NettyServer只進行1次消息傳輸,相比於方式一,效率提高了50%。

11、技術關鍵點1:客戶端分別連接在不同IM實例時如何通信?

針對本文中的架構,如果多個Client分別連接在不同的Server上,Client之間應該如何通信呢?

爲了回答這個問題,我們首先要明白Session的作用。

我們做過JavaWeb開發的朋友都知道,Session用來保存用戶的登錄信息。

在IM系統中也是如此:Session中保存用戶的Channel信息。當Client與Server建立鏈接成功後,會產生一個Channel,Client和Server是通過Channel,實現數據傳輸。當兩端鏈接建立起來後,Server會構建出一個Session對象,保存uid和Channel等信息,並把這個Session保存在一個SessionMap裏(NettyServer的內存裏),uid爲key,我們可以通過uid就可以找到這個uid對應的Session。

但只有SessionMap還不夠:我們需要利用Redis,它的作用是保存整個NettyServer集羣全部鏈接成功的用戶,這也是一種Session,但這種Session沒有保存uid和Channel的對應關係,而是保存Client鏈接到NettyServer的信息,如Client鏈接到的這個NettyServer的ip、port等。通過uid,我們同樣可以從Redis中拿到當前Client鏈接到的NettyServer的信息。正是有了這個信息,我們才能做到,NettyServer集羣任意節點水平擴容。

當用戶量少的時候:我們只需要一臺NettyServer節點便可以扛住流量,所有的Client鏈接到同一個NettyServer上,並在NettyServer的SessionMap中保存每個Client的會話。Client1與Client2通信時,Client1把消息發給NettyServer,NettyServer從SessionMap中取出Client2的Session和Channel,將消息發給Client2。

隨着用戶量不斷增多:一臺NettyServer不夠,我們增加了幾臺NettyServer,這時Client1鏈接到NettyServer1上並在SessionMap和Redis中保存了會話和Client1的鏈接信息,Client2鏈接到NettyServer2上並在SessionMap和Redis中保存了會話和Client2的鏈接信息。Client1給Client2發消息時,通過NettyServer1的SessionMap找不到Client2的會話,消息無法發送,於是便從Redis中獲取Client2鏈接在哪臺NettyServer上。獲取到Client2所鏈接的NettyServer信息後,我們可以把消息轉發給NettyServer2,NettyServer2收到消息後,從NettyServer2的SessionMap中獲取Client2的Session和Channel,然後將消息發送給Client2。

那麼:NettyServer1的消息如何轉發給NettyServer2呢?答案是通過消息隊列,如Redis中的list數據結構。每臺NettyServer啓動後都需要監聽一個自己的Redis中的消息隊列,這個隊列用戶接收其他NettyServer轉發給當前NettyServer的消息。

* Jack Jiang點評:上述集羣方案中,Redis既作爲在線用戶列表存儲中心,又作爲集羣中不同IM長連接實例的消息中轉服務(此時的Redis作用相當於MQ),那Redis不就成爲了整個分佈式集羣的單點瓶頸了嗎?

12、技術關鍵點2:鏈接斷開,如何處理?

如果Client與NettyServer,由於某種原因(客戶端退出、服務端重啓、網絡因素等)斷開鏈接,我們必須要從SessionMap刪除會話和Redis中保留的數據。

如果不清除這兩類數據的話,很有可能Client1發送給Client2的消息,可能會發給其他用戶,或者就算Client2處於登錄狀態,Client2也收到不到消息。

我們可以在Netty框架中的channelInactive方法裏,處理鏈接斷開後的會話清除操作。

13、技術關鍵點3:ping、pong的作用

當Client與NettyServer建立鏈接後,由於雙端網絡較差,Client與NettyServer斷開鏈接後,如果NettyServer沒有感知到,也就沒有清除SessionMap和Redis中的數據,這將會造成嚴重的問題(對於服務端來說,這個Client的會話實際處於“假死”狀態,消息是無法實時發送過去的)。

此時就需要一種ping/pong機制(也就是心跳機制啦)。

實現原理就是:通過定時任務,Client每隔一段時間給NettyServer發一個ping消息,NettyServer收到ping消息後給客戶端回覆一個pong消息,確保客戶端和服務端能一直保持鏈接狀態。如果Client與NettyServer斷連了,NettyServer可以立即發現並清空會話數據。Netty中的我們可以在Pipeline中添加IdleStateHandler,可達到這樣的目的。

如果你不明白心跳的作用,務必讀以下文章:

  1. 爲何基於TCP協議的移動端IM仍然需要心跳保活機制?
  2. 一文讀懂即時通訊應用中的網絡心跳包機制:作用、原理、實現思路等

也可以學習一下主流IM的心跳邏輯:

  1. 微信團隊原創分享:Android版微信後臺保活實戰分享(進程保活篇)
  2. 微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)
  3. 移動端IM實踐:實現Android版微信的智能心跳機制
  4. 移動端IM實踐:WhatsApp、Line、微信的心跳策略分析

如果覺得理論不夠直觀,下面的代碼實例可以直觀地進行學習:

  1. 正確理解IM長連接的心跳及重連機制,並動手實現(有完整IM源碼)
  2. 一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)
  3. 自已開發IM有那麼難嗎?手把手教你自擼一個Andriod版簡易IM (有源碼)
  4. 手把手教你用Netty實現網絡通信程序的心跳機制、斷線重連機制

其實,心跳算法的實際效果,還是有一些邏輯技巧的,以下兩篇建議必讀:

  1. Web端即時通訊實踐乾貨:如何讓你的WebSocket斷網重連更快速?
  2. 融雲技術分享:融雲安卓端IM產品的網絡鏈路保活技術實踐

14、技術關鍵點4:爲Server和Client添加Hook

如果NettyServer重啓了或者進程被kill掉,我們需要清除當前節點的SessionMap(其實不用清理SessionMap,數據在內存裏重啓會自動刪除的)和Redis保存的Client的鏈接信息。

我們需要遍歷SessionMap找出所有的uid,然後一一清除Redis的數據,然後優雅退出。此時,我們就需要爲我們的NettyServer添加一個Hook,來做數據清理。

15、技術關鍵點5:對方不在線該如何處理消息?

Client1給對方發消息,我們通過SessionMap或Redis拿不到對方的會話數據,這就表明對方不在線。

此時:我們需要把消息存儲在離線消息表中,當對方下次登錄時,NettyServer查離線消息表,把消息發給登錄用戶(最好是批量發送,提高性能)。

IM中的離線消息處理,也不是個簡單的技術點,有興趣可以深入學習一下:

  1. IM消息送達保證機制實現(二):保證離線消息的可靠投遞
  2. 阿里IM技術分享(六):閒魚億級IM消息系統的離線推送到達率優化
  3. IM開發乾貨分享:我是如何解決大量離線消息導致客戶端卡頓的
  4. IM開發乾貨分享:如何優雅的實現大量離線消息的可靠投遞
  5. 喜馬拉雅億級用戶量的離線消息推送系統架構設計實踐

16、寫在最後

代碼寫成這樣,也算是了確了自已手擼IM的心願。唯一遺憾的是,時間比較緊張,還沒來得及實現消息ack機制,保證消息一定會送達,這個筆者以後會補充上去的。

好了,這就是我開發的這個簡易的聊天系統,麻雀雖小,五臟俱全,大家有什麼不明白的地方,可以直接在下方留言,筆者會一一回復的,謝謝大家。

18、參考資料

[1] 新手入門:目前爲止最透徹的的Netty高性能原理和框架架構解析

[2] 寫給初學者:Java高性能NIO框架Netty的學習方法和進階策略

[3] 史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!

[4] Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!

[5] 史上最通俗Netty框架入門長文:基本介紹、環境搭建、動手實戰

[6] 理論聯繫實際:一套典型的IM通信協議設計詳解

[7] 淺談IM系統的架構設計

[8] 簡述移動端IM開發的那些坑:架構設計、通信協議和客戶端

[9] 一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)

[10] 一套原創分佈式即時通訊(IM)系統理論架構方案

[11]  一套高可用、易伸縮、高併發的IM羣聊、單聊架構方案設計實踐

[12] 一套億級用戶的IM架構技術乾貨(上篇):整體架構、服務拆分等

[13] 一套億級用戶的IM架構技術乾貨(下篇):可靠性、有序性、弱網優化等

[14] 從新手到專家:如何設計一套億級消息量的分佈式IM系統

[15] 基於實踐:一套百萬消息量小規模IM系統技術要點總結

學習交流:

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM

- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK 

本文已同步發佈於:http://www.52im.net/thread-3816-1-1.html )

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章