[開源] Golang 實現的分散式 WebSocket 微服務

woodylan發表於2020-01-15

使用場景

在實現業務的時候,我們常常有些需求需要系統主動傳送訊息給客戶端,方案有輪詢和長連線,但輪詢需要不斷的建立銷燬http連線,對客戶端、對伺服器來說都挺消耗資源的,訊息推送也不夠實時。這裡我們選擇了WebSocket長連線的方案。

有大量的專案需要服務端主動向客戶端推送訊息,為了減少重複開發,我們做成了微服務。

使用於伺服器需要主動向客戶端推送訊息、客戶端需要實時獲取訊息的請求。例如聊天、廣播訊息、多人遊戲訊息推送、任務執行結果推送等方面。

使用流程

用Websocket客戶端連線本服務,服務端會返回客戶端一個唯一的client id,透過這個client id可以知道是哪個連線,客戶端拿到這個id之後上報到服務端,服務端根據業務需求可以給這個長連線傳送指定資訊,或者繫結到分組。

分散式方案

維持大量的長連線對單臺伺服器的壓力也挺大的,這裡也就要求該服務需要可以擴容,也就是分散式地擴充套件。分散式對於可儲存的公共資源有一套完整的解決方案,但對於WebSocket來說,操作物件就是每一個連線,它是維持在每一個程式中的。每一個連線不能儲存起來共享、不能在不同的程式之間共享。所以我能想到的方案是不同程式之間進行通訊。

那麼,怎樣知道某個連線在哪個應用呢?答案是透過client id去判斷。那麼透過client id又是如何知道的呢?有以下幾種方案:

  1. 一致性hash演算法

    一致性hash演算法是將整個雜湊值空間組織成一個虛擬的圓環,在redis叢集中雜湊函式的值空間為0-2^32-1(32位無符號整型)。把伺服器的IP或主機名作為關鍵字,透過雜湊函式計算出相應的值,對應到這個虛擬的圓環空間。我們再透過雜湊函式計算key的值,得到一個在圓環空間的位置,按順時針方向找到的第一個節點就是存放該key資料的伺服器節點。

    在沒有節點的增減的時候,可以滿足我們的需求,但如果此時一個節點掛掉了或者新增一個機器怎麼辦?節點掛點之後,會在圓環上刪除節點,增加節點則反之。這時候按順時針方向找的資料就不準確,在某些業務上來說可以接受,但在WebSocket微服務上來說,影響範圍內的連線會斷掉,如果要求沒那麼高,客戶端再進行重連也可以。

  2. hash slot(雜湊槽)

    伺服器的IP或者主機名作為key,對每個key進行計算CRC16值,然後對16384進行取模,得出一個對應key的hash slot。

    HASH_SLOT = CRC16(key) mod 16384

    我們根據節點的數量,給每個節點劃分範圍,這個範圍是0-16384。hash slot的重點就在這個虛擬表,key對應的hash slot是永不變的,增減節點就是維護這張虛擬表。

以上兩種方案都可以實現需求,但一致性hash演算法的方案會使部分key找到的節點不準確;hash slot的方案需要維護一張虛擬表,在實現起來需要有一個功能去判斷伺服器是否掛了,然後修改這張虛擬表,新增節點也一樣,在實現起來會遇到很多問題。

然後我採取的方案是,每個連線都儲存在本應用,然後用redis的key value記錄每個連線client id對應的伺服器IP和埠。對指定client id進行操作時,去redis找出響應的ip和埠,判斷是否為本機,不是本機的話進行RPC通訊告訴相應的程式。長連線的連線資料不可遷移,程式掛掉了相應的連線也就掛了,在該程式上的連線也就斷開了,這時重連的話會找到另一個可用的程式。

Golang實現的分散式WebSocket微服務

簡介

本系統基於Golang、Redis、RabbitMQ、RPC實現分散式WebSocket微服務,也可以單機部署,單機部署不需要Redis、RabbitMQ和RPC。分散式部署可以支援nginx負責均衡、水平擴容部署,程式之間使用RabbitMQ廣播、RPC通訊。

基本流程為:用ws協議連線本服務,得到一個clientId,由客戶端上報這個clinetId給服務端,服務端拿到這個clientId之後,可以給這個客戶端傳送資訊,繫結這個客戶端都分組,給分組傳送訊息。

目前實現的功能有,給指定客戶端傳送訊息、繫結客戶端到分組、給分組裡的客戶端批次傳送訊息。適用於長連線的大部分場景,分組可以理解為聊天室,繫結客戶端到分組相當於把客戶端新增到聊天室,給分組傳送資訊相當於給聊天室的每個人傳送訊息。

架構圖

單機服務
WebSocket單機服務架構圖

分散式

WebSocket分散式服務架構圖

時序圖

單發訊息

  1. 客戶端傳送連線請求,連線請求透過nginx負載均衡找到一臺ws伺服器;
  2. ws伺服器響應連線請求,返回client id,記錄到redis,保持長連線;
  3. 客戶端拿到client id之後,交給業務系統;
  4. 業務系統拿到client id之後,透過http傳送相關訊息,經過nginx負載分配到一臺ws伺服器;
  5. 這臺ws伺服器拿到clinet id和訊息,去redis查詢到IP地址和埠;
  6. 拿到IP地址和埠,透過PRC協議給指定ws程式傳送資訊;
  7. 該ws程式接收到client id和資訊,給指定的連線傳送資訊;
  8. 客戶端收到資訊。

WebSocket微服務單發時序圖

群發訊息

  1. 前3個步驟跟單發的一樣;
  2. 業務系統拿到client id之後,透過http給指定分組傳送訊息,經過nginx負載分配到一臺ws伺服器;
  3. 這臺ws伺服器拿到分組ID和訊息,釋出給RabbitMQ;
  4. 所有訂閱到RabbitMQ的服務,會收到新資訊推送,找到本機所有該分組的連線;
  5. 給所有這些連線傳送訊息;
  6. 客戶端收到資訊。

WebSocket微服務群發訊息時序圖

原始碼

github:https://github.com/woodylan/go-websocket

交流

QQ群:1028314856

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章