自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一個 Node 進程的死亡與善后

開發(fā) 前端
人固有一死,一個 Node 進程亦是如此,總有萬般不愿也無法避免。從本篇文章我們看看一個進程滅亡時如何從容離去。

[[387380]]

 本文轉(zhuǎn)載自微信公眾號「全棧成長之路」,作者山月行。轉(zhuǎn)載本文請聯(lián)系全棧成長之路公眾號。  

嗯,這是山月好久沒有更新的原創(chuàng),正文從下開始。

人固有一死,一個 Node 進程亦是如此,總有萬般不愿也無法避免。從本篇文章我們看看一個進程滅亡時如何從容離去。

一個 Node 進程,除了提供 HTTP 服務(wù)外,也絕少不了跑腳本的身影。跑一個腳本拉取配置、處理數(shù)據(jù)以及定時任務(wù)更是家常便飯。在一些重要流程中能夠看到腳本的身影:

  • CI,用以測試、質(zhì)量保障及部署等
  • Cron,用以定時任務(wù)
  • Docker,用以構(gòu)建鏡像

如果在這些重要流程中腳本出錯無法及時發(fā)現(xiàn)問題,將有可能引發(fā)更加隱蔽的問題。如果在 HTTP 服務(wù)出現(xiàn)問題時,無法捕獲,服務(wù)異常是不可忍受的。

最近觀察項目鏡像構(gòu)建,會偶爾發(fā)現(xiàn)一兩個鏡像雖然構(gòu)建成功,但容器卻跑不起來的情況究其原因,是因為 一個 Node 進程滅亡卻未曾感知到的問題。

Exit Code

什么是 exit code?

exit code 代表一個進程的返回碼,通過系統(tǒng)調(diào)用 exit_group 來觸發(fā)。

在 POSIX 中,0 代表正常的返回碼,1-255 代表異常返回碼,在業(yè)務(wù)實踐中,一般主動拋出的錯誤碼都是 1。在 Node 應(yīng)用中調(diào)用 API process.exitCode = 1 來代表進程因期望外的異常而中斷退出。

這里有一張關(guān)于異常碼的附表 Appendix E. Exit Codes With Special Meanings[1]。

 

Exit Code Number Meaning Example Comments
1 Catchall for general errors let "var1 = 1/0" Miscellaneous errors, such as "divide by zero" and other impermissible operations
2 Misuse of shell builtins (according to Bash documentation) empty_function() {} Missing keyword or command, or permission problem (and diff return code on a failed binary file comparison).
126 Command invoked cannot execute /dev/null Permission problem or command is not an executable
127 "command not found" illegal_command Possible problem with $PATH or a typo
128 Invalid argument to exit exit 3.14159 exit takes only integer args in the range 0 - 255 (see first footnote)
128+n Fatal error signal "n" kill -9 $PPID of script $? returns 137 (128 + 9)
130 Script terminated by Control-C Ctl-C Control-C is fatal error signal 2, (130 = 128 + 2, see above)
255* Exit status out of range exit -1 exit takes only integer args in the range 0 - 255

異常碼在操作系統(tǒng)中隨處可見,以下是一個關(guān)于 cat 進程的異常以及它的 exit code,并使用 strace 追蹤系統(tǒng)調(diào)用。

  1. $ cat a 
  2. cat: a: No such file or directory 
  3.  
  4. # 使用 strace 查看 cat 的系統(tǒng)調(diào)用 
  5. # -e 只顯示 write 與 exit_group 的系統(tǒng)調(diào)用 
  6. $ strace -e write,exit_group cat a 
  7. write(2, "cat: ", 5cat: )                    = 5 
  8. write(2, "a", 1a)                        = 1 
  9. write(2, ": No such file or directory", 27: No such file or directory) = 27 
  10. write(2, "\n", 1 
  11. )                       = 1 
  12. exit_group(1)                           = ? 
  13. +++ exited with 1 +++ 

