Node.js 进程平滑离场剖析小结
使用Node.js搭建HTTPServer已是司空见惯的事。在生产环境中,Node进程平滑重启直接关系到服务的可靠性,它的重要性不容我们忽视。既然是平滑重启,就涉及到新旧进程的接替过渡:
- 首先,保证新进程平滑入场
- 其次,保证旧进程平滑离场
本文主要谈论下,在新旧进程接替过渡期间,如何保证旧进程平滑离场。那怎样的离场才算平滑的呢?
如何定义平滑离场
以进程离场作为时间分割点,我们可以把请求分为两类:增量请求和存量请求。
- 在进程离场前,停止接收新的(增量)请求
- 在进程离场前,保证未完成的(存量)请求正常响应
所以,达成以上两个目标,基本上我们就认为进程的离场是平滑的。在谈如何做到进程平滑离场前,我们需要一种机制,这种机制能让我们主动通知进程何时离场,这就涉及到进程间通信(IPC)的知识了,我们先简单了解下。
进程间通信
对Unix或类Unix系统而言,进程间通信的方式有很多种——信号(Signal)是其中的一种。
信号的种类有很多,如SIGINT、SIGTERM及SIGKILL等。这些信号视具体需要用于不同的场景,比如SIGKILL一般用于强杀进程。
我们可以在命令行执行kill-l查看所有的信号,如下所示(其中的数字表示signalnumber):
$kill-l 1)SIGHUP2)SIGINT3)SIGQUIT4)SIGILL 5)SIGTRAP6)SIGABRT7)SIGEMT8)SIGFPE 9)SIGKILL10)SIGBUS11)SIGSEGV12)SIGSYS 13)SIGPIPE14)SIGALRM15)SIGTERM16)SIGURG 17)SIGSTOP18)SIGTSTP19)SIGCONT20)SIGCHLD 21)SIGTTIN22)SIGTTOU23)SIGIO24)SIGXCPU 25)SIGXFSZ26)SIGVTALRM27)SIGPROF28)SIGWINCH 29)SIGINFO30)SIGUSR131)SIGUSR2
我们可以使用kill命令向进程发送指定信号:
#发送SIGTERM信号(默认,无须指定信号类型)给进程 $kill#发送SIGINT信号给进程,其中 为具体的进程ID $kill-INT #发送SIGKILL信号给进程 $kill-KILL #或者 $kill-9
进程可以对接收到的信号作出回应。对Node应用而言,信号是被当作事件发送给Node进程的,进程接收到SIGTERM及SIGINT事件有默认回调,官方文档是这么描述的:
'SIGTERM'and'SIGINT'havedefaulthandlersonnon-Windowsplatformsthatresettheterminalmodebeforeexitingwithcode128+signalnumber.Ifoneofthesesignalshasalistenerinstalled,itsdefaultbehaviorwillberemoved(Node.jswillnolongerexit).
这句话写的很抽象,它是什么意思呢?我们以一个简单的Node应用为例。
新建文件,键入如下代码,将其保存为server.js:
consthttp=require('http'); constserver=http.createServer((req,res)=>{ setTimeout(()=>{ res.writeHead(200,{'Content-Type':'text/plain'}); res.end('Itworks'); },5000); }); server.listen(9420);
这里为了方便测试,对应用接收到的每个http请求,等待5秒后再进行响应。
执行nodeserver.js启动应用。为了给应用发送信号,我们需要获取应用的进程ID,我们可以使用lsof命令查看:
$lsof-iTCP:9420 COMMANDPIDUSERFDTYPEDEVICESIZE/OFFNODENAME node70826myunlessor13uIPv60xd250033eef8912eb0t0TCP*:9420(LISTEN)
事实上,我们也可以在代码里通过console.log(process.pid)获取进程ID。这里只是顺便介绍一种,在知道监听TCP端口的情况获取进程的方式。
随后,我们发起一个请求,在收到响应之前(有5秒等待时间),我们给应用发送SIGINT信号。
$curlhttp://localhost:9420& $kill-INT70826 curl:(52)Emptyreplyfromserver [1]+Exit52curlhttp://localhost:9420
可以看到,请求没能正常收到响应。也就是说,默认情况下,Node应用在接收到SIGINT信号时,会马上把进程杀死,无视进程还没处理完成的请求。所幸的是,我们可以手动监听进程的SIGINT事件,像这样:
process.on('SIGINT',()=>{ //dosomethinghere });
如果我们在事件回调里什么都不做,就意味着忽略该信号,进程该干嘛干嘛,像什么事情都没发生一样。
那么,如果我手动监听SIGKILL会如何呢?对不起,SIGKILL是不能被监听的,官方文档如是说:
'SIGKILL'cannothavealistenerinstalled,itwillunconditionallyterminateNode.jsonallplatforms.
这是合情合理的,要知道SIGKILL是用于强杀进程的,你无法干预它的行为。
回到上面的问题,我们可以近似地理解为Node应用响应SIGINT事件的默认回调是这样子的:
process.on('SIGINT',()=>{ process.exit(128+2/*signalnumber*/); });
我们可以打印exitcode来验证:
$nodeserver.js $echo$? 130
有了信号,我们就能主动通知进程何时离场了,下面谈一谈进程如何平滑离场。
如何让进程平滑离场
我们在上面示例基础上,也就是在文件server.js中,补充如下代码:
process.on('SIGINT',()=>{ server.close(err=>{ process.exit(err?1:0); }); });
这段代码很简单,我们改写应用接收到SIGINT事件的默认行为,不再简单粗暴直接杀死进程,而是在server.close方法回调中再调用process.exit方法,接着继续试验一下。
$lsof-iTCP:9420 COMMANDPIDUSERFDTYPEDEVICESIZE/OFFNODENAME node75842myunlessor13uIPv60xd250033ec7c9362b0t0TCP*:9420(LISTEN) $curlhttp://localhost:9420& [1]75878 $kill-275842 $Itworks [1]+Donecurlhttp://localhost:9420
可以看到,应用在退出前(即进程离场前),成功地响应了存量请求。
我们还可以验证,进程离场前,确实不再接收增量请求:
$curlhttp://127.0.0.1:9420 curl:(7)Failedtoconnectto127.0.0.1port9420:Connectionrefused
这正是server.close所做的事,进程平滑离场就是这么简单,官方文档是这么描述这个API的:
Stopstheserverfromacceptingnewconnectionsandkeepsexistingconnections.Thisfunctionisasynchronous,theserverisfinallyclosedwhenallconnectionsareendedandtheserveremitsa'close'event.Theoptionalcallbackwillbecalledoncethe'close'eventoccurs.Unlikethatevent,itwillbecalledwithanErrorasitsonlyargumentiftheserverwasnotopenwhenitwasclosed.
结束语
进程平滑离场只是Node进程平滑重启的一部分。生产环境中,新旧进程的接替涉及进程负载均衡、进程生命周期管理等方方面面的考虑。专业的工具做专业的事,PM2就是Node进程管理很好的选择。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。