21 可观测性、测试与排错

1. 进程问题必须带身份信息

只记录“脚本失败”几乎无法排查。建议结构化记录:

logger.info({
  event: "analysis_process_closed",
  requestId,
  taskId,
  pid: process.pid,
  childPid: child.pid,
  clusterWorkerId,
  commandName: "python3",
  exitCode: code,
  signal,
  durationMs,
  stdoutBytes,
  stderrBytes,
  timedOut,
  aborted,
});

不要记录完整命令字符串,因为参数可能含路径、邮箱、样本标识或秘密。推荐记录经过白名单的 commandName、参数数量和必要的非敏感标识。

2. 日志、指标和审计

日志:解释某次任务发生了什么
指标:观察成功率、耗时和重启趋势
审计:记录谁在何时启动、取消或下载了什么任务

高频指标:

当前 Cluster Worker 数量
Worker 重启次数和连续崩溃次数
当前子进程数量
子进程启动失败率
非零退出率
超时/取消次数
执行时长分位数
stdout/stderr 字节数
进程 RSS、Heap、External Memory
事件循环延迟
数据库连接池总量
后台队列积压

业务错误统一返回 HTTP 200 时,仍必须统计 businessCode 和任务状态,不能只看 HTTP 5xx。

3. stdout/stderr 日志策略

第三方程序可能输出海量文本。不要把每个 Chunk 原样 logger.info()

推荐:

stdout:按协议解析,限制总字节和单行长度
stderr:写受限任务日志文件或环形缓冲,只保留末尾摘要
错误响应:返回稳定业务消息,不原样返回 stderr

完整原始日志需要访问控制和 TTL。

4. Linux 排错命令

查看进程:

ps -ef
ps -o pid,ppid,stat,etime,cmd -p 1234
pgrep -af node
pstree -ap

资源:

top
htop
lsof -p 1234

Signal:

kill -TERM 1234
kill -INT 1234

不要一上来使用 kill -9SIGKILL 无法被进程捕获,优雅关闭、日志刷新和临时文件清理都没有机会执行。

查看退出状态:

node dist/tool.js
echo $?

管道测试:

set -o pipefail
node dist/generate.js | gzip > result.gz
echo $?

5. PM2 排错

pm2 list
pm2 show nloop
pm2 logs nloop --lines 200
pm2 env <pm_id>

重点对比:

PM2 使用的 Node.js 绝对路径
cwd
PATH
NODE_ENV
实例数量
重启次数
内存
启动脚本

“终端能运行、PM2 不能运行”常见原因:

不要让子进程依赖“碰巧能在 PATH 中找到”。生产配置应明确程序绝对路径或在受控配置中解析并启动时验证版本。

6. Zombie、孤儿和阻塞

在 Unix 中,子进程退出后父进程应收取其状态;Node.js 的 ChildProcess 生命周期通常会处理,但错误的脱离/后台脚本设计仍可能产生难以管理的孙进程。

排查:

STAT 为 Z        可能是 Zombie
PPID 变化        可能成为孤儿并被其他进程接管
子进程不耗 CPU但不结束  可能 stdout/stderr 管道堵塞
父进程退出后任务仍在    可能 detached 或孙进程未终止

不要仅根据 child.killed 判断是否还活着;它只表示 Node.js 曾成功发送过终止信号。

7. 测试辅助子脚本

准备几个明确行为的测试脚本比 Mock 所有事件更可靠:

success.js          stdout JSON,exit 0
fail.js             stderr 文本,exit 3
stderr-success.js   stderr 有进度,exit 0
large-output.js     持续输出大量 stdout
sleep.js            长时间运行,响应 SIGTERM
ignore-term.js      忽略/延迟 SIGTERM(仅测试)
child-tree.js       再启动孙进程

这些脚本只放测试目录,不能允许用户选择任意脚本路径。

8. 必测时间线

命令不存在

error
  → close

正常成功

spawn
  → stdout/stderr data
  → exit(0)
  → close(0)

业务失败

spawn
  → stderr data
  → exit(non-zero)
  → close(non-zero)

超时

timer/Abort
  → 发送终止请求
  → error 可能报告 AbortError
  → exit/close
  → 清理半成品

不要写只等待 exit 或只等待 error 的测试。

9. Cluster 测试

10. 调试清单

[ ] command 是否存在且版本正确
[ ] cwd 是否正确
[ ] args 是否按数组传递
[ ] env 是否缺少或泄漏变量
[ ] stdio 是否被 pipe 但无人消费
[ ] 是否同时监听 error 与 close
[ ] 非零 exit code 是否被正确判失败
[ ] stdout 结果是否运行时校验
[ ] timeout 后是否等待 close
[ ] 是否遗留孙进程和半成品
[ ] Cluster/PM2 是否双重扩容
[ ] Worker 是否重复启动定时任务
[ ] PM2 是否运行最新 dist

练习题

  1. 实现上述七个辅助脚本,并记录各自事件顺序。
  2. 让一个子进程持续写 stdout 但父进程不消费,观察阻塞。
  3. 对比终端与 PM2 中的 cwd、PATH 和 Node 版本。
  4. 设计 Worker 连续 5 次快速崩溃后的退避策略。
  5. 为任务 stderr 设计最大字节、保留尾部和访问控制策略。

完成标准