Node.js 進程平滑離場剖析

本文由雲+社區發表html

做者:草小灰node

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

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

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

如何定義平滑離場

以進程離場做爲時間分割點,咱們能夠把請求分爲兩類:增量請求存量請求less

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

因此,達成以上兩個目標,基本上咱們就認爲進程的離場是平滑的。在談如何作到進程平滑離場前,咱們須要一種機制,這種機制能讓咱們主動通知進程什麼時候離場,這就涉及到進程間通訊(IPC)的知識了,咱們先簡單瞭解下。curl

進程間通訊

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

信號的種類有不少,如 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 命令向進程發送指定信號:ui

# 發送 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 進程管理很好的選擇。

此文已由騰訊雲+社區在各渠道發佈

獲取更多新鮮技術乾貨,能夠關注咱們騰訊雲技術社區-雲加社區官方號及知乎機構號

相關文章
相關標籤/搜索