/diat

A CLI tool to help with diagnosing Node.js processes basing on inspector.

Primary LanguageJavaScriptMIT LicenseMIT

diat

npm npm npm test

[English Doc]

diat 是基于 inspector 模块(提供: cpuprofile, heapsnapshot, debug 等能力)用于协助 Node.js 进程进行问题诊断的 CLI 工具。可以将diat当成是具有更丰富特性的 node-inspect

索引

动机

在解决 Node.js 服务端应用中发生的问题的过程中,我们发现Node.js/V8 原生的 inspector 模块是解决各类问题最有效的工具(不考虑大量使用 addon 或排查其他底层 c/cpp 代码的情况),比如:用 cpuprofile 解决 cpu 使用率异常的问题;用 heapsnapshot 排查内存泄漏的问题等等;用Debugger 协议直接打 logpoint 甚至热更新代码来协助排查业务问题。

并且不少 Node.js 开发者都具有 web 开发的经验,也就是说开发者学习利用 Chrome Devtools 进行问题排查可能是成本的最低途径之一。

但在实践过程中仍然有一些问题困扰着我们,比如:

  • 有些线上问题偶发且难以追踪、复现,开启 inspector 重启应用后问题消失
  • 有些环境我们可以开启 inspector,但外网无法访问
  • 非业务性质的线上问题诊断本身是一个重要但低频的场景,相比之下要求各个业务线事先统一接入一套诊断工具的成本较高

因此我们期望 diat 针对线上问题诊断的场景,能作为一个开箱即用的工具,围绕 Node.js/V8 inspector 的能力缩短 V8 inspector 的使用成本。

node-inspect相比于其他工具有什么优点

相比于其他诊断工具,node-inspect 支持 node-inspect -p $PID 来对一个进程直接进行调试,这种模式的优点在于:

  • 开箱即用,无需应用事先接入。因为在使用时通常不需要重启进程,所以对于偶发或难以复现的问题排查会有所帮助。
  • 部分功能在进程的主线程阻塞时也可以工作,如V8 cpu profile。因为直接基于 inspector 协议,并且 Node.js 的 inspector server 是运行在独立的线程上。
  • 需要消耗额外资源的命令在工具退出后会关闭,从而尽可能少给进程带来额外的资源消耗。因此也更适合用在生产环境上。

而对于windows或是其他不能用 node-inspect -p $PID 的场合,则需要配合 --inspect 使用。

diat相比于node-inspect做了什么改动

diat 的代码本身就包含了一份改动过的 node-inspect 代码,通过 diat inspect -r 即可使用 node-inspect。相比于node-inspect,diat添加了更多功能和优化。一些与inspector server通信相关的优化包括:

  • 支持开启 worker_threads 的 inspector server 并进行调试。
  • 支持代理 inspector server 的服务到外部网络中,从而允许其他调试工具接入。
  • 退出后关闭 inspector server 释放9229端口,避免在有多个 Node.js 进程(或者说 V8 实例)的场景下 9229 端口被一个进程占用。

而基于node-inspect新增的特性则包括:

  • 除了生成 cpuprofile 和 heapsnapshot,还支持生成 heapprofile 和 heaptimeline 文件。
  • 支持 attachConsole 来输出主线程中 console 输出的内容。
  • 支持 setLogpoint() 来设置不会中断线程运行的 logpoint。logpoint 也是一种 breakpoint,所以可以通过 clearBreakpoint() 来清除。
  • 支持 getScripts() 返回scripts的数据(而不是打印出来),从而可以在通过js表达式筛选自己需要的脚本,用于进程的文件很多的情况。
  • 支持 source(scriptId) 返回一个js脚本完整的内容。

如果有适合放到 node-inspect 中的特性,我们会尝试提交PR到上游。

推荐的排查途径

  1. 如果环境允许外部网络访问,推荐直接开启 inspector server 并用 debugger 工具接入
  2. 否则再利用利用CLI提供的各类命令解决问题

安装

npm i diat -g && diat --help

Node.js版本支持

All Node.js LTS releases.

使用场景介绍

你可以用下面的命令开启一个 Node.js 进程用于测试:

node -e "console.log(process.pid); setInterval(() => {}, 1000)"

inspect

inspect命令用来打开一个进程的 inspector 用于直接调试。通常如果你能打开 inspector 并访问到,大部分问题都可以通过 inspector 协议上的功能解决。

diat inspect -p <PID>

成功后会返回如下信息:

inspector service is listening on: 0.0.0.0:56324
or open the uri below on your Chrome to debug: devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=10.90.39.11:56324/408b7bca-1000-4c1f-a91e-de44d460e5ae
press ctrl/meta+c to exit

当看到这样的信息后,你可以用调试工具接入56324端口,注意0.0.0.0需要换成可访问到的公网 ip。你也可以直接用 Chrome 打开后面的devtools://url 用 Chrome devtools 连接进程的 inspector。