從 strace 追蹤進程顯示的最后一行可以看出,該進程的 exit code 是 1,并把錯誤信息輸出到 stderr (stderr 的 fd 為 2) 中

如何查看 exit code

從 strace 中可以來判斷進程的 exit code,但是不夠方便過于冗余,更無法第一時間來定位到異常碼。

有一種更為簡單的方法,通過 echo $? 來確認返回碼

  1. $ cat a 
  2. cat: a: No such file or directory 
  3.  
  4. $ echo $? 
  5. $ node -e "preocess.exit(52)" 
  6. $ echo $? 
  7. 52 

未曾感知的痛苦何在: throw new Error 與 Promise.reject 區(qū)別

以下是兩段代碼,第一段拋出一個異常,第二段 Promise.reject,兩段代碼都會如下打印出一段異常信息,那么兩者有什么區(qū)別?

  1. function error () { 
  2.   throw new Error('hello, error'
  3.  
  4. error() 
  5.  
  6. // Output
  7.  
  8. // /Users/shanyue/Documents/note/demo.js:2 
  9. //   throw new Error('hello, world'
  10. //   ^ 
  11. // 
  12. // Error: hello, world 
  13. //     at error (/Users/shanyue/Documents/note/demo.js:2:9) 
  1. async function error () { 
  2.   return new Error('hello, error'
  3.  
  4. error() 
  5.  
  6. // Output
  7.  
  8. // (node:60356) UnhandledPromiseRejectionWarning: Error: hello, world 
  9. //    at error (/Users/shanyue/Documents/note/demo.js:2:9) 
  10. //    at Object.<anonymous> (/Users/shanyue/Documents/note/demo.js:5:1) 
  11. //    at Module._compile (internal/modules/cjs/loader.js:701:30) 
  12. //    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10) 

在對上述兩個測試用例使用 echo $? 查看 exit code,我們會發(fā)現(xiàn) throw new Error() 的 exit code 為 1,而 Promise.reject() 的為 0。

從操作系統(tǒng)的角度來講,exit code 為 0 代表進程成功運行并退出,然而此時即使有 Promise.reject,操作系統(tǒng)也會視為它執(zhí)行成功。

這在 Dockerfile 與 CI 中執(zhí)行腳本時將留有安全隱患。

Dockerfile 在 Node 鏡像構(gòu)建時的隱患

當(dāng)使用 Dockerfile 構(gòu)建鏡像或者 CI 時,如果進程返回非 0 返回碼,構(gòu)建就會失敗。

這是一個淺顯易懂的含有 Promise.reject() 問題的鏡像,我們從這個鏡像來看出問題所在。

  1. FROM node:12-alpine 
  2.  
  3. RUN node -e "Promise.reject('hello, world')" 

構(gòu)建鏡像過程如下,最后兩行提示鏡像構(gòu)建成功:即使在構(gòu)建過程打印出了 unhandledPromiseRejection 信息,但是鏡像仍然構(gòu)建成功。

  1. $ docker build -t demo . 
  2. Sending build context to Docker daemon  33.28kB 
  3. Step 1/2 : FROM node:12-alpine 
  4.  ---> 18f4bc975732 
  5. Step 2/2 : RUN node -e "Promise.reject('hello, world')" 
  6.  ---> Running in 79a6d53c5aa6 
  7. (node:1) UnhandledPromiseRejectionWarning: hello, world 
  8. (node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1) 
  9. (node:1) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. 
  10. Removing intermediate container 79a6d53c5aa6 
  11.  ---> 09f07eb993fe 
  12. Successfully built 09f07eb993fe 
  13. Successfully tagged demo:latest 

但如果是在 node 15 鏡像內(nèi),鏡像會構(gòu)建失敗,至于原因以下再說。

  1. FROM node:15-alpine 
  2.  
  3. RUN node -e "Promise.reject('hello, world')" 
  1. $ docker build -t demo . 
  2. Sending build context to Docker daemon  2.048kB 
  3. Step 1/2 : FROM node:15-alpine 
  4.  ---> 8bf655e9f9b2 
  5. Step 2/2 : RUN node -e "Promise.reject('hello, world')" 
  6.  ---> Running in 4573ed5d5b08 
  7. node:internal/process/promises:245 
  8.           triggerUncaughtException(err, true /* fromPromise */); 
  9.           ^ 
  10.  
  11. [UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "hello, world".] { 
  12.   code: 'ERR_UNHANDLED_REJECTION' 
  13. The command '/bin/sh -c node -e "Promise.reject('hello, world')"' returned a non-zero code: 1 

Promise.reject 腳本解決方案

能在編譯時能發(fā)現(xiàn)的問題,絕不要放在運行時。所以,構(gòu)建鏡像或 CI 中需要執(zhí)行 node 腳本時,對異常處理需要手動指定 process.exitCode = 1 來提前暴露問題

  1. runScript().catch(() => { 
  2.   process.exitCode = 1 
  3. }) 

在構(gòu)建鏡像時,Node 也有關(guān)于異常解決方案的建議:

  1. runScript().catch(() => { 
  2.   process.exitCode = 1 
  3. }) 

根據(jù)提示,--unhandled-rejections=strict 將會把 Promise.reject 的退出碼設(shè)置為 1,并在將來的 node 版本中修正 Promise 異常退出碼。

而下一個版本 Node 15.0 已把 unhandled-rejections 視為異常并返回非 0 退出碼。

  1. $ node --unhandled-rejections=strict error.js 

Signal

在外部,如何殺死一個進程?答:kill $pid

而更為準確的來說,一個 kill 命令用以向一個進程發(fā)送 signal,而非殺死進程。大概是殺進程的人多了,就變成了 kill。

The kill utility sends a signal to the processes specified by the pid operands.

每一個 signal 由數(shù)字表示,signal 列表可由 kill -l 打印

  1. # 列出所有的 signal 
  2. $ kill -l 
  3.  1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP 
  4.  6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1 
  5. 11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM 
  6. 16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP 
  7. 21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ 
  8. 26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR 
  9. 31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3 
  10. 38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8 
  11. 43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 
  12. 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 
  13. 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7 
  14. 58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2 
  15. 63) SIGRTMAX-1  64) SIGRTMAX 

這些信號中與終端進程接觸最多的為以下幾個,其中 SIGTERM 為 kill 默認發(fā)送信號,SIGKILL 為強制殺進程信號

信號 數(shù)字 是否可捕獲 描述
SIGINT 2 可捕獲 Ctrl+C 中斷進程
SIGQUIT 3 可捕獲 Ctrl+D 中斷進程
SIGKILL 9 不可捕獲 強制中斷進程(無法阻塞)
SIGTERM 15 可捕獲 優(yōu)雅終止進程(默認信號)
SIGSTOP 19 不可捕獲 優(yōu)雅終止進程中

在 Node 中,process.on 可以監(jiān)聽到可捕獲的退出信號而不退出。以下示例監(jiān)聽到 SIGINT 與 SIGTERM 信號,SIGKILL 無法被監(jiān)聽,setTimeout 保證程序不會退出

  1. console.log(`Pid: ${process.pid}`) 
  2.  
  3. process.on('SIGINT',  () => console.log('Received: SIGINT')) 
  4. // process.on('SIGKILL', () => console.log('Received: SIGKILL')) 
  5. process.on('SIGTERM', () => console.log('Received: SIGTERM')) 
  6.  
  7. setTimeout(() => {}, 1000000) 

運行腳本,啟動進程,可以看到該進程的 pid,使用 kill -2 97864 發(fā)送信號,進程接收到信號并未退出

  1. $ node signal.js 
  2. Pid: 97864 
  3. Received: SIGTERM 
  4. Received: SIGTERM 
  5. Received: SIGTERM 
  6. Received: SIGINT 
  7. Received: SIGINT 
  8. Received: SIGINT 

容器中退出時的優(yōu)雅處理

當(dāng)在 k8s 容器服務(wù)升級時需要關(guān)閉過期 Pod 時,會向容器的主進程(PID 1)發(fā)送一個 SIGTERM 的信號,并預(yù)留 30s 善后。如果容器在 30s 后還沒有退出,那么 k8s 會繼續(xù)發(fā)送一個 SIGKILL 信號。如果古時皇帝白綾賜死,教你體面。

其實不僅僅是容器,CI 中腳本也要優(yōu)雅處理進程的退出。

當(dāng)接收到 SIGTERM/SIGINT 信號時,預(yù)留一分鐘時間做未做完的事情。

  1. async function gracefulClose(signal) { 
  2.   await new Promise(resolve => { 
  3.     setTimout(resolve, 60000) 
  4.   }) 
  5.  
  6.   process.exit() 
  7.  
  8. process.on('SIGINT',  gracefulClose) 
  9. process.on('SIGTERM', gracefulClose) 

這個給腳本預(yù)留時間是比較正確的做法,但是如果是一個服務(wù)有源源不斷的請求過來呢?那就由服務(wù)主動關(guān)閉吧,調(diào)用 server.close() 結(jié)束服務(wù)

  1. const server = http.createServer(handler) 
  2.  
  3. function gracefulClose(signal) { 
  4.   server.close(() => { 
  5.     process.exit() 
  6.   }) 
  7.  
  8. process.on('SIGINT',  gracefulClose) 
  9. process.on('SIGTERM', gracefulClose) 

總結(jié)

  1. 當(dāng)進程結(jié)束的 exit code 為非 0 時,系統(tǒng)會認為該進程執(zhí)行失敗
  2. 通過 echo $? 可查看終端上一進程的 exit code
  3. Node 中 Promise.reject 時 exit code 為 0
  4. Node 中可以通過 process.exitCode = 1 顯式設(shè)置 exit code
  5. 在 Node12+ 中可以通過 node --unhandled-rejections=strict error.js 執(zhí)行腳本,視 Promise.reject 的 exit code 為 1,在 Node15 中修復(fù)了這一個問題
  6. Node 進程退出時需要優(yōu)雅退出
  7. k8s 關(guān)閉 POD 時先發(fā)一個 SIGTERM 信號,留 30s 時間處理未完成的事,如若 POD 沒有正常退出,30s 過后發(fā)送 SIGKILL 信號

參考資料

[1]

Appendix E. Exit Codes With Special Meanings: http://www.tldp.org/LDP/abs/html/exitcodes.html

 

責(zé)任編輯:武曉燕 來源: 全棧成長之路
相關(guān)推薦

2013-04-25 09:55:21

進程線程

2013-01-15 15:22:07

2019-09-18 15:09:50

進程線程操作系統(tǒng)

2015-10-15 17:50:00

2021-05-21 09:36:42

開發(fā)技能代碼

2011-10-25 09:28:30

Node.js

2020-07-16 07:22:10

PythonNode.JS編程語言

2013-02-25 10:26:31

創(chuàng)業(yè)創(chuàng)業(yè)者

2024-05-27 00:00:20

2020-07-28 10:25:30

Node模塊前端

2020-08-07 10:40:56

Node.jsexpress前端

2024-11-21 08:31:07

耗資源神秘進程

2023-07-03 07:27:41

進程線程Win32

2018-06-05 15:41:22

進程線程協(xié)程

2023-09-04 08:08:59

2019-05-29 10:55:01

開源Linux發(fā)行版

2013-02-25 10:18:08

ThreadMsgC#

2019-06-10 15:00:27

node命令行前端

2015-10-12 16:45:26

NodeWeb應(yīng)用框架

2010-10-09 17:11:16

病毒分析
點贊
收藏

51CTO技術(shù)棧公眾號