这段时间思考了一个问题,怎么用pm2实现平滑更新(即更新不会影响影响线上服务)?
起因:经常做功能迭代就会发现一个很麻烦的事情,每次需要发布新版本的时候必须得找网关的人配合切流量,然后再一台台更新。如果只有一两台主机还好,当主机数量多了以后会发现这是一个很痛苦的事。
发展:本着能自己动手绝不BB得原则搜了一下能不能自己实现平滑更新,到网上找了一圈,没有发现很成熟的技术文章。所以最后自己研究了一下pm2集群的原理及相关的问题自己找出了一种可以实现我的需求的方式。本着独乐乐不如众乐乐的想法写下这篇文章,希望能帮到更多的人。
何谓平滑更新?我自己的定义是在更新时不需要停止服务,不会影响现有连接,在前端无感知的情况下完成新版本的发布。我们这次讨论的平滑更新区别于java等语言里的热更新,他们的热更新是通过在运行的过程中重新加载新代码实现的,整个过程服务器不需要重启。而我们讨论的是服务器需要重启的情况。请看官们区别对待。
大家都知道pm2中对重启服务的命令有三种,分别是“restart”、“reload”、“gracefulReload”,下面我们就重点分析一下这三种的区别与我们最后的选择。
备注:往下我们所有实验都是建立在pm2使用cluster模式的前提下,fork模式不适用。具体原因又是另外一个故事,以后有时间单开一文详解。
restart是直接将server kill掉然后重新启动一个server。这其中在重新的这一段时间内,server是有一个空窗期的。这个时间内所有的request都会被内核拒绝,因为根本没有它们需要找的server存在。
我使用postman做循环请求,你会发现当服务重启时,postman发出的请求会失败很多次。因为这些请求在发起的时候都因为找不到server而被拒绝访问(从抓包来看就是RST的请求重置报文)。

然后我们来看reload,reload命令在pm2官网的解释是0秒重启,说它是在一个个重启“With reload, pm2 restarts all processes one by one, always keeping at least one process running”(原文连接:https://pm2.io/doc/en/runtime/guide/load-balancing/?utm_source=pm2&utm_medium=website&utm_campaign=rebranding)。
在postman循环访问时使用reload重启服务。发现只有一个请求出错,其它请求全部正常。如图:
此处,按照pm2官方的解释及我的个人实验观察。reload命令会先创建一个server,然后将原server kill掉,此时原server上的正在执行的request因为进程被关闭而失败。而postman后续的request进来时,因为新server已经启动了。所以不存在空窗期,所以后续的request不会报错。
看到此处,其实你会发现reload命令已经足够满足很多人的需求了。但是reload还是有着很大的缺点的。那就是如果用户的请求正在处理,则这一些请求就会失败,虽然这些情况的失败请求不会太多,但是如果你的服务要求很严谨,这一类情况也应该避免。
此时你就该使用gracefulReload了,这个命令我就不多做赘述了,使用它,你可以在你的进程里监听SIGINT事件,然后调用server.close函数,设置server不再接收新的连接。而等待现有的连接全部关闭后出发server.close事件。我们这这个事件中执行process.exit()退出进程。这样用户的请求就不会因为进程被kill而失败了。
process.on('SIGINT', () => {
server.close(()=>{
process.exit(0);
})});
是不是感觉很理想?在这么设置之后我使用ab做请求测试发现一个fail也没有,完美。
但是,当我使用postman再测的时候发现还是会有一个fail,为什么呢?在一番查探后我发现,postman发起的请求会默认使用keepalive,这样,虽然你设置的等待所有请求处理完成退出可以保证当你启动重启命令时server上的请求能完成,但是因为是长连接,后续的请求不会分发到新的server,而是会继续请求到旧的server。这样,server.close事件将永远不会触发,因为一直有keppalive的请求进来。所以永远有连接。
是问题总会有解决方法的,最后我发现,这种情况的fail是因为keepalive引起的,那我是不是可以在keepalive上做手脚?不允许用户使用keepalive当然是不现实的。如果我从server上关闭keepalive呢?
于是我发现有两种方法可以满足我的需求:
1:server.keepAliveTimeout=1;
设置server的keepalive超时时间为1毫秒,在接收到SIGINT信号后设置server的keepalive超时时间为1毫秒,也就是说旧server所有现有的连接,在处理完成后都会因为server的keepalive超时时间而被server主动关闭。这样,就算客户端还有连续的请求也只会发送到新的server,而不会发送到旧的server随着进程被关闭而失败。
2:还有一种是设置请求的response的header keepalive=close,这样来关闭keepalive。也能实现。
所以最后的终极方案如下:
process.on('SIGINT', () => {
server.keepAliveTimeout=1;
server.close(()=>{
process.exit(0);
})
});
在进程接收到关闭信号后,设置server的keepalive超时为1毫秒,然后调用监听server.close事件,等所有连接都被关闭后。再退出进程。
但是还是存在一个问题,就是pm2的重启有一个kill_timeout的参数,它设置了关闭一个进程的超时时间。也就是说如果再固定时间内进程没有关闭,会被pm2强制kill。 所以,如果你kill_timeout设置的是3秒。而用户的请求在server中处理的时间超过3秒。就会因为pm2的kill超时而被强制kill。这样的请求也还会是表现为失败。
还好我们可以自己估计自己的请求最大时间长度,然后自己配置kill_timeout就可以了。
好了,至此利用pm2实现平滑更新的方案就初步实现了。当然,这是我个人调试测试发现的结果,如果屏幕前的你发现某些我未知的bug欢迎联系我。谢谢。