why-is-node-running

Published: · LastMod: July 20, 2024 · 981 words

why-is-node-running 为什么一个Node.js进程没有退出 🔗

why-is-node-running (Github) 用于诊断为什么一个Node.js进程没有退出。当你运行一个Node.js应用并且没有及时退出时,这个工具可以找出原因。

demo.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function startServer () {
  const server = createServer()
  setInterval(() => {}, 1000)
  server.listen(0)
}

startServer()
startServer()

// logs out active handles that are keeping node running
setImmediate(() => whyIsNodeRunning())

结果输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ node example.js 
TCPSERVERWRAP created
TCPSERVERWRAP created
There are 6 handle(s) keeping the process running.

# Timeout
example.js:6  - setInterval(() => {}, 1000)
example.js:10 - startServer()

# TTYWRAP
index.js:20 - console.info(`TCPSERVERWRAP created`);

# TTYWRAP
(unknown stack trace)

# TCPSERVERWRAP
example.js:7  - server.listen(0)
example.js:10 - startServer()

# Timeout
example.js:6  - setInterval(() => {}, 1000)
example.js:11 - startServer()

# TCPSERVERWRAP
example.js:7  - server.listen(0)
example.js:11 - startServer()

安装 🔗

1
npm i -D why-is-node-running

使用 🔗

命令行使用 🔗

1
why-is-node-running jsfile.js

或者使用node的--import属性提前预加载

1
node --import why-is-node-running/include /path/to/some/file.js

源码 🔗

createHook 🔗

async_hooks是Node.js提供的一种机制,用于监控异步资源的生命周期,在异步操作开始和结束时执行回调,这在调试、性能分析和资源管理等方面非常有用。

async_hooks通过统一的接口,使开发者能够注册回调函数,这些回调函数会在异步操作的不同阶段被调用,如初始化(init)、前向(before)、后向(after)、承诺解析(promiseResolve)和销毁(destroy)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const async_hooks = require('async_hooks');

const hook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    console.log(`Async operation started: ${type}`);
  },
  before(asyncId) {
    console.log(`Before async operation with id: ${asyncId}`);
  },
  after(asyncId) {
    console.log(`After async operation with id: ${asyncId}`);
  },
  destroy(asyncId) {
    console.log(`Async operation destroyed: ${asyncId}`);
  }
});

// 启用hook
hook.enable();

当异步操作发生时,回调会被调用,提供有关操作的信息,如类型、触发它的异步ID以及资源本身

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
init (asyncId, type, triggerAsyncId, resource) {
    if (IGNORED_TYPES.includes(type)) {
      return
    }
  
    const stacks = captureStackTraces().slice(1)
    asyncResources.set(asyncId, {
      type,
      resource,
      stacks
    })
},
destroy (asyncId) {
    asyncResources.delete(asyncId)
}

源码中忽略了以下类型,通过一个Map搜集堆栈信息

1
2
3
4
5
6
const IGNORED_TYPES = [
  'TIMERWRAP',
  'PROMISE',
  'PerformanceObserver',
  'RANDOMBYTESREQUEST'
]

这里重写了获取堆栈信息的方法,通过v8内置方法代理把堆栈对象输出 用于捕获当前JavaScript执行环境的调用栈轨迹。它通过暂时替换Error.prepareStackTrace方法,使用Error.captureStackTrace方法为目标对象target生成调用栈轨迹,最后恢复原先的Error.prepareStackTrace方法。返回值是捕获到的调用栈轨迹。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// See: https://v8.dev/docs/stack-trace-api
function captureStackTraces () {
  const target = {}
  const original = Error.prepareStackTrace

  Error.prepareStackTrace = (error, stackTraces) => stackTraces
  Error.captureStackTrace(target, captureStackTraces)

  const capturedTraces = target.stack
  Error.prepareStackTrace = original
  return capturedTraces
}
  • 运行之前先关闭
  • 打印堆栈信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function whyIsNodeRunning (logger = console) {
  hook.disable()

  const activeAsyncResources = Array.from(asyncResources.values())
    .filter(({ resource }) => resource.hasRef?.() ?? true)

  logger.error(`There are ${activeAsyncResources.length} handle(s) keeping the process running.`)

  for (const asyncResource of activeAsyncResources) {
    printStacks(asyncResource, logger)
  }
}