Node.js 调试与观测:Inspector、日志与性能剖析

线上「跑不起来」往往比「写不出来」更费时间;Node 又是异步优先,栈信息一截就断。
这一篇想先把观测手段摆清楚:调试器(Inspector)在什么场景用、日志与结构化输出怎么接、以及性能问题该从哪几类工具下手,而不是一上来就加 console.log 扫雷。

Node 内置对 Chrome DevTools 协议(Inspector) 的支持,常见用法:

  • 启动时加 --inspect(或 --inspect-brk 在第一行就停住),用浏览器连上 调试端口
  • 在 VS Code / WebStorm 里直接 Attach 到进程,设断点、看调用栈、检查闭包变量。

和「print 调试」的差别在于:

  • 断点让你在「时间真的停在这一刻」时看状态,而不是事后猜日志顺序;
  • 异步栈(在较新工具链里)能帮你看到 Promise/async 背后丢掉的上下文——这对 Node 特别关键。

实践上:

  • 本地开发:优先用 IDE 断点 + 条件断点;
  • 短时附着线上:要谨慎(端口暴露、性能开销),通常配合 只读安全策略短窗口开启

console.log / console.error 在脚本里很快,在服务端里容易失控:

  • 输出格式不统一,难以检索;
  • 高频路径上字符串拼接本身也是成本;
  • 敏感信息容易顺手打出去。

更稳的习惯是引入 结构化日志(例如 pinowinston 等):

  • 每条日志带 级别(error / warn / info / debug)、时间戳请求 id / 链路 id
  • 输出 JSON 行,方便丢进 ELK、Loki、CloudWatch 里查。

心智模型可以记一句:

  • 日志是「可查询的事实流」,不是「给人看的 println」。

和观测强相关的有两类钩子:

  • process.on('uncaughtException'):未捕获的同步异常;
  • process.on('unhandledRejection'):未处理的 Promise 拒绝。

工程含义:

  • 长期依赖「全局吞掉一切继续跑」很危险,容易进入 不确定状态
  • 更常见的做法是:记录 + 告警 + 按策略退出(让进程管理器重启),而不是假装没事。

也就是说,观测里要包含「进程为什么挂」这一条,而不是只看业务指标。

遇到「慢」或「卡」,大致先归三类:

  • CPU 热点:纯计算、加密、序列化过重 → 看 CPU profile(Inspector 里也能采);
  • I/O 等待:外部服务慢、连接池打满 → 看 延迟分布、依赖的 trace、连接数;
  • 事件循环滞后:回调排队、nextTick 滥用、同步阻塞 → 看 event loop lag 指标、诊断报告。

Node 自带的 --prof / V8 采样、以及 clinic.js 一类工具,适合在复现环境里跑一轮,看 火焰图 上到底是哪段 JS 在吃时间。

避免一上来就:

  • 盲目加缓存;
  • 把异步全改成同步「求稳」——往往适得其反。

内存持续上涨时:

  • heap snapshot 对比两个时间点的对象分布,找 泄漏类型(闭包挂着大对象、全局 Map 只增不减等);
  • 配合 GC 日志(启动参数打开,版本间略有差异,以当前 Node 文档为准)看回收是否正常。

这类工作通常不在热路径上天天做,但属于 后端必备技能清单 里的一项。

  • Inspector 解决「停在真实时刻看状态」,适合复杂异步与断点调试;
  • 结构化日志 + 错误钩子 解决「线上可追溯」,要和部署、告警一起设计;
  • 性能与内存工具 解决「慢在哪、胀在哪」,先分类再下刀。

把这套手段备齐,后面接 HTTP 服务、进程拆分、微服务链路时,才有地方下手查问题。