您好,登錄后才能下訂單哦!
這篇文章主要講解了“分析一個Node進程的死亡與善后”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“分析一個Node進程的死亡與善后”吧!
Exit Code
什么是 exit code?
exit code 代表一個進程的返回碼,通過系統調用 exit_group 來觸發。
在 POSIX 中,0 代表正常的返回碼,1-255 代表異常返回碼,在業務實踐中,一般主動拋出的錯誤碼都是 1。在 Node 應用中調用 API process.exitCode = 1 來代表進程因期望外的異常而中斷退出。
這里有一張關于異常碼的附表 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 |
異常碼在操作系統中隨處可見,以下是一個關于 cat 進程的異常以及它的 exit code,并使用 strace 追蹤系統調用。
$ cat a cat: a: No such file or directory # 使用 strace 查看 cat 的系統調用 # -e 只顯示 write 與 exit_group 的系統調用 $ 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 追蹤進程顯示的最后一行可以看出,該進程的 exit code 是 1,并把錯誤信息輸出到 stderr (stderr 的 fd 為 2) 中
如何查看 exit code
從 strace 中可以來判斷進程的 exit code,但是不夠方便過于冗余,更無法第一時間來定位到異常碼。
有一種更為簡單的方法,通過 echo $? 來確認返回碼
$ cat a cat: a: No such file or directory $ echo $? 1 $ node -e "preocess.exit(52)" $ echo $? 52
未曾感知的痛苦何在: throw new Error 與 Promise.reject 區別
以下是兩段代碼,第一段拋出一個異常,第二段 Promise.reject,兩段代碼都會如下打印出一段異常信息,那么兩者有什么區別?
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)
在對上述兩個測試用例使用 echo $? 查看 exit code,我們會發現 throw new Error() 的 exit code 為 1,而 Promise.reject() 的為 0。
從操作系統的角度來講,exit code 為 0 代表進程成功運行并退出,然而此時即使有 Promise.reject,操作系統也會視為它執行成功。
這在 Dockerfile 與 CI 中執行腳本時將留有安全隱患。
Dockerfile 在 Node 鏡像構建時的隱患
當使用 Dockerfile 構建鏡像或者 CI 時,如果進程返回非 0 返回碼,構建就會失敗。
這是一個淺顯易懂的含有 Promise.reject() 問題的鏡像,我們從這個鏡像來看出問題所在。
FROM node:12-alpine RUN node -e "Promise.reject('hello, world')"
構建鏡像過程如下,最后兩行提示鏡像構建成功:即使在構建過程打印出了 unhandledPromiseRejection 信息,但是鏡像仍然構建成功。
$ 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 鏡像內,鏡像會構建失敗,至于原因以下再說。
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 腳本解決方案
能在編譯時能發現的問題,絕不要放在運行時。所以,構建鏡像或 CI 中需要執行 node 腳本時,對異常處理需要手動指定 process.exitCode = 1 來提前暴露問題
runScript().catch(() => { process.exitCode = 1 })
在構建鏡像時,Node 也有關于異常解決方案的建議:
runScript().catch(() => { process.exitCode = 1 })
根據提示,--unhandled-rejections=strict 將會把 Promise.reject 的退出碼設置為 1,并在將來的 node 版本中修正 Promise 異常退出碼。
而下一個版本 Node 15.0 已把 unhandled-rejections 視為異常并返回非 0 退出碼。
$ node --unhandled-rejections=strict error.js
Signal
在外部,如何殺死一個進程?答:kill $pid
而更為準確的來說,一個 kill 命令用以向一個進程發送 signal,而非殺死進程。大概是殺進程的人多了,就變成了 kill。
The kill utility sends a signal to the processes specified by the pid operands.
每一個 signal 由數字表示,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
這些信號中與終端進程接觸最多的為以下幾個,其中 SIGTERM 為 kill 默認發送信號,SIGKILL 為強制殺進程信號
信號 | 數字 | 是否可捕獲 | 描述 |
---|---|---|---|
SIGINT | 2 | 可捕獲 | Ctrl+C 中斷進程 |
SIGQUIT | 3 | 可捕獲 | Ctrl+D 中斷進程 |
SIGKILL | 9 | 不可捕獲 | 強制中斷進程(無法阻塞) |
SIGTERM | 15 | 可捕獲 | 優雅終止進程(默認信號) |
SIGSTOP | 19 | 不可捕獲 | 優雅終止進程中 |
在 Node 中,process.on 可以監聽到可捕獲的退出信號而不退出。以下示例監聽到 SIGINT 與 SIGTERM 信號,SIGKILL 無法被監聽,setTimeout 保證程序不會退出
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)
運行腳本,啟動進程,可以看到該進程的 pid,使用 kill -2 97864 發送信號,進程接收到信號并未退出
$ node signal.js Pid: 97864 Received: SIGTERM Received: SIGTERM Received: SIGTERM Received: SIGINT Received: SIGINT Received: SIGINT
容器中退出時的優雅處理
當在 k8s 容器服務升級時需要關閉過期 Pod 時,會向容器的主進程(PID 1)發送一個 SIGTERM 的信號,并預留 30s 善后。如果容器在 30s 后還沒有退出,那么 k8s 會繼續發送一個 SIGKILL 信號。如果古時皇帝白綾賜死,教你體面。
其實不僅僅是容器,CI 中腳本也要優雅處理進程的退出。
當接收到 SIGTERM/SIGINT 信號時,預留一分鐘時間做未做完的事情。
async function gracefulClose(signal) { await new Promise(resolve => { setTimout(resolve, 60000) }) process.exit() } process.on('SIGINT', gracefulClose) process.on('SIGTERM', gracefulClose)
這個給腳本預留時間是比較正確的做法,但是如果是一個服務有源源不斷的請求過來呢?那就由服務主動關閉吧,調用 server.close() 結束服務
const server = http.createServer(handler) function gracefulClose(signal) { server.close(() => { process.exit() }) } process.on('SIGINT', gracefulClose) process.on('SIGTERM', gracefulClose)
感謝各位的閱讀,以上就是“分析一個Node進程的死亡與善后”的內容了,經過本文的學習后,相信大家對分析一個Node進程的死亡與善后這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。