Shutdown Check

搜索文档

查找页面或章节

就绪检查与排空流量

把新流量引走,同时让已经在处理的请求安全完成。

收到 SIGTERM 后,服务应当先停止报告就绪,再去完成进行中的请求。这样在实例排空期间,负载均衡器就能把流量转到其他地方。

关闭前如何排空连接?

按以下顺序处理:

  1. 设置一个全局的排空状态。
  2. 让就绪检查(readiness)路由返回未就绪的状态码。
  3. 拒绝新的业务请求。
  4. 让之前已经开始的请求处理完。
  5. 关闭监听器和共享资源。
  6. 在平台的截止时间之前退出。

就绪状态的变化并不会立刻在所有地方生效。负载均衡器可能每隔几秒才探测一次,endpoint 更新也需要时间传播。服务必须处理好这段延迟内到达的请求。

选择排空方式

方式收到信号后的就绪检查收到信号后的新请求适用场景
立即关闭监听器连接被拒绝连接被拒绝客户端会重试,且就绪状态没有传播延迟
继续监听,并短暂返回 503503503负载均衡器需要时间才能感知就绪状态的变化

两种方式都能通过 shutdown-check。返回 503 能给客户端更明确的响应,也给流量系统留出一小段时间来更新。

编写能排空的服务器

server.js
const http = require("node:http");
 
const port = Number(process.env.PORT ?? 3000);
const drainDelayMs = Number(process.env.DRAIN_DELAY_MS ?? 1000);
let draining = false;
 
const server = http.createServer((request, response) => {
  if (request.url === "/health") {
    response.writeHead(draining ? 503 : 200, {
      "content-type": "text/plain",
    });
    response.end(draining ? "draining" : "ok");
    return;
  }
 
  if (draining) {
    response.writeHead(503, {
      connection: "close",
      "retry-after": "1",
    });
    response.end("shutting down");
    return;
  }
 
  if (request.url === "/slow") {
    response.writeHead(200, { "content-type": "text/plain" });
    response.flushHeaders();
    setTimeout(() => response.end("work complete\n"), 2000);
    return;
  }
 
  if (request.url === "/test/new-work") {
    response.writeHead(200).end("accepted");
    return;
  }
 
  response.writeHead(404).end();
});
 
server.listen(port, "127.0.0.1");
 
process.on("SIGTERM", () => {
  if (draining) return;
  draining = true;
 
  setTimeout(() => {
    server.close((error) => {
      if (error) {
        console.error(error);
        process.exitCode = 1;
      }
    });
  }, drainDelayMs);
});

已经进入处理函数的请求不会再检查 draining,所以会正常完成。新请求会经过排空判断,收到 503。

这段延迟让监听器保持可用,好让流量系统感知到就绪状态的变化。延迟结束后,server.close() 停止接受连接,并等待原有的工作完成。

配置排空检查

shutdown-check.json
{
  "command": ["node", "server.js"],
  "baseUrl": "http://127.0.0.1:3000",
  "readiness": { "path": "/health", "status": 200 },
  "workload": {
    "path": "/slow",
    "bodyIncludes": "work complete",
    "concurrent": 2,
    "started": { "type": "response-headers" }
  },
  "shutdown": {
    "deadlineMs": 10000,
    "readinessWithdrawal": true,
    "newRequests": {
      "path": "/test/new-work",
      "rejectStatuses": [503],
      "allowConnectionRefused": true
    },
    "repeatSignalAfterMs": 500
  }
}

readinessWithdrawal 检查什么

发送 SIGTERM 后,shutdown-check 会轮询平常使用的就绪检查路由。只要看到以下任一情况,这项检查就通过:

  • 状态码不等于 readiness.status
  • 连接被拒绝或被重置

请求超时不算,因为超时并不能明确告诉负载均衡器服务已不可用。

如果直到截止时间,就绪检查仍然返回就绪,shutdown-check 会返回 SC310。

newRequests 检查什么

撤回就绪状态后,shutdown-check 会在原有测试请求仍在进行时,向 newRequests.path 发送一个 GET 请求。

这个新请求必须满足以下任一条件:

  • 返回的状态码在 rejectStatuses 中
  • allowConnectionRefused 为 true 时,连接被拒绝

如果超时、返回 200 或其他不在列表中的状态码,结果是 SC311。如果还没来得及测试新请求,原有的工作就已经结束,结果是 SC312。

请选择一个安全的 GET 路由。shutdown-check 会真实调用它,所以它不能创建订单、发送邮件或修改生产数据。

看懂通过时的时间线

PASS SC000: Graceful shutdown verified
 
Timeline:
  +    5 ms  process launched — pid=73882
  +  112 ms  service ready — HTTP 200
  +  112 ms  work request sent — #1 GET /slow
  +  112 ms  work request sent — #2 GET /slow
  +  113 ms  work confirmed active — 2 response(s) sent headers; bodies still in progress
  +  113 ms  signal sent — SIGTERM
  +  114 ms  readiness withdrawn — HTTP 503
  +  115 ms  new request rejected — HTTP 503
  +  615 ms  signal repeated — SIGTERM
  + 2115 ms  work request finished — #1 HTTP 200
  + 2115 ms  work request finished — #2 HTTP 200
  + 2118 ms  process exited — code=0, signal=none
  + 2119 ms  shutdown verified — work completed and service exited before deadline

关键在于顺序:撤回就绪状态和拒绝新请求,都发生在原有工作完成之前。

选择排空延迟

先用负载均衡器的就绪探测间隔乘以它的失败阈值作为起点。如果涉及 endpoint 传播,再加上传播所需的时间。

总时间仍然必须满足:

排空延迟 + 最慢的进行中请求 + 清理 < 平台的关闭截止时间

shutdown.deadlineMs 要覆盖服务端这部分的耗时,并留出安全余量。在 Kubernetes 上,别忘了 preStop 的时间也包含在 terminationGracePeriodSeconds 之内。

安全处理重复的 SIGTERM

信号处理函数应当是幂等的。第一个信号启动排空;之后的信号不应重启定时器、重复执行清理,也不应强制中断进行中的工作。

示例中的 if (draining) return 判断就是用来处理这种情况的。启用 repeatSignalAfterMs 可以测试这一行为。

常见失败

失败现象结果修复方法
就绪检查一直返回 200SC310把切换就绪状态作为信号处理的第一步
新请求返回 200SC311在业务路由之前加上排空判断
新请求一直挂起SC311返回明确的拒绝状态码
还没测试拒绝,旧的工作就已结束SC312使用耗时更长且可控的工作,或更早撤回就绪状态
旧请求收到 503SC202只对排空开始后才进入的请求应用排空判断
进程一直存活SC300HTTP 工作排空后,关闭定时器和共享资源