Shutdown Check

搜索文档

查找页面或章节

安装包,接入一个真实路由,跑通第一次关闭测试。

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。如果文件已存在,不会覆盖。生成的内容如下:

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 之前确认请求确实正在处理。

下面是一个与初始配置对应的完整服务:

server.js
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

测试会依次:

  1. 检查配置的端口是否空闲
  2. 在独立的进程组中启动 command
  3. 等待就绪检查通过
  4. 发出测试请求,并确认请求正在处理
  5. 发送 SIGTERM
  6. 等待响应完成和进程退出
  7. 检查端口是否已关闭

测试通过时命令以 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. 加强检查

初始配置只覆盖一个请求、进程退出和端口关闭。你还可以验证多个请求、响应内容、撤回就绪状态、拒绝新请求,以及重复发送信号:

shutdown-check.json
{
  "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 的示例。