Node.js 程式平滑離場剖析

騰訊雲加社群發表於2019-01-24

本文由雲+社群發表

作者:草小灰

使用 Node.js 搭建 HTTP Server 已是司空見慣的事。在生產環境中,Node 程式平滑重啟直接關係到服務的可靠性,它的重要性不容我們忽視。既然是平滑重啟,就涉及到新舊程式的接替過渡:

  • 首先,保證新程式平滑入場
  • 其次,保證舊程式平滑離場

本文主要談論下,在新舊程式接替過渡期間,如何保證舊程式平滑離場。那怎樣的離場才算平滑的呢?

如何定義平滑離場

以程式離場作為時間分割點,我們可以把請求分為兩類:增量請求存量請求

  • 在程式離場前,停止接收新的(增量)請求
  • 在程式離場前,保證未完成的(存量)請求正常響應

所以,達成以上兩個目標,基本上我們就認為程式的離場是平滑的。在談如何做到程式平滑離場前,我們需要一種機制,這種機制能讓我們主動通知程式何時離場,這就涉及到程式間通訊(IPC)的知識了,我們先簡單瞭解下。

程式間通訊

對 Unix 或類 Unix 系統而言,程式間通訊的方式有很多種 —— 訊號(Signal)是其中的一種。

訊號的種類有很多,如 SIGINTSIGTERMSIGKILL 等。這些訊號視具體需要用於不同的場景,比如 SIGKILL 一般用於強殺程式。

我們可以在命令列執行 kill -l 檢視所有的訊號,如下所示(其中的數字表示 signal number):

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL
 5) SIGTRAP	 6) SIGABRT	 7) SIGEMT	 8) SIGFPE
 9) SIGKILL	10) SIGBUS	11) SIGSEGV	12) SIGSYS
13) SIGPIPE	14) SIGALRM	15) SIGTERM	16) SIGURG
17) SIGSTOP	18) SIGTSTP	19) SIGCONT	20) SIGCHLD
21) SIGTTIN	22) SIGTTOU	23) SIGIO	24) SIGXCPU
25) SIGXFSZ	26) SIGVTALRM	27) SIGPROF	28) SIGWINCH
29) SIGINFO	30) SIGUSR1	31) SIGUSR2
複製程式碼

我們可以使用 kill 命令向程式傳送指定訊號:

# 傳送 SIGTERM 訊號(預設,無須指定訊號型別)給程式
$ kill <pid>

# 傳送 SIGINT 訊號給程式,其中 <pid> 為具體的程式 ID
$ kill -INT <pid>

# 傳送 SIGKILL 訊號給程式
$ kill -KILL <pid>

# 或者
$ kill -9 <pid>
複製程式碼

程式可以對接收到的訊號作出回應。對 Node 應用而言,訊號是被當作事件傳送給 Node 程式的,程式接收到 SIGTERMSIGINT 事件有預設回撥,官方文件是這麼描述的:

'SIGTERM' and 'SIGINT' have default handlers on non-Windows platforms that reset the terminal mode before exiting with code 128 + signal number. If one of these signals has a listener installed, its default behavior will be removed (Node.js will no longer exit).

這句話寫的很抽象,它是什麼意思呢?我們以一個簡單的 Node 應用為例。

新建檔案,鍵入如下程式碼,將其儲存為 server.js:

const http = require('http');

const server = http.createServer((req, res) => {
  setTimeout(() => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('It works');
  }, 5000);
});

server.listen(9420);
複製程式碼

這裡為了方便測試,對應用接收到的每個 http 請求,等待 5 秒後再進行響應。

執行 node server.js 啟動應用。為了給應用傳送訊號,我們需要獲取應用的程式 ID,我們可以使用 lsof 命令檢視:

$ lsof -i TCP:9420
COMMAND   PID       USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
node    70826 myunlessor   13u  IPv6 0xd250033eef8912eb      0t0  TCP *:9420 (LISTEN)
複製程式碼

事實上,我們也可以在程式碼裡通過 console.log(process.pid) 獲取程式 ID。這裡只是順便介紹一種,在知道監聽 TCP 埠的情況獲取程式的方式。

隨後,我們發起一個請求,在收到響應之前(有 5 秒等待時間),我們給應用傳送 SIGINT 訊號。

$ curl http://localhost:9420 &

$ kill -INT 70826
curl: (52) Empty reply from server
[1]+  Exit 52                 curl http://localhost:9420
複製程式碼

可以看到,請求沒能正常收到響應。也就是說,預設情況下,Node 應用在接收到 SIGINT 訊號時,會馬上把程式殺死,無視程式還沒處理完成的請求。所幸的是,我們可以手動監聽程式的 SIGINT 事件,像這樣:

process.on('SIGINT', () => {
  // do something here
});
複製程式碼

如果我們在事件回撥裡什麼都不做,就意味著忽略該訊號,程式該幹嘛幹嘛,像什麼事情都沒發生一樣。

那麼,如果我手動監聽 SIGKILL 會如何呢?對不起,SIGKILL 是不能被監聽的,官方文件如是說:

'SIGKILL' cannot have a listener installed, it will unconditionally terminate Node.js on all platforms.

這是合情合理的,要知道 SIGKILL 是用於強殺程式的,你無法干預它的行為。

回到上面的問題,我們可以近似地理解為 Node 應用響應 SIGINT 事件的預設回撥是這樣子的:

process.on('SIGINT', () => {
  process.exit(128 + 2/* signal number */);
});
複製程式碼

我們可以列印 exit code 來驗證:

$ node server.js

$ echo $?
130
複製程式碼

有了訊號,我們就能主動通知程式何時離場了,下面談一談程式如何平滑離場。

如何讓程式平滑離場

我們在上面示例基礎上,也就是在檔案 server.js 中,補充如下程式碼:

process.on('SIGINT', () => {
  server.close(err => {
    process.exit(err ? 1 : 0);
  });
});
複製程式碼

這段程式碼很簡單,我們改寫應用接收到 SIGINT 事件的預設行為,不再簡單粗暴直接殺死程式,而是在 server.close 方法回撥中再呼叫 process.exit 方法,接著繼續試驗一下。

$ lsof -i TCP:9420
COMMAND   PID       USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
node    75842 myunlessor   13u  IPv6 0xd250033ec7c9362b      0t0  TCP *:9420 (LISTEN)

$ curl http://localhost:9420 &
[1] 75878

$ kill -2 75842

$ It works
[1]+  Done                    curl http://localhost:9420
複製程式碼

可以看到,應用在退出前(即程式離場前),成功地響應了存量請求。

我們還可以驗證,程式離場前,確實不再接收增量請求:

$ curl http://127.0.0.1:9420
curl: (7) Failed to connect to 127.0.0.1 port 9420: Connection refused
複製程式碼

這正是 server.close 所做的事,程式平滑離場就是這麼簡單,官方文件是這麼描述這個 API 的:

Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the server is finally closed when all connections are ended and the server emits a 'close' event. The optional callback will be called once the 'close' event occurs. Unlike that event, it will be called with an Error as its only argument if the server was not open when it was closed.

結束語

程式平滑離場只是 Node 程式平滑重啟的一部分。生產環境中,新舊程式的接替涉及程式負載均衡、程式生命週期管理等方方面面的考慮。專業的工具做專業的事,PM2 就是 Node 程式管理很好的選擇。

此文已由騰訊雲+社群在各渠道釋出

獲取更多新鮮技術乾貨,可以關注我們騰訊雲技術社群-雲加社群官方號及知乎機構號

相關文章