Node.js 为什么进程没有 exit?

论坛 期权论坛 期权     
Node地下铁   2019-7-8 00:32   2186   0
不知道各位 noder 们有没有碰到过这样一个疑问,当你写的 Node.js 代码是异步逻辑的时候,我们要怎么才能知道 Node.js 进程是什么什么时候才会退出呢?Node.js 又是怎么知道异步执行结束了?或者当你执行一段 Node.js 代码之后,进程去一直等在这里没有退出又是怎么回事呢?
  1. setTimeout(() => {   console.log('hello');}, 10000);
复制代码
比如我们看到以上的代码,会发现这个进程会等待 10s 之后才会输出一个 hello 然后退出。

实际上Node.js 会注意追踪所有异步请求的进展,当我们使用文件异步读写、socket 读写、定时器等异步操作时,所有的异步请求都会维持在 Node.js 的事件队列中。这里有很多常见的异步请求:

  • http 请求、数据库请求等 IO 请求操作
  • net.Server.listen() 或者 http.Server.listen() 等端口监听
  • fs.write() 类型的文件 IO 操作
  • console.log() 输出日志
  • setTimeout()、setInterval() 等定时器操作
  • process.send() 等异步请求发送

等等,只有当所有的这些异步操作都结束的时候,Node.js 的进程才会退出。如果不了解这个情况的话,可能用户会直接使用 process.exit() 来退出进程,这个方式过于简单粗暴在某些边界情况下可能会造成一些麻烦或者损失。

实际上,随着 Node.js 在国内各个大厂的日渐纯熟的运用下,Node.js 不可能避免的也要接入各个运维体系之中。而在运维体系下,有很多常见的操作,比如单节点的 start、stop、restart。而这中间的 stop 和 restart 操作过程中,Node.js 进程的退出实际上是需要像传统服务端的运维方案靠近的。

常见的是 signal 的方式(比如大家熟悉的 kill -9)来对进程进行运维操作。但本文要讨论并不是 kill -9 这样比较粗暴的退出方式,而是运维过程中更常见的 kill -15 (软退出),这种情况下不论一个进程是由什么语言都应该注意需要处理和关闭好各项资源以及请求然后来优雅的退出进程。

优雅退出主要针对的是:

  • 此时进程不应该继续对外提供服务了,比如 Node.js 中的 http, net 等 listen 状态的 server 应该 close 了,否则此时有请求进来,可能执行到一半进程就直接 exit 导致提供了不可用的服务。
  • 有些数据库的锁、共享内存等公共资源需要释放。如果没有注意释放可能会有一些未期望/未定义的边缘 case 出现。
  • 常规的运维过程中输出各项自检/调试的日志(直接 process.exit() 可能啥记录都没有了)

了解了一些运维场景下,对进程退出的一些要求之后,我们最后再来看一个情况,也就是说如果你想主动的优雅的,close 掉各项 server 或者回收各项资源的情况下,为什么 Node.js 进程没有自然而然的退出掉?

上文中,我们举得例子十分简单,但实际项目中可能存在着大量的异步逻辑,某项漏掉的项可能会有一些没有还没结束的异步请求是我们需要去等待,不要粗暴退出的,而另外某些有些没有意义的定时器则确实可以直接忽略,在这样的复杂情况下我们如果去排查到底有哪些异步请求还在 pending,哪些又是业务可以忽略的的异步呢?

这里笔者推荐大家两个办法,一个是通过 Node.js 内置的两个方法去获取正在 pending 进程的一些信息:
  1. process._getActiveHandles()process._getActiveRequests()
复制代码
这个是原生支持的检查方法,大家可以在 Node.js 官方的 issue 中看到相关的讨论(https://github.com/nodejs/node-v0.x-archive/issues/1025#issuecomment-10672235)。

不过这个方法获取的日志不是那么直观,这里不做太多介绍。与之相对的是,另外一个推荐方案,使用一个可以直观检查 “为什么 Node.js 还在运行” 的库来专门检查一下:

  • 运行
    1. npm install -D why-is-node-running
    复制代码
    来安装这个依赖。
  • 添加
    1. const analysisLog = require('why-is-node-running');
    复制代码
    到你的代码入口。
  • 最后在你结束所有关闭操作,但是进程还没有推出的时候运行:
  1. afterAll(async () => {
复制代码
  1.   analysisLog();
复制代码
  1. }
复制代码

通过以上方式,你可以获得一个详细的追踪信息,里面会列出所有出于 pending 状态的异步操作的代码位置,例如:
  1. There are 5 handle(s) keeping the process running
复制代码
  1. # Timeout
复制代码
  1. /xxx/node_modules/why-is-node-running/example.js:6  - setInterval(function () {}, 1000)
复制代码
  1. /xxx/node_modules/why-is-node-running/example.js:10 - createServer()
复制代码
  1. [/code]
  2. [code]# TCPSERVERWRAP
复制代码
  1. /xxx/node_modules/why-is-node-running/example.js:7  - server.listen(0)
复制代码
  1. /xxx/node_modules/why-is-node-running/example.js:10 - createServer()
复制代码


通过这些信息,你可以排查到有哪些异步操作/请求是你准备优雅退出时还没有处理,从而导致你的进程没有自然退出的。

不过需要注意的是,这个库的实现原理,是通过 Node.js 8.x 中引入的 async hooks 这个新特性注册了全局的异步监听器,把所有的异步请求的类型都记录过异步汇总整理的,所以仅建议在开发和调试环境使用,不推荐在线上环境使用这个工具来排查。

小结

  • Node.js 的进程退出会等待异步处理完成
  • 常见的运维过程中会碰到需要进程优雅退出的场景,而 Node.js 自然退出是最好的,process.exit 是比较粗暴的
  • Node.js 开发者可以使用排查工具来排查哪些因素阻碍了进程自然退出。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:25
帖子:5
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP