本文转载自微信公众号《全栈成长之路》,作者单月星。转载本文请联系全栈成长之路公众号。嗯,这是山月的原创作品,好久没更新了,正文从下开始。人性终有一死,Node进程亦然,总有种种不甘,无法避免。从这篇文章开始,我们来看看当一个进程死掉时,如何从容离开。一个Node进程,除了提供HTTP服务外,还必须运行脚本。运行脚本来拉取配置、处理数据和安排任务更为常见。在一些重要的流程中可以看到脚本:CI,用于测试、质量保证和部署等Cron,用于定时任务Docker,用于构建镜像可能会出现更隐蔽的问题。如果在HTTP服务出现问题的时候不能捕捉到,服务异常就无法忍受了。最近观察项目镜像构建,偶尔发现一两个镜像虽然构建成功,但是容器无法运行。原因是有一个Node进程死掉了,但是并没有意识到这个问题。退出代码什么是退出代码?exitcode表示进程的返回码,由系统调用exit_group触发。在POSIX中,0代表正常返回码,1-255代表异常返回码。在业务实践中,一般主动抛出的错误码为1,在Node应用中调用APIprocess.exitCode=1表示进程因意外异常中断退出。这是关于异常代码的表格附录E.具有特殊含义的退出代码[1]。退出代码编号含义示例注释1一般错误的Catchalllet"var1=1/0"杂项错误,例如“除以零”和其他不允许的操作2滥用shell内置函数(根据Bash文档)empty_function(){}Missing关键字或命令,或权限问题(以及二进制文件比较失败时的diff返回代码)。126调用的命令无法执行/dev/null权限问题或命令不是可执行文件127“未找到命令”illegal_command$PATH可能存在问题或拼写错误128退出参数无效exit3.14159exit只接受0-255范围内的整数参数(见第一个脚注)128+n致命错误信号“n”kill-9$PPIDofscript$?返回137(128+9)130由Control-CCtl-CControl-C终止的脚本是致命错误信号2,(130=128+2,见上文)255*退出状态超出范围exit-1exit只接受整数args0-255异常代码在操作系统中随处可见,下面是cat进程的异常及其退出代码,使用strace跟踪系统调用$catacat:a:Nosuchfileordirectory#使用strace查看cat的系统调用#-e只显示write和exit_group的系统调用$strace-ewrite,exit_groupcatawrite(2,"cat:",5cat:)=5write(2,"a",1a)=1write(2,":Nosuchfileordirectory",27:Nosuchfileordirectory)=27write(2,"\n",1)=1exit_group(1)=?+++exitedwith1++++最后一行可以看出strace跟踪进程显示进程的退出码为1,错误信息输出到stderr(stderr的fd为2)。如何查看退出代码?可以通过strace来判断进程的退出码,但是不够方便,也过于冗余,更何况第一个异常代码的定位需要一段时间。有一种更简单的方式可以通过echo$?thrownewError和Promise.reject的区别下面是两段代码,第一段抛出异常,第二段Promise.reject,两段代码都会打印出一条异常信息如下,那么什么是两者的区别?functionerror(){thrownewError('hello,error')}error()//Output:///Users/shanyue/Documents/note/demo.js:2//thrownewError('hello,world')//^////Error:hello,world//aterror(/Users/shanyue/Documents/note/demo.js:2:9)asyncfunctionerror(){returnnewError('hello,error')}error()//输出://(node:60356)UnhandledPromiseRejectionWarning:Error:hello,world//aterror(/Users/shanyue/Documents/note/demo.js:2:9)//atObject.(/Users/shanyue/Documents/note/demo.js:5:1)//atModule._compile(internal/modules/cjs/loader.js:701:30)//atObject.Module._extensions..js(internal/modules/cjs/loader.js:712:10)使用echo$?查看上面两个测试用例的退出码,我们会发现thrownewError()的退出码是1,Promise.reject()的退出码是0。从操作系统的角度来看,0的退出代码意味着进程已成功运行并退出。但是,即使此时有Promise.reject,操作系统也会认为是执行成功。这会在Dockerfile和CI中执行脚本时留下安全隐患。构建Node镜像时Dockerfile的隐患使用Dockerfile构建镜像或CI时,如果进程返回非零返回码,会导致构建失败。这是Promise.reject()问题的一个易于理解的图像,我们可以从这张图像中看出问题所在。FROMnode:12-alpineRUNnode-e"Promise.reject('hello,world')"构建镜像的过程如下,最后两行表示镜像构建成功:构建过程中,镜像仍然构建成功。$dockerbuild-tdemo.SendingbuildcontexttoDockerdaemon33.28kBStep1/2:FROMnode:12-alpine--->18f4bc975732Step2/2:RUNnode-e"Promise.reject('hello,world')"--->Runningin79a6d53c5aa6(node:1)UnhandledPromiseWarningRejection:hello,world(node:1)UnhandledPromiseRejectionWarning:Unhandledpromiserejection.Thiserrororiginatedeitherbythrowinginsideofanasyncfunctionwithoutacatchblock,orbyrejectingapromisewhichwasnothandledwith.catch().Toterminatethenodeprocessonunhandledpromiserejection,usetheCLIflag`--unhandled-rejections=strict`(seehttps://nodejs.org/api/cli.html#cli_unhandled_rejections_mode).(rejectionid:1)(node:1)[DEP0018]DeprecationWarning:Unhandledpromiserejectionsaredeprecated.Inthefuture,promiserejectionsthatarenothandledwillterminatetheNode.jsprocesswithanon-zeroexitcode.Removingintermediatecontainer79a6d53c5aa6--->09f07eb993feSuccessfullybuilt09f07eb993feSuccessfullytaggeddemo:latest但如果是在node15镜像内,镜像会构建失败,至于原因稍后详细介绍。FROMnode:15-alpineRUNnode-e"Promise.reject('hello,world')"$dockerbuild-tdemo.SendingbuildcontexttoDockerdaemon2.048kBStep1/2:FROMnode:15-alpine--->8bf655e9f9b2Step2/2:RUNnode-e"Promise.reject('hello,world')"--->Runningin4573ed5d5b08node:internal/process/promises:245triggerUncaughtException(err,true/*fromPromise*/);^[UnhandledPromiseRejection:Thiserrororiginatedeitherbythrowinginsideofanasyncfunctionwithoutacatchblock,orbyrejectingapromisewhichwasnothandledwith.catch().Thepromisesonrejectedwith"你好,eaworld"你好...运行。因此,在构建镜像或CI时需要执行node脚本时,需要手动指定process.exitCode=1进行异常处理,提前暴露问题runScript().catch(()=>{process.exitCode=1})在构建镜像时,Node也有异常解决的建议:runScript().catch(()=>{process.exitCode=1})根据提示,--unhandled-rejections=strict会设置Promise的退出码.reject为1,并在未来的节点版本中修复Promise异常退出代码。下一个版本Node15.0已经将未处理的拒绝视为异常并返回非零退出代码。$node--unhandled-rejections=strictterror.jsSignal是外部的,如何杀死进程?答案:kill$pid更准确??地说,kill命令用于向进程发送信号,而不是杀死进程。可能是杀进程的人太多,就变成了kill。kill实用程序向pid操作数指定的进程发送信号。每个信号用一个数字表示,信号列表可以通过kill-l#Listallsignals$kill-l1)SIGHUP2)SIGINT3)SIGQUIT4)SIGILL5)SIGTRAP6)SIGABRT7)SIGBUS8)SIGFPE9)SIGKILL10)SIGUSR111)SIGSEGV12)SIGUSR213)SIGPIPE14)SIGALRM15)SIGTERM16)SIGSTKFLT17)SIGCHLD18)SIGCONT19)SIGSTOP20)SIGTSTP21)SIGTTIN22)SIGTTOU23)SIGURG24)SIGXCPU25)SIGXFSZ26)SIGVTALRM27)SIGPROF28)SIGWINCH29)SIGIO30)SIGPWR31)SIGSYS34)SIGRTMIN35)SIGRTMIN+136)SIGRTMIN+237)SIGRTMIN+338)SIGRTMIN+439)SIGRTMIN+540)SIGRTMIN+641)SIGRTMIN+742)SIGRTMIN+843)SIGRTMIN+944)SIGRTMIN+1045)SIGRTMIN+1146)SIGRTMIN+1141247)SIGRTMIN+1348)SIGRTMIN+1449)SIGRTMIN+1550)SIGRTMAX-1451)SIGRTMAX-1352)SIGRTMAX-1253)SIGRTMAX-1154)SIGRTMAX-1055)SIGRTMAX-956)SIGRTMAX-857)SIGRTMAX-758)SIGRTMAX-659)SIGRTMAX-560)SIGRTMAX-461)SIGRTMAX-362)SIGRTMAX-263)SIGRTMAX-164)SIGRTMAX在这些信号中,与终端进程接触最多的有以下几种,其中SIGTERM是kill默认发送的信号,SIGKILL是到的信号强制杀死进程是否有多少可捕获描述SIGINT2可捕获Ctrl+C中断进程SIGQUIT3可捕获Ctrl+D中断进程SIGKILL9不可捕获强制中断进程(不可阻塞)SIGTERM15可捕获进程优雅终止(默认信号)SIGSTOP19不可捕获进程优雅终止在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-297864发送信号,进程收到$nosignal.jsPid:97864Received:SIGTERMReceived:SIGTERMReceived:SIGTERMReceived:SIGINTReceived:SIGINTReceived:SIGINT在容器退出时的优雅处理k8s容器服务升级过程中需要关闭过期的Pod时,会发出SIGTERM信号被发送到容器的主进程(PID1),并留出30s做善后处理。如果30s后容器还没有退出,那么k8s会继续发送SIGKILL信号。若是白灵古帝给了你死,我便教你为人正派。其实不只是容器,CI中的脚本也需要优雅的处理进程的退出。当收到SIGTERM/SIGINT信号时,留出一分钟时间做未完成的事情。asyncfunctiongracefulClose(signal){awaitnewPromise(resolve=>{setTimout(resolve,60000)})process.exit()}process.on('SIGINT',gracefulClose)process.on('SIGTERM',gracefulClose)这是为脚本时间是更正确的做法,但是如果一个服务有源源不断的请求呢?然后让服务主动关闭,调用server.close()结束服务constserver=http.createServer(handler)functiongracefulClose(signal){server.close(()=>{process.exit()})}process.on('SIGINT',gracefulClose)process.on('SIGTERM',gracefulClose)总结当进程结束的退出码非零时,系统会认为进程执行失败。可以在终端通过echo$?查看上一个进程的退出码。在Node中Promise.reject时,exitcode为0。在Node中,可以通过process.exitCode=1显式设置exitcode。在Node12+中,可以通过node--unhandled-rejections=stricterror.js执行脚本,将Promise.reject的退出码视为1,在Node15中修复该问题。当Node进程退出时,需要优雅的退出k8s。关闭POD时,先发送SIGTERM信号,留30s处理未完成的业务,如果POD没有正常退出,30s后发送SIGKILL信号。参考[1]附录E.具有特殊含义的退出代码:http://www.tldp.org/LDP/abs/html/exitcodes.html