关闭库和关闭测试解决的是不同的问题。库负责给你的服务加上信号处理和清理逻辑;shutdown-check 则启动真实的服务,从外部测试这些逻辑是否真的生效。
shutdown-check 适合做什么?
如果你已经有信号处理函数(不管是自己写的、框架提供的,还是来自某个库),并且想验证进程完整的生命周期,就可以用 shutdown-check。
它会检查:
- 接近生产环境的启动命令
- 就绪检查
- 一个真实的、正在处理的 HTTP 请求
SIGTERM的送达- 响应是否完成
- 可选的流量排空
- 进程退出和退出码
- 最终端口是否关闭
它不会替你注册信号处理函数,也不会替你关闭应用的资源。
关闭库
@godaddy/terminus、http-terminator、lightship、close-with-grace 这类库运行在应用内部。它们可能会注册信号监听器、关闭 HTTP 服务器、管理超时,并提供清理钩子。
需要一套关闭实现时,就用库;配置好之后,再用 shutdown-check 测试最终效果。库本身没问题,也可能被用错:关闭的可能不是正确的 server 对象,清理的顺序可能不对,或者生产环境的启动命令可能让信号到不了 Node.js。
框架的关闭钩子
Fastify、NestJS 等框架都提供了关闭相关的 API。它们同样是实现,而不是端到端测试。
框架层面的测试通常直接调用 close 方法,而部署时是向进程发送信号。shutdown-check 把信号监听器、启动命令、框架、打开的 socket 和最终退出放在一起覆盖。
单元测试
单元测试可以调用关闭函数,并断言 mock 的依赖都已经关闭。
优点:
- 快
- 失败时能精确定位
- 容易覆盖错误分支
- 适合测试数据库和队列的清理逻辑
局限:
- 通常没有真实的子进程
- 没有操作系统信号
- HTTP 连接是 mock 的
- 无法证明生产环境的启动命令会退出
- 不检查是否留下孤儿子进程
单元测试要保留。再补一个黑盒测试,覆盖单元测试测不到的进程行为。
用 kill 和 curl 写的 shell 脚本
自己写脚本也可以:启动应用,用 curl 发请求,发送 kill -TERM,再检查退出情况。
这种做法行得通,但很难做到可靠的同步。固定的 sleep 并不能证明信号到达时请求正在处理。脚本还得自己处理并发请求、响应体、超时、进程组、清理、退出码和 CI 报告。
shutdown-check 把这些细节封装成一份可重复运行的配置和一套固定不变的诊断码。
预发布环境与 Kubernetes 测试
集群层面的行为只能在预发布环境(staging)里测试,比如 endpoint 的传播、ingress 的时序、sidecar 和 preStop 钩子。
但预发布测试也更慢,更难复现。终止时不一定有请求正在处理,而一次成功的发布也无法证明没有丢请求。
部署前,先用 shutdown-check 测试服务本身的行为,结果稳定、可复现;之后再到预发布环境测试基础设施层面的行为。
对比表
| 能力 | shutdown-check | 关闭库 | 单元测试 | Shell 脚本 | 预发布测试 |
|---|---|---|---|---|---|
| 添加关闭逻辑 | 否 | 是 | 否 | 否 | 否 |
| 运行真实的启动命令 | 是 | 不适用 | 通常否 | 是 | 是 |
发送真实的 SIGTERM | 是 | 负责处理 | 通常否 | 是 | 是 |
| 证明发信号前请求正在处理 | 是 | 否 | 借助 mock | 困难 | 困难 |
| 验证响应状态码和响应体 | 是 | 否 | 是 | 可以做到 | 可以做到 |
| 检查就绪状态撤回 | 可选 | 可能提供实现 | 借助 mock | 可以做到 | 是 |
| 检查进程退出和最终端口 | 是 | 否 | 否 | 可以做到 | 间接 |
| 直接检查数据库和队列 | 否 | 可能负责关闭 | 是 | 需自行实现 | 需自行实现 |
| 覆盖集群路由和 sidecar | 否 | 否 | 否 | 否 | 是 |
| 输出固定的失败码 | 是 | 否 | 测试名称 | 需自行实现 | 需自行实现 |
应该选哪种方式?
大多数服务可以这样做:
- 用框架自带的功能、某个库或一个简单的处理函数来实现关闭。
- 用单元测试覆盖应用特有的清理逻辑和错误路径。
- 在本地和 CI 中运行 shutdown-check,测试进程和 HTTP 的生命周期。
- 在预发布环境中测试 Kubernetes、代理和负载均衡器的行为。
这几层是互补的。只用其中一种测试替代全部,会让一些重要行为得不到验证。
shutdown-check 不够用的情况
如果正确性依赖以下内容,还需要补充其他测试:
- HTTP 响应之后的数据库事务
- 队列消息确认(ack)
- 后台任务
- WebSocket 或 HTTP/2
- 与远程服务的协调
- 容器或集群的终止顺序
具体边界见兼容性说明。