利用 cluster 進行讓 client 不斷線的 server 換版

Posted on Updated on

最近一直在思考跟規劃關於維運的事情,這部分對我來說比程式開發還要難上許多,要如何才能達到 3 個 9、 4 個 9,甚至是 5 個 9 的境界呢(五燈獎?這樣好像不小心透露我的年齡了。)

website availability

要能到這樣的境界,除了程式開發本身設計、錯誤處理要做得好之外,整體的架構跟維運的規劃也要很努力才行。

要讓 node 能善用多核心,大多是採用 cluster 的方式。由於好奇心太重,不知道為什麼就會用的很不安心,很想要知道為什麼 cluster 可以讓多個 process 去監聽同一個 port,就找了一些資料稍微研究了一下,詳情大家可以參考 Fred 這篇《用 NODE.JS 實作多個 PROCESS 監聽並處理同一個 PORT》。

看過這篇文章後可以知道,cluster 是一種 master / worker 的架構,master 與 worker 可以進行通訊,除了傳遞一般的訊息外,他還能傳遞 socket handle,也正是利用這點來達成不同的 process 可以監聽同一個 port。(另外還有 SO_REUSEADDR,有興趣的可以查查看。)

看到這邊,加上之前研究的利用 cluster 來進行平順換版的事,當時是做到可以讓每一個 worker 輪流換板,而不是整個系統全部停機、斷線,但這樣還不夠,因為每一個 worker 輪流換,可能會讓使用者覺得一直在反覆地斷線、連線。

相關連結:https://imazole.wordpress.com/2014/09/02/nodejs-update-app-gracefully/

另外也曾想利用 node.js socket server 的 close 並不是真的斷線,而只是先暫停接受新連線這件事,讓其他的 worker 先暫停接受新連線,等要換版的這個 worker 重新啟動成功後,之前被斷線的 clients 自然就會連上這個新的 worker,此時再開放其他 workers。

相關連結:https://imazole.wordpress.com/2014/09/17/socket-server-close-in-nodejs/

這樣感覺還可以,不是全部的人都斷線,假設 8 核心的機器起 7 個 workers,每次換版只斷了 1/7 的用戶。

但,還是有 1/7 的人被斷線了。

然後再回頭去複習了 cluster 監聽同一個 port 的原理,看到他的 send 是長這樣:

worker.send(message, [sendHandle])

OMG,這個 handle 是可以自己送的,那能不能讓 master 先啟動一個新版的 worker,然後再把某一個舊版 worker 上的 socket handle 全都送回 master,讓 master 轉送給這個新版的 worker,都全部轉送完畢後,再 kill 掉這個舊版的 worker,這樣 clients 全部都不用斷線,也達成了換版的目的。

想到這邊就立刻著手進行一個試驗,驗證一下是否真的可以這樣把 socket 從一個 worker 轉送到另外一個 worker。

寫一個用 cluster 啟動的 socket server,會啟動兩個 workers:worker1 與 worker2,不管是誰接受了 client 連線,就通知 master,並且把這個 client socket 送過去,master 接收到後,就轉送給另外一個 worker,然後 kill 掉剛剛接收連線的那個 worker。(這個 client 會每三秒送一個 hello 跟 timestamp 過來。)

這是剛啟動時的樣子,有兩個 workers 都在 listen 了,pid 分別是 3840 與 3841,後面這個 AAAAA 代表換版前的內容,這時候我去改 code,把 AAAAA 改成 BBBBB。
start socket server

然後啟動 client,看下圖,是 worker2-3841 跟這個 client 建立連線的,他會把這個 socket 送到 server 去,server 會轉送這個 socket 給另外一個 worker,然後回覆跟請他自己自殺的訊息,所以就會看到這行:[3841:AAAAA] receives kill,然後另外一個 worker 收到了[3840:AAAAA] receives sock,接下來收到來自於 client 的 hello 的都是 worker 3840。

另外我也設定了,當 master 有收到一個 worker 離開的訊息後,隔 3 秒啟動一個新的 worker,所以這邊可以看到一個新的 worker 被啟動:[listen] [3854:BBBBB],pid 是 3854,而且內容已經換成新版的 BBBBB,client完全沒有被斷線,持續地送出 hello。

說明版:
brief

證明可行了,是不是很簡單,真的是江湖一點訣,說穿了不值錢。

看到這邊大家也別太開心,並不是所有的東西都可以這樣送來送去,可以傳送的 handle 的種類有:net.Socket, net.Server, net.Native, dgram.Socket, dgram.Native,還有基於這些 socket 開發的 handle,像 http。

不過路還很遠,實務上不知道用起來有沒有問題。由於 server 也有一些管理要做,還需要一些精細的控制。另外像是效能方面,也不知道是否會有影響,master 與 worker 之間的通訊是走 IPC,理論上應該蠻高效的,只要效能方面沒有太大的問題,即使無法做到全部移轉,只要能有效降低斷線的 clients 數,就蠻值得一試的。

這邊有個小心得,其實之前看文件,也不是沒有看到 [sendHandle],但當時學識不足,不能理解這一點。所以每天都要努力,一點一點累積,同樣的東西過一陣子再回頭去看,這段時間累積的東西會發酵出來的,時間從來都不會白花的,共勉之。

原圖:after update

// worker.js 部分程式碼
cluster.worker.on('message', function(type, m) {
  console.log(title + ' receives ' + type);
  // 接收的是 socket,進行原本 client socket 該做的事
  if (type === 'sock') {
    m.on('data', function(data) {
      console.log(title + ' from client -' + data.toString());
    });
  } else if (type === 'kill') {
    // 接收了一個自殺命令
    cluster.worker.kill();
  }
});

server.on('connection', function(sock) {

  console.log(title + ' get connection.');
  // 把 client socket 送到 master
  cluster.worker.send('sock', sock);

  sock.on('data', function(data) {
    // do something...
  });

  // other events
});

發表留言