js监听多个事件_如何调试Node.js中的线程 (worker_threads)

8f11554ca91b89360713be0b9a103b4b.png

我们封装在diat这个工具来针对生产环境提供更丰富的调试能力:

https://github.com/bytedance/diat/​github.com

如何调试Node.js中的线程?

Node.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#L41​github.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工具。

再梳理一下这个流程,我们要做的事情是:

  1. 用--inspect配置或SIGUSR1信号打开主线程的inspector
  2. 通过inspector与子线程通信,打开子线程的inspector server监听一个ip:port
  3. 用调试工具连接这个ip:port,开始调试

当然实际的处理会稍微复杂些,比如进程中可能有多个线程,所以我们需要选择其中一个线程。这里直接忽略实现细节,来看我们在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还会碰到一些问题,比如:

  • Node.js进程接收到SIGUSR1信号后监听的是127.0.0.1地址,如果我们想用进行远程调试(因为GUI比较方便/容易使用)至少还需要做个代理。
  • 如果系统上有多个Node.js进程,那么用SIGUSR1打开一个进程inspector server监听9229端口后,我们想再调试其他进程的话需要先关闭上一个进程释放9229端口。

我们在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中,但不会阻塞线程的运行。

184ebb4122ba71297fa545cd70745187.png
在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%A8​github.com


如果有适合添加到node-inspect中的命令,我们也会积极地向node-inspect提交PR。

延伸:在线上用inspector是否是种危险的行为?

在线上用inspector是不是比较危险?我一开始担心的是使用inspector中的一些功能(比如直接修改运行中的代码,甚至不用重启进程..),可能会让我们用一种很特殊的途径篡改了代码逻辑。如果我们无意中利用这种手段制造了一些问题的话,这些问题可能会非常难以排查。

但如果线上环境真的出现了一些异常情况致使我们的服务不可用、或是出现严重的逻辑错误,特别是当你通过其他常规手段无法解决问题的时候,那相比于让问题持续存在不断产生损失,能用inspector定位问题的话我肯定会用。既然Node.js/V8有如此强大的调试能力,那至少我们也应该把它当成是一种备选工具准备好,在我们判断合适的场景下拿出来用。

总结

这篇文章介绍了现阶段调试Node.js线程(worker_threads)的一种工具 diat,以及node-inspect对于线上环境调试的优点。同时也介绍了封装diat的原因及它和node-inspect之间的差别。也非常欢迎在Github与我们进行讨论或:https://github.com/bytedance/diat/issues/new

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值