inspect命令是通过发送 SIGUSR1 信号让 Node.js 内置的代码开启 inspector 端口,详情见文档。但出于安全考虑 Node.js 是让 inspector 监听127.0.0.1ip 地址,也就是外网无法访问。diat 在这基础之上做了个 tcp 代理让外网可以访问到进程 inspector,也就是存在被恶意访问 inspector 的风险。因此需要仔细斟酌你的使用场景是否适用

排查结束用ctrl/meta+c退出 diat 进程后,diat 会关闭业务进程中的 inspector。如果 diat 进程异常退出没能关闭进程的 inspector 的话,因为 inspector 默认监听的是127.0.0.1端口,一般风险也不大。

关闭inspector

你可以通过下列命令手动关闭一个端口上的inspector server,从而释放对应的端口:

diat inspectstop -a 127.0.0.1:9229

inspectworker

目前社区缺少对 worker_threads 开启的线程进行调试的支持(ndb支持)。inspectworker命令可以用来打开线程的 inspector 进行调试:

diat inspectworker -p <PID>

进程可以通过 worker_threads 打开多个线程,所以接入成功后首先要选择我们想要 inspect 的线程:

? Choose a worker to inspect (Use arrow keys)
❯ Worker 2(id: 1) [file:///diat/packages/diat/
__tests__/test_process/thread_worker.js]
  Worker 1(id: 2) [file:///diat/packages/diat/
__tests__/test_process/thread_worker.js]

选择相应的线程后,diat 会打开对应线程的 inspector,后续使用方式同inspect命令,可以参照inspect中的描述。

因为目前 Node.js 对 worker_threads 中的 inspector 的支持有所缺失(或者说未来 worker_threads 的调试方式不一定是以 inspector 为主),所以目前 diat 打开线程中的 inspector 后无法关闭。

repl

前面介绍了用 inspectinspectworker 打开 inspector 的方式,但在一些环境中我们并不能用外部 debugger 接入,比如:网络隔离的情况。这种情况下我们可以利用 -r 配置在命令行上进行调试,如:

diat inspect -p <PID> -r

成功后输入 help 查看 node-inspect 支持的命令。关于 node-inspect 的详细信息可以查看文档:

metric

metric命令用于查看进程占用的资源:

diat metric -p <PID>

开启后会展示 cpu、memory 和 uv 相关的一些基础数据:

[cpu] load(user): 0.00032 load(system): 0.000068
[memory] rss: 29.78MB heapTotal: 4.18MB heapUsed: 2.33MB external: 873.74KB
[uv] handle: 3, request: 0, latency: 5ms

数据每隔 2s 进行一次更新。

cpuprofile

cpuprofile命令用于让进程进行 cpu prfile,从而生成.cpuprofile 文件记录一段时间内 js 中的函数执行情况。cpu profile 可以帮助我们排查 cpu 使用率过高的问题,或是用于协助进行性能分析:

diat cpuprofile -p <PID>

当.cpuprofile 文件生成成功后,diat 会返回文件所在的位置:

profiling...
cpuprofile generated at: /diat_90504_1584018222518.cpuprofile

你可以在 Chrome Devtools 中的 Profiler 面板中打开.cpuprofile 文件进行分析。关于 cpu profile 的使用说明可以参考 Chrome Devtools 的官方文档

cpuprofile 支持配置如下参数:

  • --duration 表示采样的时间,默认为 5000ms。
  • --interval 表示采样间隔,默认为 1000us。采样间隔越小,则 cpuprofile 越准确,但需要进程额外消耗的资源越多。

cpuprofile 默认的文件格式是:./diat_$PID_$TS.cpuprofile,可通过--file改变指定生成文件的名称。

heapsnapshot

heapsnapshot命令用于生成.heapsnapshot 文件(堆快照)。heap snapshot 可以让我们了解进程中的内存占用细节,可以用来帮助我们排查内存泄漏问题:

diat heapsnapshot -p <PID>

你可以在 Chrome Devtools 中的 Memory 面板中打开.heapsnapshot 文件进行分析。关于 heap snapshot 的使用说明可以参考 Chrome Devtools 的官方文档

注意: 生成 heap snapshot 可能导致内存占用比较高的进程退出。因为没有指定参数的话,Node.js 进程在 64bit 机器上的 max-old-space-size 是 1.4GB 左右(Node.js 12 上的某个版本开始不再做这个默认的限制),而 heap snapshot 在生成的过程中会额外占用不少内存。此时继续增大内存占用会导致 V8 abort 或系统 OOM killer 关闭业务进程。对于这个问题暂时可能没有什么好的办法处理。

heapsnapshot 文件的默认格式是:./diat_$PID_$TS.heapsnapshot,可通过--file改变指定生成文件的名称。

其他V8内存相关的profile

除了 heapsnapshot,还可以直接通过diat生成 heapprofile 文件:

diat heapprofile -p <PID> -d 5000

和 heaptimeline 文件:

diat heaptimeline -p <PID> -d 5000

