Shutdown Check

搜索文档

查找页面或章节

进行中的工作与启动确认

在 shutdown-check 发送 SIGTERM 之前,证明确实有真实工作正在进行。

要测试关闭期间进行中的请求,shutdown-check 必须确认请求仍在进行。启动确认(start barrier)就是用来提供这个证明的。只有启动确认通过后,才会发送信号。

为什么需要启动确认?

如果没有启动确认,一个很快的请求可能在 SIGTERM 之前就已结束。这时即使服务根本没有关闭处理函数,测试也会通过,因为已经没有需要排空的工作了。

shutdown-check 会防止这种误判。如果无法证明有工作正在进行,它会返回 SC111,并且不发送信号。

选择启动确认方式

方式通过条件适用场景请求数
response-headers每个响应都已发送响应头,且响应体未结束路由可以流式输出或延迟发送响应体1–20
probe一个单独的路由从非活跃变为活跃处理函数在操作完成前不发送任何内容1

response-headers 证明的是有一个 HTTP 响应处于打开状态。探测路由则可以证明某个具体的内部操作已经开始。

使用 response-headers 启动确认

配置测试请求:

shutdown-check.json
{
  "workload": {
    "path": "/slow",
    "concurrent": 2,
    "started": {
      "type": "response-headers",
      "timeoutMs": 5000
    }
  }
}

每个请求都必须在 timeoutMs 内收到响应头,且响应体必须仍未结束。有两个请求时,两个都必须满足启动确认。

编写合适的路由

Node.js 可能会把响应头留到第一次写入响应体时才发送。调用 flushHeaders() 可以立即发送:

server.js
if (request.url === "/slow") {
  response.writeHead(200, { "content-type": "text/plain" });
  response.flushHeaders();
 
  setTimeout(() => {
    response.end("work complete\n");
  }, 2000);
  return;
}

延迟时间必须足够长,让 shutdown-check 来得及发送信号,但要比 shutdown.deadlineMs 短。这个路由应该可以安全地重复执行,且不应修改生产数据。

了解失败情况

以下情况会导致启动确认失败:

  • 在所有响应头得到确认之前,某个响应已经结束
  • 并发的一组请求中,有一个提前结束
  • timeoutMs 之前没有收到响应头
  • 路由拒绝或重置了连接

成功时,时间线中会出现:

  +  114 ms  work confirmed active — 2 response(s) sent headers; bodies still in progress

使用 probe 启动确认

如果处理函数要先算出完整结果才发送响应,就使用探测路由。探测路由会报告这个操作当前是否正在运行。

shutdown-check.json
{
  "command": ["node", "server.js"],
  "env": { "ENABLE_TEST_ROUTES": "1" },
  "baseUrl": "http://127.0.0.1:3000",
  "readiness": { "path": "/health" },
  "workload": {
    "path": "/reports",
    "method": "POST",
    "status": 200,
    "bodyIncludes": "report ready",
    "started": {
      "type": "probe",
      "path": "/test/work-active",
      "inactiveStatus": 204,
      "activeStatus": 200,
      "timeoutMs": 5000,
      "intervalMs": 50
    }
  },
  "shutdown": { "deadlineMs": 10000 }
}

探测分两个阶段:

  1. 发出测试请求之前,探测路由必须返回 inactiveStatus。
  2. 测试请求开始后,在它的响应仍未结束时,探测路由必须返回 activeStatus。

第一阶段失败时,结果为 SC110。第二阶段超时,或测试请求先结束时,结果为 SC111。

编写探测路由

用一个计数器跟踪真实的操作。只在测试环境中开放这个路由。

server.js
const http = require("node:http");
const { setTimeout: delay } = require("node:timers/promises");
 
let activeReports = 0;
 
async function buildReport() {
  await delay(2000);
  return { rows: 42 };
}
 
const server = http.createServer(async (request, response) => {
  if (request.url === "/health") {
    response.writeHead(200).end("ok");
    return;
  }
 
  if (
    request.url === "/test/work-active" &&
    process.env.ENABLE_TEST_ROUTES === "1"
  ) {
    response.writeHead(activeReports > 0 ? 200 : 204).end();
    return;
  }
 
  if (request.url === "/reports" && request.method === "POST") {
    activeReports++;
    try {
      const report = await buildReport();
      response.writeHead(200, { "content-type": "application/json" });
      response.end(JSON.stringify({ status: "report ready", ...report }));
    } finally {
      activeReports--;
    }
    return;
  }
 
  response.writeHead(404).end();
});
 
server.listen(3000, "127.0.0.1");
process.on("SIGTERM", () => server.close());

计数器在耗时操作开始前增加,并在 finally 中复原,因此无论操作成功还是失败,探测路由都能如实反映。

解读通过时的探测时间线

PASS SC000: Graceful shutdown verified
 
Timeline:
  +    6 ms  process launched — pid=73999
  +  115 ms  service ready — HTTP 200
  +  116 ms  work request sent — #1 POST /reports
  +  117 ms  work confirmed active — probe /test/work-active returned HTTP 200
  +  117 ms  signal sent — SIGTERM
  + 2120 ms  work request finished — #1 HTTP 200
  + 2122 ms  process exited — code=0, signal=none
  + 2123 ms  shutdown verified — work completed and service exited before deadline

证明操作已经完成

状态码为 200 的响应,内容仍可能是错误信息。把 bodyIncludes 设为只有工作成功后才会出现的文本:

{
  "bodyIncludes": "report ready"
}

匹配区分大小写。找不到该文本时返回 SC203。请选择固定不变的文本,不要用生成的 ID 或时间戳。

安全地设计测试专用路由

  • 只通过测试环境变量启用这些路由。
  • 不要在生产环境中暴露内部状态。
  • 避免真实扣费、发送邮件或破坏性的副作用。
  • 在 finally 中重置计数器。
  • 在较慢的 CI 机器上,也要保证测试请求的行为稳定可预期。
  • 为并行运行的测试分配相互隔离的端口和状态。

修复常见的启动确认失败

现象可能原因修复方法
响应立即结束路由太快延迟或流式输出这个受控的测试响应
始终收不到响应头Node.js 缓冲了响应头调用 flushHeaders() 或写入一段响应体
探测一开始就是活跃状态有残留的旧工作,或探测状态不正确重置状态,并先确认返回的是 inactiveStatus
探测始终不变为活跃计数器更新太晚,或路径不对在操作开始前设置状态,并检查路由
本地通过但 CI 失败时间窗口太小让测试请求的耗时明显更长
多个请求导致配置被拒绝探测只支持一个请求需要并发时改用 response-headers