如何在kubernetes中實現分散式可延伸的WebSocket服務架構

2023-09-13 12:01:51

如何在kubernetes中實現分散式可延伸的WebSocket服務架構

How to implement a distributed and auto-scalable WebSocket server architecture on Kubernetes一文中雖然解決是WebSocket長連線問題,但可以為其他長連線負載均衡場景提供參考價值

WebRTC 是一套開放web標準,用於在使用者端之間建立(端到端方式的)直接通訊。WebRTC signaling 是WebRTC協定的前置步驟,它依賴signaling server在需要建立WebRTC連線的使用者端之間轉發協商協定。使用者端和signaling server之間的連線通常使用WebSockets

這種方式可以解決分散式約束問題,但有兩個關鍵限制:

  1. 每個signaling範例都會讀取其他範例釋出的訊息,這會導致讀取的訊息數量是範例數的平方,但平均只有1/N 的訊息是有效的(即被接收方所在的範例接收到),大部分訊息都會被丟棄。
  2. 有可能還需要對pub/sub broker實現自動縮放功能,複雜且增加了開支。

解決均衡約束

大部分預設的負載均衡演演算法為round-robin,但這種方式適用於HTTP短連線,不能在自動擴縮容情況下均衡WebSocket連線。另外有一種least-connected演演算法,可以將WebSocket連線請求分配給具有最少active連線的範例。這種方式可以保證在擴容情況下達到最終均衡。

這種方案的問題是並不是所有的負載均衡器都支援least-connected負載均衡演演算法,如Nginx支援,但 GCP’s HTTP(S) 負載均衡器不支援,這種情況下可能要訴諸於比較笨拙的辦法,如readiness probes:即讓具有最多負載的signaling範例暫時處於Unready狀態(此時endpoint controller會從所有service上移除該pod),以此來阻止負載均衡器向該範例傳送新的連線請求。

image

我們的解決方案:使用基於雜湊的負載均衡演演算法

使用rendezvous 希解決分佈性約束

基於雜湊的負載均衡演演算法是一種確定均衡流量的方法,根據使用者端請求中的內容(如header的值、請求或路徑引數以及使用者端IP等)來計算雜湊值。有兩種著名的雜湊演演算法: 一致性雜湊rendezvous 雜湊。這裡我們選擇了後者,原因是它更加簡單,且均衡性更好。演演算法如下:

H(val, I) = I_i

- H is the hash-based algorithm
- val is the value (extracted from the request) from which the hash is computed
- I = {I_1, I_2, ..., I_N} is the set of all backend instances
- I_i is the backend instance that was "selected" by the algorithm

如果使用使用者端的clientId作為引數val,那麼就可以將每個使用者端對映到特定的signaling範例上。此外,只要知道clientId和後端範例,就可以通過該函數了解到使用者端和範例的對應關係,這也意味著,如果一個signaling範例接收到發起端的訊息,但沒有在本地找到接收端,此時就可以通過雜湊演演算法知道接收端位於哪個範例上。下面看下具體實施步驟:

  1. 當接收到新的WebSocket連線請求時,使用請求中的clientId作為rendezvous 雜湊的入參。
  2. 每個signaling範例需要了解系統中的其他範例,這可以通過kubernetes中的Headless Service關聯signaling deployment,然後呼叫Kubernetes Endpoints API獲得範例地址。
  3. 當signaling I₁從一個發起端接收到WebSocket訊息時,會從請求中讀取接收端的clientId,然後從本地查詢接收端,如果找到,則通過WebSocket將訊息轉發給對端即可,如果沒有找到,則使用rendezvous 雜湊演演算法,並使用clientId作為val,signaling範例的IPs作為I,計算出接收端註冊的範例I₂。如果 I₂ = I₁ ,說明接收端已經斷開連線或從未註冊,反之則直接將訊息轉發給 I₂ 。
  4. I₁ 轉發給 I₂的方式有很多種,這裡採用普通的HTTP請求作為範例間通訊。我們採用批次傳送的方式來減少HTTP請求數量。
image

解決均衡約束

使用基於雜湊的負載均衡可以優雅地解決分佈性約束,通過kubernetes Endpoint API也可以很容易地獲取signaling範例的變動。rendezvous雜湊的一個特點是,當新增或刪除後端範例時,會改變函數的引數I,函數的返回值只會影響一部分資料(如果範例從N-1擴充套件為N,則平均影響1/N的資料)。

但在範例變更之後,誰去負責重新分配註冊的使用者端?下面有兩種方式解決該問題:

1.強制使用者端斷開連線

當一個signaling範例Iᵢ通過kubernetes Engpoint API探測到擴縮容事件後,它會遍歷本地註冊的所有使用者端,然後使用rendezvous雜湊演演算法針對更新後的範例集中的每個clientId重新計算所有結果。理論上,計算出的部分新結果不屬於Iᵢ,此時Iᵢ可以斷開這部分使用者端的WebSocket連線,如果使用者端有重連機制,就會重新發起建鏈,當請求到達負載均衡器之後,會被分配到正確的signaling範例上。

image

擴容前

image

在擴容後,觸發使用者端重連

該方式比較簡單,但存在一些弊端:

  1. 首先使用者端需要有重連機制
  2. 其次會打斷使用者端對談
  3. 增加了signaling服務實現程式碼和周邊架構的耦合
  4. 在每次擴縮容之後會增加請求峰值。

出於上述原因,我們放棄了這種方式。

2.負載均衡器本身中重新對映Websocket

這裡我們自己實現了負載均衡器,但僅用於代理WebSocket的請求和訊息,不處理如TLS和ALPN之類的功能(這部分由前置的負載均衡處理)。實現步驟如下:

  1. 通過kubernetes API來發現signaling範例,並實現rendezvous雜湊邏輯。
  2. 設定一個基本的Websocket服務監聽連線請求,並根據rendezvous雜湊計算(使用者端的clientId)的結果將請求路由到後端signaling範例,最後將響應返回給使用者端。如果返回結果有效,則與該使用者端建立兩條WebSocket連線:一條從使用者端到負載均衡器,另一條從負載均衡器到signaling範例。
  3. 當負載均衡器從 使用者端-複雜均衡器 的WebSocket上接收到訊息後,它會通過 負載均衡器-signaling 進行轉發,反之亦然。
  4. 最後根據擴縮容實現WebSocket的對映邏輯:當負載均衡器通過kubernetes API檢測到signaling範例變動時,它會遍歷所有使用者端及其當前代理Websocket的clientId,然後使用rendezvous雜湊演演算法並代入新的後端範例重新計算結果。當返回的範例與當前使用者端註冊的不一致,則負載均衡器只會斷開與該使用者端相關的 負載均衡器-signaling 之間的WebSocket,並重新建立一條到正確的signaling範例的 負載均衡器-signaling 連結。
image

總結

文中最後使用自實現的負載均衡器來緩解後端範例擴縮容對使用者端的影響。需要注意的是,rendezvous雜湊演演算法在擴容場景下不大友好,需要重新計算所有key(文中為clientId)的雜湊值,因此在資料量大的情況下會造成一定的效能問題,因此適合資料量減小或快取場景。

參考