其中 heap profile 不会阻塞线程、对进程影响较小,而 heap timeline 则可以获取到生成对象所对应的代码。更多细节可查看官方文档

用perf生成火焰图

关于 perf + --perf-basic-prof 的用法也可以参考 diagnostics-flamegraph

cpu profile 对于排查 js 中与 cpu 相关的问题很有帮助。但是因为 cpu profile 是 V8 记录的 js 中的函数执行情况,所以对于 Node.js 底层代码中或 addon 代码中的函数调用情况,我们没办法通过 cpu profile 进行排查。如果发生这类问题我们需要 c/cpp 的 profile 进行排查。diat 对 Linux perf 方案提供额外的支持(可以参考node.js Flame Graphs on Linux)。

如果运行环境中已经安装了 perf 工具,则可以通过 perf 命令生成火焰图:

diat perf -p <PID>

最终会生成火焰图的svg文件(默认文件名称为: diat_perf.svg):

分步实现

diat perf 是对多个命令的封装,下面将分步介绍单个命令的使用方式。

首先通过perfbasicprof让 Node.js 进程生成.map 文件,.map 文件让 perf 能识别 js 的函数:

diat perfbasicprof -p <PID> -e true

接着让 perf 对进程进行 profile:

perf record -F 1000 -p <PID> -g -m 512B -- sleep 5

成功后我们会在当前文件下找到 perf.data 文件,文件中描述了这段时间内进程中的函数调用。用 perf 再次处理以获取可以直接读取的内容:

perf script > out.nodestacks01

操作结束后让 Node.js 停止生成.map 文件,减少资源消耗:

diat perfbasicprof -p <PID> -e false

如果我们想生成 Flame graph,可以perf2svg用做进一步处理生成 svg:

diat perf2svg -f out.nodestacks01

已知限制

1. 无法在 Windows 上直接传入 PID

因为 Windows 不支持给进程发送信号打开 inspector,所以也就没办法用-p选项传入 pid。可以考虑在启动 Node.js 时增加--inspect打开 inspector 并在 diat 的命令中用-a/--inspector_addr配置替代-p配置传入 inspector 的地址,比如:

node --inspect=9229 index.js

然后用 diat“打开”inspector(实际上做的事情只是在公共 ip 代理 inspector 服务):

diat inspect -a=127.0.0.1:9229

2. 9229 端口被占用后,无法通过 SIGUSR1 信号在默认的 9229 端口上打开 inspector

同样可以考虑在启动 Node.js 时增加--inspect=PORT指定一个可用的端口打开 inspector,并在 diat 的命令中用-a/--inspector_addr配置替代-p配置传入 inspector 的地址。

3. Node.js 8 版本中 inspector 的限制

Node.js 8 版本(目前已经退出 LTS)中的 inspector 有一些限制(这些问题不存在于 Node.js >= 10 的版本中),比如:

  1. 同一时间只能有一个inspector.Session接入

因为这个限制的存在,也就意味着如果已经打开并接入了进程的 inspector 端口,比如:用 Chrome Devtools 接入。那后续接入 inspector 的尝试都会失败,diat 也就没办法生效。而有些工具,比如pm2中的某些配置,比如:--max-memory-restart,也会打开进程的 inspector 并接入,所以这种情况下新的接入也会失败。

  1. 新版本的 node-inspect 使用了 Node.js 8 所不支持的api,如: require('url').fileURLToPath,会导致部分命令在 Node.js 8 中失败。

工作原理

基本工作原理

  1. 用 SIGUSR1 信号在 9229 端口上打开 inspector: https://nodejs.org/api/process.html#process_signal_events
  2. 利用 v8-inspector(node) 协议进行通信,可以执行对应的 inspector 功能,包括执行一段指定的代码: https://chromedevtools.github.io/devtools-protocol/v8/HeapProfiler/

inspectworker 的工作原理

除了 v8-inspector(node) 的协议外,Node.js 内部还有定义一些协议用于 trace 和 worker_threads 等功能,定义见:https://github.com/nodejs/node/blob/master/src/inspector/node_protocol.pdl

这些协议中包括和线程中的 inspector 进行通信的部分。diat 通过该协议让线程打开 inspector,从而允许外部接入。

在Electron上使用

你可以对 Electron 的 Node.js 进程使用 diat。因为工作原理对 Node.js 进程是通用的,所以理论上 diat 对于这类应用都是生效的。

Contributing

项目使用 lerna 进行管理,git clone 项目后进行安装:

cd diat && npm install

packages 文件夹下的 linux-perf、node-inspect 和 stackvis-simplified 是对社区里面的项目进行了些改造的代码。diat 自身的代码主要在:

  • packages/diat:命令行工具的主要代码
  • packages/live-inspector:处理与 inspector 通信

提交代码前需要确保测试通过,并在 commit message 中描述对应的改动。测试可通过npm run test执行。

已知问题:目前因为 jest 在检测 worker_threads 开启的线程上似乎有些问题,可能导致测试无法自动退出。

License

MIT