上一篇文章中,咱們理解了一般 web 系統的擴展方法後,接下來我們來一篇外傳,來說說關於
即時通訊服務 ( IM Instant Messaging Service ) 的擴展。
本篇文章共分以下幾個章節 :
在開台之前咱們先來簡單的談談,什麼是即時通訊服務的擴展。
簡單的說就是像 line 一樣可以進行即時的溝通。
傳統上要建立這種類型的系統,通常會使用以下兩種機制來建立雙向的溝通 :
首先一般 web 應用都是使用 http 單向的來取得資料,也就是 request 然後 response 這種機制,但是在 im 這種服務系統中,場景通常都是 client A 發送訊息,然後 client B 會收到。
IM 服務就是像聊天室例如 Line 這種類型的服務
而通常要實現這個功能目前應該只有兩種機制 :
咱們來簡單用下圖 1 來看一下這兩種運行的差別。
圖 1 : long polling 與 websocket 差別。
long polling 就如同上圖 1 所示,它會不斷的打 http 來去問 server 有沒有我的訊息,當然想也知道這種很沒效率,可能你問 10 次只有 1 次會收到訊息。
而另一種 websocket 就是當 server 一收到 client A 傳來的訊息後,會直接的推到 client B。
~ 小備註 ~
想比較理解 socket 或 websocket 的差別,可以看看筆者這篇文章。
那 im 服務有什麼問題會導致擴展方式和一般 web 服務不同呢 ? 那就是 :
長連線的問題
在 http 那篇文章中,咱們有提到在 http 1.1 由於有 keepalive 機制,所以預設都是本來就是長連線,但是別忘了 web 這種一般的應用,通常不會一直打 http,所以就算你網頁開這,過了一段時間,這條連線會自動的關閉。
但是在 im 這種系統中,不論是 long polling 或 websocket 基本上都是建立一條很久的長連線,除非你下線,不然有幾個 client 就代表 server 要有幾條永久連線。
先來說一下,咱們通常在建立 IM 服務時,最簡單的起手式就是使用『 socket.io 』 這個套件來快速的建立服務,然後它有分為以下兩個部份 :
簡單的方例如下使用。首先來看 server 端的程式碼。
var io = require('socket.io').listen(8080);
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
再來是 client 方的程式碼
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost:8080');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
</script>
這樣就可以完成一個即時通訊服務。
~ 小備註 ~
想理解 socket.io 相關知識的,可以參考筆者以下幾篇文章。
Socket.io 的說話島
Socket.io 的架構
Socket.io 原始碼分析之建立連線
首先咱們來看第一個方案,如下圖 2 所示。這應該不少人使用的方案,就是與傳統模式一樣,中間加一個負載均衡,記憶中在 nginx 1.3 的版本時,就有支援 websocket。
圖 2 : im 服務擴展方案 1
但是這個方案有幾個雷。
這個雷在於 :
連線貧頸會變成在負載均衡服務
因為 im 服務會一直有連線在,所以如果用傳統的負載均衡會變成如下圖 3 所示,那麼他會發生 :
建立與維護太多條連線,導致 nginx 掛掉
下圖 3 中,你看到的每一條紅線,就是 nginx 所建立的連線量,所以如果 client 有 10 萬個,那這樣實際上它會建立 20 萬條,因為它與 im 服務也需要建連線,而且用戶也要和他建立連線。
這會導致以下兩個問題。
圖 3 : 傳統負載均衡在 im 服務的慘況。
上面咱們有提到 socket.io 基本上可以是咱們最常見的 im 服務套件,然後它有提供三種 transport 方式 :
其中 polling 與 websocket,上面已經有說過,而 defaut 指的就是 :
先用 polling 然後再嘗試升級為 websocket
但這時如果使用『 defaut 與 polling 』這個方案會出事情。
這兩個 transport 會無法建立連線
主要的原因在於 polling ,它就是打多次 http 來看看沒有訊息,但是你想想,如果第一次打 a 台,第二次打 b 台,會發生什麼事情呢 ?
現面來模擬一下這個流程 :
圖 4 : 方案 1 雷 2 圖
這個問題有沒有覺得有點眼熟,這個就和上一篇說的 session 問題有點相似,但這可沒辦法加個 redis 就可以處理。
這裡基本上有以下兩種解法 :
嚴格來說第二種 websocket 方法會比較好,不過咱公司有發生過沒辦法穿透防火牆的問題,不過詳細我也不確定實際情況是如何,就當參考一下。
~ 小補充 1 ~
這裡補充一下,通常咱們會使用 pm2 來當 socket.io process 服務的管理器,然後它有一個 cluster 功能,也就是說它可以在一台機器中,開多個 socket.io process 來處理,這樣就可以用多 cpu 資源。
但是你一開始使用建立一定會失敗,主要的原因也如上述,就是打錯 process 囉,解法就是打同一 process。
~ 小補充 2 ~
應該也有人使用 aws 的 ec2 + alb 方案來建立出 socket.io 服務,這裡應該也會碰到這個雷,而且
alb 雖然有支援某個用戶在一定時間打同一台。
但 ! 它是用 cookie 來決定打同一台,所以瀏覽器的用戶不會出事,手機端的應該就會死一片,如果沒有處理 cookie 的話。
簡單的說將 nginx 拿掉,然後建立一個 dispatcher 服務上去,它就是一個 http 服務,然後它會回應讓 client 去那一台 im 服務來建立長連線,如下圖 3 所示。
圖 5 : 方案
這種方案的優點就在於 :
沒有負載均衡連線量太大的貧頸
這個方案事實上有個大地雷 :
沒處理好,會沒有 HA 機制
socket.io 這個套件它有幫我們建立一套維護連線的心跳機制,也就是說正常來想,如果有啥網路不穩還啥的,導致連線中斷了,它會自動幫我們重連。
坑點在於 :
它是和 IM 服務重連
嗯這很正常,有啥毛病了 ? 假設你那台 im 服務倒或是需要下掉維護,那如果你使用上述方案會產生什麼事情呢 ?
它會一直不斷的重連那台死掉的 IM 服務,直到到達重連上限
如下圖 6 所示。
圖 6 : 分配器模式的雷點
這也代表這 :
你的系統沒有可用性,一台機器倒了,在那台上的用戶全部無法使用,除非用戶主動進行重連。
所以記好,如果是用上述方案來進行擴展的,請記得要處理以下的事情 :
用戶端重連時,請設定要至 dispatcher 拿新的 im 服務位置
你知道如果是已經上線一段的服務,發現這問題有多慘嗎 ? 首先用戶端一定不能保證強制更新到最新版 ( 你想想你的系統多久沒更新了 ? ),所以說不能指望用戶端全部改完就 ok,那這時你要如何更新機器呢 ? 你爆了要如何呢 ? 幾乎完全動不了啊…… 項多只能在全服務關機時才能更新啊……,但如果你的服務是要 7 x 24 小時的,有可能動嗎 ?
一開始走錯,這真的卡很死……啊
本篇文章本來在考慮要不要寫的,但是考慮到自已的血淚史,還是寫出讓後人不要踩到這些雷坑。
最後這裡總結一下各方案的優缺與雷坑注意。
這種方案的優點如下 :
但雷坑點在於 :
這種方案的優點如下 :
而這種方案的大雷坑在於 :
祈禱未來的走這條路的,謹記前人的血淚史。
IM 服務的擴展方案 2 : IM 服務分配器,建立一個 dispatcher 服務上去
dispatcher 服務,是否就需要自己寫相關程式,沒有現成工具可以使用?
我一直以為 nginx 是類似這邊所說 dispatcher
原來實際上 nginx 和 client 及 應用伺服器 兩邊都會保持連結
誤解好久
這裡我不確定喔 ~ 當初沒有到很認真的找 ~