Node.js 进程平滑离场剖析
使用 Node.js 搭建 HTTP Server 已是司空见惯的事。在生产环境中,Node 进程平滑重启直接关系到服务的可靠性,它的重要性不容我们忽视。既然是平滑重启,就涉及到新旧进程的接替过渡:
- 首先,保证新进程平滑入场
- 其次,保证旧进程平滑离场
本文主要谈论下,在新旧进程接替过渡期间,如何保证旧进程平滑离场。那怎样的离场才算平滑的呢?
如何定义平滑离场
以进程离场作为时间分割点,我们可以把请求分为两类:增量请求
和存量请求
。
- 在进程离场前,停止接收新的(
增量
)请求 - 在进程离场前,保证未完成的(
存量
)请求正常响应
所以,达成以上两个目标,基本上我们就认为进程的离场是平滑的。在谈如何做到进程平滑离场前,我们需要一种机制,这种机制能让我们主动通知进程何时离场,这就涉及到进程间通信(IPC)的知识了,我们先简单了解下。
进程间通信
对 Unix 或类 Unix 系统而言,进程间通信的方式有很多种 —— 信号(Signal)是其中的一种。
信号的种类有很多,如 SIGINT
、 SIGTERM
及 SIGKILL
等。这些信号视具体需要用于不同的场景,比如 SIGKILL
一般用于强杀进程。
我们可以在命令行执行 kill -l
查看所有的信号,如下所示(其中的数字表示 signal number
):
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGEMT 8) SIGFPE 9) SIGKILL 10) SIGBUS 11) SIGSEGV 12) SIGSYS 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGURG 17) SIGSTOP 18) SIGTSTP 19) SIGCONT 20) SIGCHLD 21) SIGTTIN 22) SIGTTOU 23) SIGIO 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGINFO 30) SIGUSR1 31) SIGUSR2
我们可以使用 kill
命令向进程发送指定信号:
# 发送 SIGTERM 信号(默认,无须指定信号类型)给进程 $ kill <pid> # 发送 SIGINT 信号给进程,其中 <pid> 为具体的进程 ID $ kill -INT <pid> # 发送 SIGKILL 信号给进程 $ kill -KILL <pid> # 或者 $ kill -9 <pid>
进程可以对接收到的信号作出回应。对 Node 应用而言,信号是被当作事件发送给 Node 进程的,进程接收到 SIGTERM
及 SIGINT
事件有默认回调,官方文档是这么描述的:
'SIGTERM' and 'SIGINT' have default handlers on non-Windows platforms that reset the terminal mode before exiting with code 128 + signal number. If one of these signals has a listener installed, its default behavior will be removed (Node.js will no longer exit).
这句话写的很抽象,它是什么意思呢?我们以一个简单的 Node 应用为例。
新建文件,键入如下代码,将其保存为 server.js
:
const http = require('http'); const server = http.createServer((req, res) => { setTimeout(() => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('It works'); }, 5000); }); server.listen(9420);
这里为了方便测试,对应用接收到的每个 http 请求,等待 5 秒后再进行响应。
执行 node server.js
启动应用。为了给应用发送信号,我们需要获取应用的进程 ID,我们可以使用 lsof
命令查看:
$ lsof -i TCP:9420 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME node 70826 myunlessor 13u IPv6 0xd250033eef8912eb 0t0 TCP *:9420 (LISTEN)
事实上,我们也可以在代码里通过
console.log(process.pid)
获取进程 ID。这里只是顺便介绍一种,在知道监听 TCP 端口的情况获取进程的方式。
随后,我们发起一个请求,在收到响应之前(有 5 秒等待时间),我们给应用发送 SIGINT
信号。
$ curl http://localhost:9420 & $ kill -INT 70826 curl: (52) Empty reply from server [1]+ Exit 52 curl http://localhost:9420
可以看到,请求没能正常收到响应。也就是说,默认情况下,Node 应用在接收到 SIGINT
信号时,会马上把进程杀死,无视进程还没处理完成的请求。所幸的是,我们可以手动监听进程的 SIGINT
事件,像这样:
process.on('SIGINT', () => { // do something here });
如果我们在事件回调里什么都不做,就意味着忽略该信号,进程该干嘛干嘛,像什么事情都没发生一样。
那么,如果我手动监听 SIGKILL
会如何呢?对不起,SIGKILL
是不能被监听的,官方文档如是说:
'SIGKILL' cannot have a listener installed, it will unconditionally terminate Node.js on all platforms.
这是合情合理的,要知道 SIGKILL
是用于强杀进程的,你无法干预它的行为。
回到上面的问题,我们可以近似地理解为 Node 应用响应 SIGINT
事件的默认回调是这样子的:
process.on('SIGINT', () => { process.exit(128 + 2/* signal number */); });
我们可以打印 exit code
来验证:
$ node server.js $ echo $? 130
有了信号,我们就能主动通知进程何时离场了,下面谈一谈进程如何平滑离场。
如何让进程平滑离场
我们在上面示例基础上,也就是在文件 server.js
中,补充如下代码:
process.on('SIGINT', () => { server.close(err => { process.exit(err ? 1 : 0); }); });
这段代码很简单,我们改写应用接收到 SIGINT
事件的默认行为,不再简单粗暴直接杀死进程,而是在 server.close
方法回调中再调用 process.exit
方法,接着继续试验一下。
$ lsof -i TCP:9420 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME node 75842 myunlessor 13u IPv6 0xd250033ec7c9362b 0t0 TCP *:9420 (LISTEN) $ curl http://localhost:9420 & [1] 75878 $ kill -2 75842 $ It works [1]+ Done curl http://localhost:9420
可以看到,应用在退出前(即进程离场前),成功地响应了存量
请求。
我们还可以验证,进程离场前,确实不再接收增量
请求:
$ curl http://127.0.0.1:9420 curl: (7) Failed to connect to 127.0.0.1 port 9420: Connection refused
这正是 server.close
所做的事,进程平滑离场就是这么简单,官方文档是这么描述这个 API 的:
Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the server is finally closed when all connections are ended and the server emits a 'close' event. The optional callback will be called once the 'close' event occurs. Unlike that event, it will be called with an Error as its only argument if the server was not open when it was closed.
结束语
进程平滑离场只是 Node 进程平滑重启的一部分。生产环境中,新旧进程的接替涉及进程负载均衡、进程生命周期管理等方方面面的考虑。专业的工具做专业的事,PM2 就是 Node 进程管理很好的选择。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
那些年我们一起踩过的Dubbo坑
前言 微服务架构在如今的9102年已经不是什么新鲜的话题了,但是怎么做好微服务架构,却又是一个永恒的话题。比如服务粒度的划分,怎么控制好粗细?服务划分后,对于项目的部署会有什么改变?... 这会是一个很大的话题,以后可以分开篇章探讨一翻,但是我们本篇并不打算聊这个,而是讨论一下具体的实现技术--dubbo。 dubbo历史 2011 年末,阿里巴巴在 GitHub 上开源了基于 Java 的分布式服务治理框架 Dubbo,之后它成为了国内该类开源项目的佼佼者,许多开发者对其表示青睐。同时,先后有不少公司在实践中基于 Dubbo 进行分布式系统架构,目前在 GitHub 上,它的 fork、star 数均已破万。2014 年 10 月 30 号发布版本 dubbo-2.4.11,修复了一个小 Bug,版本又陷入漫长的停滞到2017年九月份。 在dubbo停滞的期间呢,当当网 Fork 了阿里的一个 Dubbo 版本开始维护,并命名为 dubbox-2.8.0。值得注意的是,当当网扩展 Dubbo 服务框架支持 REST 风格远程调用,并且跟随着 ZooKeepe 和 Spring ...
- 下一篇
JVM虚拟机栈——JAVA方法的消亡史
引子 这是由一个“无聊”的问题引发的故事:方法ipp和ppi分别会打印什么结果? public class Opcode { public static void main(String[] args) { System.out.println("hello wang ni ma"); } public void ipp(){ int i = 0; i = i++; System.out.println(i); } public void ppi(){ int i = 0; i = ++i; System.out.println(i); } } 当然了,把两个方法放在一起,凭借些许的逻辑思维分析,可以很快给出答案: 0 1 那JVM为什么会执行出这样的结果呢,本文将结合 字节码 和 虚拟机栈 做出解释。 番外 javap 反汇编器 javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。通过它,我们可以对照源代码和字节码,从而了解很多编译器内部的工作。 java字节码指令集 Java 程序编译之后就变成了一条条字节码指令,其形式类似汇编,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7设置SWAP分区,小内存服务器的救世主
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Hadoop3单机部署,实现最简伪集群
- Mario游戏-低调大师作品
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Red5直播服务器,属于Java语言的直播服务器