shutdown-check 会启动你的 Node.js 服务,发起一个请求,在请求还没结束时发送 SIGTERM,然后观察接下来发生了什么。结果为通过,说明请求处理完成、进程按时退出、端口已经关闭。
准备工作
你需要准备:
- Node.js 22 或更高版本
- macOS 或 Linux
- 一个本地 HTTP/1 服务
- 一个就绪检查(readiness)路由,例如
/health - 一个耗时足够长的路由,保证请求还在处理时能收到
SIGTERM
这个包适用于 node:http、Express、Fastify、NestJS 以及其他 Node.js 框架。它不会给你的应用添加关闭逻辑,只测试你已经写好的关闭逻辑。如果要用于 HTTPS、HTTP/2、WebSocket、Windows 或远程服务,请先阅读兼容性与限制。
1. 安装 shutdown-check
把 shutdown-check 安装为开发依赖:
npm install -D shutdown-check这个包没有运行时依赖,也不需要随生产应用一起发布。
2. 创建配置
在项目根目录运行初始化命令:
npx shutdown-check init该命令会生成 shutdown-check.json。如果文件已存在,不会覆盖。生成的内容如下:
{
"command": ["node", "server.js"],
"cwd": ".",
"baseUrl": "http://127.0.0.1:3000",
"readiness": { "path": "/health", "status": 200, "timeoutMs": 10000 },
"workload": {
"path": "/slow",
"method": "GET",
"status": 200,
"started": { "type": "response-headers", "timeoutMs": 5000 }
},
"shutdown": { "deadlineMs": 10000, "exitCode": 0 }
}修改以下几项:
| 字段 | 填写内容 |
|---|---|
command | 直接启动构建产物的命令 |
baseUrl | 专门留给这次测试的本地端口 |
readiness.path | 服务就绪时返回预期状态码的路由 |
workload.path | 耗时足够长、能在处理过程中收到关闭信号的路由 |
尽量直接启动服务,例如 ["node", "dist/server.js"]。如果外面包了一层 shell 或包管理器,信号可能被它们收到,而不是服务本身。
3. 准备可测试的请求路由
初始配置使用 response-headers 启动确认(start barrier)。测试请求对应的路由必须先发出响应头,并保持响应体未结束。这样 shutdown-check 才能在发送 SIGTERM 之前确认请求确实正在处理。
下面是一个与初始配置对应的完整服务:
const http = require("node:http");
const port = Number(process.env.PORT ?? 3000);
const server = http.createServer((request, response) => {
if (request.url === "/health") {
response.writeHead(200, { "content-type": "text/plain" });
response.end("ok");
return;
}
if (request.url === "/slow") {
response.writeHead(200, { "content-type": "text/plain" });
response.flushHeaders();
setTimeout(() => response.end("work complete\n"), 2000);
return;
}
response.writeHead(404).end();
});
server.listen(port, "127.0.0.1", () => {
console.log(`listening on http://127.0.0.1:${port}`);
});
process.on("SIGTERM", () => {
console.log("SIGTERM received; closing the server");
server.close((error) => {
if (error) {
console.error(error);
process.exitCode = 1;
}
});
});response.flushHeaders() 会立即发出 200 响应头,响应体在两秒后才结束。收到 SIGTERM 时,server.close() 停止接受新连接,并等待尚未结束的响应完成。
如果你的处理函数要等工作全部完成后才发送任何内容,请改用 probe 启动确认。普通的、很快返回的路由不适用:请求可能在信号到达前就已结束,结果会是 SC111。
4. 运行测试
npx shutdown-check test测试会依次:
- 检查配置的端口是否空闲
- 在独立的进程组中启动
command - 等待就绪检查通过
- 发出测试请求,并确认请求正在处理
- 发送
SIGTERM - 等待响应完成和进程退出
- 检查端口是否已关闭
测试通过时命令以 0 退出;关闭检查失败时以 1 退出;环境或配置无效、导致测试无法运行时以 2 退出。
5. 看懂通过结果
上面的服务会输出类似下面的结果。耗时和进程 ID 每次都会不同。
PASS SC000: Graceful shutdown verified
Timeline:
+ 5 ms process launched — pid=73661
+ 112 ms service ready — HTTP 200
+ 112 ms work request sent — #1 GET /slow
+ 113 ms work confirmed active — 1 response(s) sent headers; bodies still in progress
+ 113 ms signal sent — SIGTERM
+ 2118 ms work request finished — #1 HTTP 200
+ 2125 ms process exited — code=0, signal=none
+ 2126 ms shutdown verified — work completed and service exited before deadline关键看这几点的顺序:
work confirmed active出现在signal sent之前- 请求在信号之后才完成
- 进程以退出码
0退出 shutdown verified出现在最后
SC000 是通过时的诊断码。每个时间线事件的含义见输出参考。
6. 看一次真实的失败
删掉 SIGTERM 处理函数,再运行一次。Node.js 会按默认的信号行为立即退出,正在处理的请求被直接中断:
FAIL SC201: In-flight request was interrupted: aborted
Timeline:
+ 6 ms process launched — pid=73692
+ 112 ms service ready — HTTP 200
+ 113 ms work request sent — #1 GET /slow
+ 113 ms work confirmed active — 1 response(s) sent headers; bodies still in progress
+ 113 ms signal sent — SIGTERM
+ 116 ms process exited — code=null, signal=SIGTERM
+ 116 ms work request finished — #1 aborted
+ 116 ms check failed — SC201: In-flight request was interrupted: aborted
Service stdout (last 8 KiB):
listening on http://127.0.0.1:3000第一行给出固定不变的诊断码。打开 SC201 可以查看原因和修复方法。失败时,CLI 还会附上服务 stdout 和 stderr 的最后 8 KiB。
配置无效时输出不一样:以 shutdown-check: 开头,没有 SC 诊断码和时间线,退出码为 2:
shutdown-check: Cannot read JSON config /path/shutdown-check.json: Error: ENOENT: no such file or directory, open '/path/shutdown-check.json'如果命令没能跑出正常的测试结果,请参考故障排查。
7. 加强检查
初始配置只覆盖一个请求、进程退出和端口关闭。你还可以验证多个请求、响应内容、撤回就绪状态、拒绝新请求,以及重复发送信号:
{
"command": ["node", "server.js"],
"baseUrl": "http://127.0.0.1:3000",
"readiness": { "path": "/health" },
"workload": {
"path": "/slow",
"bodyIncludes": "work complete",
"concurrent": 2,
"started": { "type": "response-headers" }
},
"shutdown": {
"deadlineMs": 10000,
"readinessWithdrawal": true,
"newRequests": { "path": "/health" },
"repeatSignalAfterMs": 500
}
}示例服务同样能通过就绪检查和新请求检查,因为 server.close() 会拒绝新连接。如果你的应用在排空期间仍保持监听,也可以改为返回 503,参见就绪检查与排空。
8. 在 CI 中运行
先构建服务,然后运行:
npx shutdown-check test --json --junit shutdown-result.xml非零退出码会让任务失败。JUnit 文件可以作为测试报告发布,JSON 则把完整结果保留在任务日志里。CI 指南提供了 GitHub Actions 和 GitLab 的示例。