shutdown-check 像部署平台一样对待你的服务:运行真实的启动命令,发送 HTTP 流量,发出 SIGTERM,然后在进程外部观察结果。它不会 import 你的应用,也不会直接调用关闭处理函数。
什么是黑盒关闭测试?
黑盒关闭测试只使用服务对外可见的行为:启动命令、端口、HTTP 路由、信号处理和退出状态。单元测试常常漏掉的问题都能覆盖到,包括 shell 包装进程、框架行为、真实的 socket、进程退出和子进程。
代价是可见性有限。shutdown-check 能看到 HTTP 响应和进程事件,但除非响应本身能证明,否则它无法知道内部的数据库写入或队列消息是否已经完成。具体边界见兼容性与限制。
一次运行会经过哪些阶段?
各阶段总是按以下顺序执行:
| 阶段 | shutdown-check 做什么 | 主要失败码 |
|---|---|---|
| 1 | 确认端口空闲 | SC001 |
| 2 | 启动配置的命令 | SC002 |
| 3 | 等待服务就绪 | SC100–101 |
| 4 | 发出测试请求,并确认请求正在处理 | SC110–111 |
| 5 | 确认进程和测试请求仍然存活 | SC112 |
| 6 | 发送 SIGTERM | SC113 |
| 7 | 可选:重复发送信号 | SC114 |
| 8 | 可选:检查就绪状态撤回和新请求拒绝 | SC310–312 |
| 9 | 等待进行中的响应完成 | SC200–203 |
| 10 | 检查进程退出 | SC300–301 |
| 11 | 确认端口已关闭 | SC302 |
所有必需的等待结束后,报告的第一个失败就是最早出现的有效问题。通过时的诊断码是 SC000。
1. 检查端口是否空闲
启动服务之前,shutdown-check 会向就绪检查 URL 发一个短请求,这时连接必须被拒绝。只要收到任何响应、超时,或者出现无法判断的网络错误,都说明无法确认端口空闲,结果为 SC001。
这样可以避免请求打到一个已经在运行的服务上,得出虚假的通过结果。同时也让并行测试的结果可预期:每个测试都需要自己的端口。
2. 启动真实命令
命令数组直接启动,不经过 shell。命令在独立的进程组中运行,cwd 和 env 取自配置。
{
"command": ["node", "dist/server.js"],
"cwd": ".",
"env": { "PORT": "3510", "NODE_ENV": "production" }
}尽可能使用与生产环境相同的入口。直接启动服务,还能避开那些吞掉 SIGTERM、或者先于子进程中的服务退出的包装进程。
3. 等待服务就绪
shutdown-check 会轮询 baseUrl + readiness.path,直到拿到预期的状态码。连接被拒绝或返回其他状态码时会一直重试,直到超过 readiness.timeoutMs。
就绪检查通过之前不会发送任何测试请求。这样可以把启动失败和关闭失败区分开。
4. 确认请求正在处理
测试会发出一个或多个测试请求,然后等待配置的启动确认(start barrier)。
response-headers 启动确认
每个请求都必须在响应体尚未结束时收到响应头。适用于流式路由,或者在处理完成前就先发出响应头的可控接口。
probe 启动确认
测试请求保持处理中时,另一个独立路由必须从 inactiveStatus 变为 activeStatus。适用于处理函数要等全部工作完成后才发送响应的场景。
如果探测路由一开始就处于活跃状态,结果为 SC110;如果无法确认请求正在处理,结果为 SC111。完整示例见进行中的工作指南。
5. 再次确认进程和请求
从启动确认通过到发出信号,中间有一个很短的间隔。shutdown-check 会确认在这段间隔里,服务和每个测试请求都仍然存活。只要有一个已经结束,结果就是 SC112。
这样,即使测试请求只是勉强慢到能通过启动确认,测试也不会因此误判为通过。
6. 发送 SIGTERM
shutdown-check 把 SIGTERM 发给它启动的那个进程本身。这一阶段不会向整个进程组发信号。正因为如此,包装进程的问题会暴露出来:启动器必须把信号转发给真正的服务。
如果操作系统拒绝发送信号,结果为 SC113。发送成功后,关闭截止时间开始计时,时间线记录 signal sent。
7. 按需重复发送信号
设置了 repeatSignalAfterMs 时,只有在进程和最初的请求都仍在处理中时,shutdown-check 才会再发一次 SIGTERM。这是为了确认第二个信号不会让进程提前退出。
如果无法测试或无法发送重复信号,结果为 SC114。重复发送的延迟要比测试请求的耗时和 shutdown.deadlineMs 都短。
8. 检查流量排空
这些检查都是可选的。
设置 readinessWithdrawal: true 后,shutdown-check 会在发出信号后轮询就绪检查路由。返回非就绪状态码或连接被拒绝,都算作已撤回就绪状态;超时不算。如果就绪状态一直没有变化,结果为 SC310。
设置 newRequests 后,测试接着会在最初的请求仍在处理时,再发一个新的 GET。这个请求必须返回配置的拒绝状态码;如果配置允许,也可以是连接被拒绝。
在这段时间内返回 503 的服务示例,见就绪检查与排空。
9. 等待每个响应完成
每个测试请求都必须在 shutdown.deadlineMs 之前完成,并返回配置的状态码,以及可选的响应体文本。
并发请求会逐个检查。按请求顺序,第一个失败的请求决定诊断码。
10. 检查进程退出
服务必须在同一个关闭截止时间之前退出。
默认的预期退出码是 0。对进程管理器和监控系统来说,一次计划内的关闭通常应该表现为正常退出。
11. 确认端口已关闭
启动的进程退出后,shutdown-check 会再请求一次就绪检查 URL,这时连接必须被拒绝。如果仍有响应,多半是某个子进程中的服务还在运行,结果为 SC302。
测试正是靠这一步发现 npm start、shell 脚本或其他启动器自己退出了,却没有停掉真正的服务。
失败后如何清理?
无论结果如何,shutdown-check 都会关闭自己发起的请求并清除定时器。如果服务或子进程还在,工具会向它创建的进程组发送 SIGKILL。清理可以避免失败的测试一直占用端口,影响下一次运行。
清理不等于关闭通过。只有服务在需要清理之前自己完成工作、以预期的退出码退出并关闭端口,才算通过。
怎样使用时间线?
从上往下看,找到 check failed 之前最后一个成功的事件。例如:
- 没有
service ready:启动或就绪检查失败 - 没有
work confirmed active:启动确认失败 - 有
signal sent,但之后请求一直没有完成:排空过程卡住了 - 有
process exited,但端口仍然开着:有子进程中的服务没有退出
每个事件以及 JSON 中可用的字段,见输出参考。