我们封装在diat这个工具来针对生产环境提供更丰富的调试能力:
https://github.com/bytedance/diat/github.comNode.js中的worker_threads模块允许我们在Node.js中开启线程(或者说是Worker),这让我们可以更加充分地利用多核cpu。但我们要怎么调试Node.js中的线程呢?
从Node.js社区的issue和文档来看,目前似乎还没有比较官方的调试手段。不过因为用worker_threads开启的线程拥有自己的V8实例和uv loop,所以我们可以直接在线程里面调用inspector模块开启inspector server进行调试,比如执行下面一段代码,会让inspector server监听127.0.0.1:9230地址:
const
之后便可利用工具通过该地址进行调试。
但这种方式需要事先准备代码,与其他一些手段相比并不是很方便。熟悉Node.js inspector的开发者应该知道我们可以用--inspect这一命令行选项、SIGUSR1信号、以及node inspect -p $PID打开进程(也就是主线程)的inspector server,从而进行调试。那我们有没有办法更方便地调试一个运行中的线程呢?
好在Node.js中有内置inspector协议,可以让我们在主线程中与子线程中的V8实例进行通信(是的,除了V8 inspector中定义的用于Node.js中的协议外,Node.js内部也有自定义一些inspector协议),具体可见:
https://github.com/nodejs/node/blob/accc984ca93a9d002cc61f5960fcfe5902622cb4/src/inspector/node_protocol.pdl#L41github.com我们可以通过sendMessageToWorker向Worker中的inspector发送消息:
# Sends protocol message over session with given id.
command sendMessageToWorker
parameters
string message
# Identifier of the session.
SessionID sessionId
然后再通过绑定receivedMessageFromWorker事件接收来自worker中的消息:
# Notifies about a new protocol message received from the session
# (session ID is provided in attachedToWorker notification).
event receivedMessageFromWorker
parameters
# Identifier of a session which sends a message.
SessionID sessionId
string message
这也就意味着我们可以直接在主线程中与Worker的inspector进行通信,从而进行调试。这实际上也是ndb能调试worker的原理。但ndb没办法进行远程调试,并且要用ndb的话,我们需要用ndb命令启动我们的进程(试想一下假如我们已经用pm2或其他daemon工具开启的)。如果问题发生在线上环境中的话想用ndb会显得更加困难。
如果我们能与Worker中的inspector通信了,那我们可以通过Runtime.evaluate直接让Worker开启inspector server。这样后续对Worker的调试方式就和主线程调试的流程一样了,因为对于调试工具来说,它们都是独立的inspector。并且通过这种方式我们也可以充分利用现有的工具与经验(比如我觉得最便利的Chrome Devtools),也不需要重新学习一种debugger工具。
再梳理一下这个流程,我们要做的事情是:
当然实际的处理会稍微复杂些,比如进程中可能有多个线程,所以我们需要选择其中一个线程。这里直接忽略实现细节,来看我们在diat中做法。
找到目标进程的PID以后执行:
$PID
然后选择要调试的Worker:
? 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]
成功以后就能拿到Worker中inspector server监听的地址了 :
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
拿到inspector server的地址之后,后面就是用调试工具接入了。如果我们没法通过外网访问到inspector server,或者是不想用让inspector server被外网访问到,那我们可以通过命令行的repl来进行调试(实际上diat是内置了一个拥有更多命令的node-inspect):
$PID -r
不过目前worker_threads对inspector的支持并不完整,所以开启的inspector服务无法关闭 。不过diat默认让inspector server监听的是127.0.0.1地址,所以diat退出后外网也就无法访问,还是比较安全的。
通过这种方式,我们应该可以比较方便地调试线上常驻的Node.js线程了。
既然node-inspect也支持通过 node-inspect -p $PID 调试一个Node.js进程(包括手动给进程发送SIGUSR1命令的方式),并且node-inspect还内置在了Node.js里面,即node inspect,甚至不需要安装。那为什么还要封装diat?
因为除了上面说到的调试worker_threads外,直接使用node-inspect还会碰到一些问题,比如:
我们在diat中优化了与inspector server的通信。对于这两个问题,所以diat在打开inspector server后会再做个tcp代理,监听0.0.0.0地址从而允许外部网络访问;而diat在退出后也会关闭一个进程 inspector server,这种方式相比于直接关闭进程会更加优雅一些。
另外diat中的node-inspect实际上是fork出代码后单独维护了一份,这样可以加入一些我们觉得有用的特性,同时不用优先考虑这个特性加到node-inspect(或者说Node.js)中是否合适。diat在node-inspect中加入了更多命令。
举个例子,我经常用logpoint来临时输出线程运行时的信息。logpoint是breakpoint的一种,只能用来输出代码执行到某个位置的一些信息到console中,但不会阻塞线程的运行。
在Chrome Devtools中添加logpoint的方式回想一下我们发现线上问题时排查问题的方式:我们通常要找到和问题有关的代码,并尽可能多的增加日志,然后重新部署应用,再通过日志的内容分析问题。logpoint本质上是对这个流程的简化:我们找到能帮助判断问题代码,然后添加logpoint,输出信息到console中,再通过console输出的结果分析问题。所以对于这类问题logpoint能很方便地帮我们缩短这个流程。
但node-inspect不支持设置logpoint,所以我们在diat中增加了setLogpoint和attachConsole两个命令。通过setLogpoint设置logpoint,再通过attachConsole输出进程中console的内容:
(
这样也就能在不阻塞线程的同时得到更多运行信息了。当然diat中改动不止这些,其他改动可见:
https://github.com/bytedance/diat/#diat%E7%9B%B8%E6%AF%94%E4%BA%8Enode-inspect%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88%E6%94%B9%E5%8A%A8github.com
如果有适合添加到node-inspect中的命令,我们也会积极地向node-inspect提交PR。
在线上用inspector是不是比较危险?我一开始担心的是使用inspector中的一些功能(比如直接修改运行中的代码,甚至不用重启进程..),可能会让我们用一种很特殊的途径篡改了代码逻辑。如果我们无意中利用这种手段制造了一些问题的话,这些问题可能会非常难以排查。
但如果线上环境真的出现了一些异常情况致使我们的服务不可用、或是出现严重的逻辑错误,特别是当你通过其他常规手段无法解决问题的时候,那相比于让问题持续存在不断产生损失,能用inspector定位问题的话我肯定会用。既然Node.js/V8有如此强大的调试能力,那至少我们也应该把它当成是一种备选工具准备好,在我们判断合适的场景下拿出来用。
这篇文章介绍了现阶段调试Node.js线程(worker_threads)的一种工具 diat,以及node-inspect对于线上环境调试的优点。同时也介绍了封装diat的原因及它和node-inspect之间的差别。也非常欢迎在Github与我们进行讨论或:https://github.com/bytedance/diat/issues/new