從零開始構建本身的分佈式消息推送系統

前言


你還在羨慕別人成熟的推送系統麼?
你想定製本身的推送系統麼?
你有內網推送的需求而不能使用外網推送產品的困擾麼? java

文章將向你介紹研究分佈式推送的過程與心得(本人嘔心瀝血打造......),但願看完文章的你能有所收穫。mysql

  • 經過這個標題點進來就默認你對websocket有了基本的瞭解,若是你不知道,這裏有個很好的答案websocket是什麼
  • 若是你不想聽我bb,完整項目傳送門(給個star支持一下....3q ^_^),裏面有詳細的項目介紹與搭建過程。
  • websocket協議只是推送的一種實現,固然可使用其餘協議,但願構建的思路能夠啓發到你。

技術選型

在搜索了n屢次websocket這個關鍵詞之後...我選擇用netty這個支持nio的高性能網絡框架做爲推送支持,它幫咱們屏蔽了網絡底層複雜通訊邏輯,提供簡單易用的api。(websocket的netty實現網上一搜一大把)

客戶端的websocket握手請求如:ws://127.0.0.1:9003/websocket?channelId=123456
將客戶端的惟一標識123456與客戶端在咱們netty中抽象出的鏈接對象channel,維護至全局變量中git

private static Map<String,Channel> channels = new ConcurrentHashMap(1000);

推送邏輯爲:根據http請求或者客戶端發過來的websocket消息格式,解析內容,經過客戶端鏈接的標識channelId找到對應客戶端的鏈接channel對象,調用channel.writeAndFlush(new TextWebSocketFrame("須要推送的內容")),完成推送。github

消息體的兩個主要參數爲:web

  • 推送的目標客戶端標識(who)
  • 推送的內容(what)

到此,咱們的推送功能,已基本實現,只是受限於單機各項性能參數,沒有任何的拓展性。redis

初步拆分

咱們但願根據系統的業務職能,作初步的拆分,以便將來根據不一樣業務的負載,作更細粒度的集羣。
  • portal—負責處理http請求
  • websocket—負責處理websocket握手鍊接,接收和推送websocket消息

集羣模式

註冊中心與網關

當咱們考慮把各個業務模塊部署多份時,咱們要面對這些問題:
1.須要對外暴露一個統一的入口來路由到咱們不一樣的集羣服務
2.對於集羣中節點的上下線要作到動態感知

咱們加入這兩個組件spring

  • gateway—網關,統一入口,動態路由(zuul並不支持websocket)
  • eureka—註冊中心,動態的感知服務上下線

在這個階段中,咱們須要解決一個推送業務中比較核心的問題:sql

  • 推送請求的路由
咱們知道:
websockethttp瞬時無狀態的請求響應不同,客戶端在發起 websocket握手成功後,不會立馬斷開,會維持住與服務器的 tcp鏈接,用於全雙工通訊。在這種狀況下,咱們的推送請求,就不能任由 portal服務端負載均衡,路由到各個 websocket服務節點上了。
舉個栗子:
如上圖所示 client1發起 websocket握手時,由網關將握手請求路由給 websocket1節點, client1會經過網關與 websocket1節點維持一個 tcp通道,那麼以 http請求來觸發推送時,必需要把對 client1的推送請求指定路由給 websocket1節點來處理(對於以 websocket消息觸發的推送請求也是如此, client4發起對 client1的推送必須由接收到消息的 websocket2節點轉發給 websocket1節點處理)。

分佈式緩存

此時,咱們須要引入redis來記錄各個websocket節點上所維護的客戶端:數據庫

  • websocket服務節點處理完客戶端的握手請求之後,將節點與客戶端的路由關係保存進redis
  • http觸發推送的流程:portal接收到推送請求時,根據須要推送的客戶端的目的地,從redis中找到客戶端所在的服務器,轉發http請求至客戶端所在服務器的websocket節點,websocket節點發起推送
  • websocket消息觸發推送的流程:websocket節點接收到客戶端websocket消息推送請求時,判斷須要推送的客戶端是否在本節點上,若是是則直接推送,若是不是則轉發給對應的其餘websocket節點發起推送

在我當前的項目中,一些地方使用到了 redis的管道特性,因此這裏 redis不支持 cluster這種分片的集羣部署方式,要想適用分片的集羣方式來提升併發只能使用 codis作代理。。要麼就單個 master

消息中間件

上圖可見:服務間的調用關係很是複雜,系統間耦合很是高,在線上對端口嚴格管控的服務器下部署這樣一套系統是很是痛苦的,咱們考慮引入MQ解耦:當有須要轉發給websocket節點進行推送時,投遞到MQ中對應節點所訂閱的主題就行 api

簡單描述mq的選型問題:目前經常使用的分佈式高可用MQ中間件有: RabbitMQkafkaRocketMQ
RabbitMQ:須要安裝Erlang環境。。我的但願系統部署儘可能簡單,首先就排除(對java有把握一點。。)
kafka:通常用於日誌分析,大數據計算
RocketMQ: 相比於kafka,可靠,實時,易用等特性更適用與業務系統交互的場景中,註冊中心nameSer也比kafka的zk要輕便(就他了) 詳細對比傳送門

配置中心

到此,功能已基本完成,只是咱們如今每部署一個服務節點都須要配置相同的註冊中心地址、redis地址、mq地址,當某個中間件地址變更時,整個服務的全部節點都須要相適配,爲避免這種繁瑣而重複的配置,咱們考慮引入配置中心:

  • 配置中心存放公共的中間件地址
  • 各服務節點從配置中心去獲取所需的中間件地址
配置中心選型:
springcloud config:須要依賴git
apollo:須要依賴mysql
nacos:服務自帶數據庫,沒有任何依賴,安裝使用簡單便捷。(就他了)

如今的完整配置如圖:

補充

咱們的分佈式消息推送系統已經完成了O(∩_∩)O,不過這裏存在一個運行上的小問題:

如今從gateway網關路由websocket握手請求到各個websocket服務節點的權重都是相同的,也就是說理論上咱們但願各個websocket節點所維持的客戶端鏈接數量是大體相同的,可是當咱們服務部署運行一段時間後,發現各個websocket節點的負載較高,咱們但願增長(或者服務節點重啓)websocket服務節點的數量。可是增長(重啓)完websocket節點之後,gateway路由到各個websocket節點的權重依然相同,其實咱們但願gateway網關將websocket握手請求能優先能路由給新部署上來的websocket服務節點,即:

  • 網關動態權重路由(將客戶端websocket握手鍊接優先分配給較少鏈接數量的websocket節點)
具體實如今我 完整項目傳送門中的 task模塊

第一次寫文章。。但願你們多多支持。。。 認知有限。。若有描述錯誤的地方還請指出。。。有問題提交至項目中的issue

相關文章
相關標籤/搜索