溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

分析一個(gè)Node進(jìn)程的死亡與善后

發(fā)布時(shí)間:2021-10-15 16:26:45 來(lái)源:億速云 閱讀:155 作者:iii 欄目:web開(kāi)發(fā)

這篇文章主要講解了“分析一個(gè)Node進(jìn)程的死亡與善后”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“分析一個(gè)Node進(jìn)程的死亡與善后”吧!

Exit Code

什么是 exit code?

exit code 代表一個(gè)進(jìn)程的返回碼,通過(guò)系統(tǒng)調(diào)用 exit_group 來(lái)觸發(fā)。

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

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

Exit Code NumberMeaningExampleComments
1Catchall for general errorslet "var1 = 1/0"Miscellaneous errors, such as "divide by zero" and other impermissible operations
2Misuse 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).
126Command invoked cannot execute/dev/nullPermission problem or command is not an executable
127"command not found"illegal_commandPossible problem with $PATH or a typo
128Invalid argument to exitexit 3.14159exit takes only integer args in the range 0 - 255 (see first footnote)
128+nFatal error signal "n"kill -9 $PPID of script$? returns 137 (128 + 9)
130Script terminated by Control-CCtl-CControl-C is fatal error signal 2, (130 = 128 + 2, see above)
255*Exit status out of rangeexit -1exit takes only integer args in the range 0 - 255

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

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

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

如何查看 exit code

從 strace 中可以來(lái)判斷進(jìn)程的 exit code,但是不夠方便過(guò)于冗余,更無(wú)法第一時(shí)間來(lái)定位到異常碼。

有一種更為簡(jiǎn)單的方法,通過(guò) echo $? 來(lái)確認(rèn)返回碼

$ cat a cat: a: No such file or directory  $ echo $? 1 $ node -e "preocess.exit(52)" $ echo $? 52

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

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

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

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

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

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

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

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

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

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

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

$ docker build -t demo . Sending build context to Docker daemon  33.28kB Step 1/2 : FROM node:12-alpine  ---> 18f4bc975732 Step 2/2 : RUN node -e "Promise.reject('hello, world')"  ---> Running in 79a6d53c5aa6 (node:1) UnhandledPromiseRejectionWarning: hello, world (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) (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. Removing intermediate container 79a6d53c5aa6  ---> 09f07eb993fe Successfully built 09f07eb993fe Successfully tagged demo:latest

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

FROM node:15-alpine  RUN node -e "Promise.reject('hello, world')"
$ docker build -t demo . Sending build context to Docker daemon  2.048kB Step 1/2 : FROM node:15-alpine  ---> 8bf655e9f9b2 Step 2/2 : RUN node -e "Promise.reject('hello, world')"  ---> Running in 4573ed5d5b08 node:internal/process/promises:245           triggerUncaughtException(err, true /* fromPromise */);           ^  [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".] {   code: 'ERR_UNHANDLED_REJECTION' } The command '/bin/sh -c node -e "Promise.reject('hello, world')"' returned a non-zero code: 1

Promise.reject 腳本解決方案

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

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

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

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

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

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

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

Signal

在外部,如何殺死一個(gè)進(jìn)程?答:kill $pid

而更為準(zhǔn)確的來(lái)說(shuō),一個(gè) kill 命令用以向一個(gè)進(jìn)程發(fā)送 signal,而非殺死進(jìn)程。大概是殺進(jìn)程的人多了,就變成了 kill。

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

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

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

這些信號(hào)中與終端進(jìn)程接觸最多的為以下幾個(gè),其中 SIGTERM 為 kill 默認(rèn)發(fā)送信號(hào),SIGKILL 為強(qiáng)制殺進(jìn)程信號(hào)

信號(hào)數(shù)字是否可捕獲描述
SIGINT2可捕獲Ctrl+C 中斷進(jìn)程
SIGQUIT3可捕獲Ctrl+D 中斷進(jìn)程
SIGKILL9不可捕獲強(qiáng)制中斷進(jìn)程(無(wú)法阻塞)
SIGTERM15可捕獲優(yōu)雅終止進(jìn)程(默認(rèn)信號(hào))
SIGSTOP19不可捕獲優(yōu)雅終止進(jìn)程中

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

console.log(`Pid: ${process.pid}`)  process.on('SIGINT',  () => console.log('Received: SIGINT')) // process.on('SIGKILL', () => console.log('Received: SIGKILL')) process.on('SIGTERM', () => console.log('Received: SIGTERM'))  setTimeout(() => {}, 1000000)

運(yùn)行腳本,啟動(dòng)進(jìn)程,可以看到該進(jìn)程的 pid,使用 kill -2 97864 發(fā)送信號(hào),進(jìn)程接收到信號(hào)并未退出

$ node signal.js Pid: 97864 Received: SIGTERM Received: SIGTERM Received: SIGTERM Received: SIGINT Received: SIGINT Received: SIGINT

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

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

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

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

async function gracefulClose(signal) {   await new Promise(resolve => {     setTimout(resolve, 60000)   })    process.exit() }  process.on('SIGINT',  gracefulClose) process.on('SIGTERM', gracefulClose)

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

const server = http.createServer(handler)  function gracefulClose(signal) {   server.close(() => {     process.exit()   }) }  process.on('SIGINT',  gracefulClose) process.on('SIGTERM', gracefulClose)

感謝各位的閱讀,以上就是“分析一個(gè)Node進(jìn)程的死亡與善后”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)分析一個(gè)Node進(jìn)程的死亡與善后這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI