# 本地调试远程服务器上的Node代码
30 秒速记
- 现代做法:服务器上
node --inspect=127.0.0.1:9229 app.js,本地ssh -L 9229:127.0.0.1:9229 user@host,再用chrome://inspect或VS Code连localhost:9229 - 版本差异:老的
node --debug/node debug/node-inspector基于旧调试协议,Node 8起已移除,现在统一用Inspector协议(--inspect、node inspect) - 调试端口绝不能绑
0.0.0.0暴露公网,连上的人可以在你的进程里执行任意代码 - 断点会暂停整个事件循环,这个实例上所有请求都会卡住,只在摘掉流量的实例或副本上下断点
- 已经在跑的进程可以
kill -USR1 <pid>临时打开调试(Linux/macOS);本地代码版本要和线上一致,否则断点会错位
远程调试的思路是:服务器上的 Node 只在本机回环地址开调试端口,本地通过 SSH 隧道把端口转发过来,再用 Chrome DevTools 或 VS Code 连上去。 这样调试端口从不暴露在公网上,安全性靠 SSH 保证。老教程里的 --debug 和 node-inspector 已经淘汰,现在都是 --inspect。真正要小心的是断点:Node 是单线程的,断点一停整个进程都停,线上实例下断点等于让它挂掉,所以要么摘流量,要么拉个副本复现,线上优先靠日志和监控。
理解「本地调试远程服务器上的Node代码」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
远程调试 Node 的完整步骤,按现在的 Inspector 协议来做:
- 在服务器上启动进程时打开调试,只绑本机回环地址:
node --inspect=127.0.0.1:9229 app.js
# Debugger listening on ws://127.0.0.1:9229/xxxx-xxxx
如果进程已经在跑,不想重启,可以发信号临时打开:
kill -USR1 <pid>
- 在本地开一个
SSH隧道,把本地的9229转发到服务器的127.0.0.1:9229:
ssh -N -L 9229:127.0.0.1:9229 user@your-server
- 本地连接调试器,二选一:
Chrome打开chrome://inspect,在Remote Target里点inspectVS Code的launch.json加一个attach配置:
{
"type": "node",
"request": "attach",
"name": "Attach Remote",
"address": "localhost",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/srv/app"
}
localRoot 和 remoteRoot 告诉 VS Code 本地哪个目录对应服务器上哪个目录,配错了断点就是灰的。
几个版本上的变化要知道:node --debug、node debug 和 node-inspector 用的是旧的 V8 Debugger 协议,Node 8 起已经移除;命令行调试现在是 node inspect app.js,命令(next、cont、step、out、repl、watch)基本沿用。
最后是安全和稳定性:调试端口能执行任意代码,不能对公网开放;断点会让整个进程停下,只在摘掉流量的实例或者专门的复现环境里用。线上问题优先看日志、指标、trace,需要现场信息时用 --cpu-prof、--heapsnapshot-signal 或 process.report 生成诊断文件,不影响正常服务。
面试官追问
追问 1为了方便,把服务器启动参数改成 --inspect=0.0.0.0:9229,有什么问题?
任何能访问这个端口的人都能连进来执行任意代码,相当于把服务器的 shell 送出去了。只绑 127.0.0.1,本地通过 ssh -L 隧道访问,用完关掉。
追问 2在线上实例上打了个断点,接着用户反馈整个服务卡死,为什么?
断点暂停的是整个 JS 线程,事件循环停了,这个实例上所有请求都在排队。先从负载均衡上摘掉这个实例再调;能用日志、--cpu-prof 或者堆快照解决的,就别下断点。
追问 3服务已经在跑,启动时没加 --inspect,不想重启怎么办?
Linux/macOS 上 kill -USR1 <pid>,进程会在 127.0.0.1:9229 打开调试器,日志里会打出 Debugger listening on ws://...。之后照样走 SSH 隧道连。
追问 4连上了,但断点打在 src/user.ts 上一直不生效,为什么?
线上跑的是编译后的 dist 代码,要有对应的 source map,而且本地代码要和线上部署的那个提交完全一致,不然行号对不上。先确认 commit,再看 VS Code 的 outFiles 配得对不对。
追问 5调完之后发现 9229 端口还被占着,下一次启动报 EADDRINUSE,怎么处理?
多半是带 --inspect 的旧进程没退干净,lsof -i :9229 找到进程结束掉。重启要换端口可以 --inspect=127.0.0.1:0,让系统随机分配一个。
# 写在前面
30 秒速记
- 三种断点调试入口:内置命令行调试器 /
VS Code这类IDE/ 浏览器DevTools(老版本靠node-inspector) - 本质是同一件事:
Node进程开一个调试端口,调试客户端连上去下断点、看变量 - 版本分水岭:
Node 6.3前是旧debug协议(--debug,端口5858);之后是Inspector协议(--inspect,端口9229),Node 8起旧协议直接移除 - 远程调试 = 远端开端口 + 本地客户端连过去;端口只绑
127.0.0.1,走SSH隧道 - 线上排查优先日志、指标、
trace,断点只在隔离实例上用
Node 的几种断点调试方式只是客户端不同,底层都是「进程开调试端口,客户端连上去」。 命令行调试器、VS Code、浏览器 DevTools 本质都是调试协议的客户端,区别只在界面顺不顺手。这里有个版本坑:早年教程里的 --debug-brk、node-inspector 走的是旧协议,Node 8 之后已经跑不起来了,现在统一用 --inspect。搞懂这一点,远程调试就不神秘了,无非是把端口安全地接到本地。
谈到node断点调试,目前主要有三种方式,通过node内置调试工具、通过IDE(如vscode)、通过node-inspector,三者本质上差不多。本文着重点在于介绍 如何在本地通过node-inspector 调试远程服务器上的node代码。
在进入主题之前,首先会对三种调试方式进行入门讲解,方便新手理解后面的内容。至于老司机们,可以直接跳到主题去。
面试官追问
追问 1命令行、VS Code、DevTools 下断点,背后原理一样吗?
一样。都是 Node 进程用 --inspect 开一个 WebSocket 调试端口,客户端按 Inspector 协议发「设断点」「继续执行」这类指令,换客户端不换原理。
追问 2照着老教程装了 node-inspector,在 Node 18 上连不上,为什么?
node-inspector 依赖旧的 --debug 协议,Node 8 起这个协议就被移除了,项目也早停止维护。直接 node --inspect app.js,然后在 Chrome 里打开 chrome://inspect 就行。
追问 3远程调试最容易出事的地方在哪?
把调试端口开到公网。Inspector 能在进程里执行任意代码,谁连上谁就拿到了这台机器上这个进程的权限。端口只绑 127.0.0.1,本地用 ssh -L 转发过来。
追问 4线上接口偶发报错,直接在生产实例上挂断点查可以吗?
一般不这么干。断点一停,整个事件循环都停了,这个实例上所有请求一起卡住。先看日志和 trace,真要断点就摘掉一个实例的流量,或者起一个隔离副本复现。
# 方式一:内置debug功能
30 秒速记
- 现在的入口:
node inspect app.js(Node 8前是node debug,已废弃) - 单步四件套:
n/next下一行、c/cont到下一个断点、s/step进函数、o/out跳出函数 - 看变量:输入
repl进入求值模式,敲变量名就能看值,Ctrl+C退回调试命令 - 监视:
watch('expr')加、watchers看、unwatch('expr')删;restart重跑不用退出 - 远程:服务端
