Shutdown Check

搜索文档

查找页面或章节

告诉 shutdown-check 如何启动服务、如何发起进行中的工作,以及如何判定关闭是否通过。

shutdown-check 的配置是一个名为 shutdown-check.json 的 JSON 文件。它告诉 CLI 如何启动你的服务、如何等待服务就绪、如何发起进行中的工作、如何发送 SIGTERM,以及如何判断这次关闭是否通过。

生成初始配置文件

运行:

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 }
}

第一次运行前,先改好启动命令、端口、就绪检查路径和测试请求路径。快速开始里有一个和这份配置配套的服务端示例。

配置规则

  • CLI 只接受 JSON,不允许注释和末尾逗号。
  • 所有数值都必须是整数,并且在文档规定的范围内。
  • 校验遇到第一个无效值就会停止。
  • 配置无效时,CLI 会打印 shutdown-check: <message>,并以退出码 2 退出。
  • 未知字段会被忽略。所以可选字段拼错时不会报错,而是直接回退到默认值。
  • 路由路径只能是 baseUrl 下的路径;完整 URL 和 //host/path 形式都会被拒绝。

全部字段

选项类型默认值说明
command*string[]—服务的启动命令,写成参数数组,而不是 shell 字符串:["node", "dist/server.js"]。命令会直接启动,并运行在独立的进程组中。
cwdstring配置文件所在目录command 的工作目录,相对于配置文件解析(在 API 中相对于 baseDirectory)。
envRecord<string, string>{}传给启动进程的环境变量,会加到检查自身的环境变量中,或覆盖其中的同名变量。
baseUrl*string—服务的源地址(origin)。必须是 localhost、127.0.0.1 或 [::1] 上的纯 HTTP 地址,不能带路径、查询参数或凭据。使用专门留给测试的端口。
readiness*object—表明服务已可以接收流量的路由。检查会持续轮询这个路由,直到返回 status。
readiness.path*string—就绪检查路由的本地路径,例如 /health。
readiness.statusnumber200表示服务已就绪的状态码,范围 100–599。
readiness.timeoutMsnumber10000等待服务就绪的最长时间,范围 100–300000 ms。
readiness.intervalMsnumber100两次就绪轮询之间的间隔,范围 10–10000 ms。
workload*object—SIGTERM 到达时仍在进行中的慢请求,以及它的响应必须满足的条件。
workload.path*string—端点的本地路径。该端点的处理时间要足够长,确保发送信号时它仍在运行。
workload.methodstring"GET"HTTP 方法(大写)。
workload.headersRecord<string, string>{}请求头,值为字符串。
workload.bodystring—请求体,以字符串形式提供。
workload.statusnumber200每个进行中的请求都必须以这个状态码结束。其他状态码会报 SC202。
workload.bodyIncludesstring—每个进行中请求的响应体都必须包含的文本,用来证明工作确实完成了。缺少这段文本会报 SC203。
workload.concurrentnumber1同时处于进行中的测试请求数量,范围 1–20。大于 1 时需要使用 response-headers 启动确认。
workload.started*object—发送 SIGTERM 之前,检查靠什么确认工作已经真正开始,即启动确认(start barrier)。参见启动确认。
workload.started.type*"response-headers" | "probe"—"response-headers":响应头已到达、响应体仍未结束时,视为工作已开始。"probe":由单独的路由报告工作是否已开始。
workload.started.timeoutMsnumber5000等待工作开始的最长时间,范围 100–300000 ms。未按时开始会报 SC111。
workload.started.pathstring—用于 type: "probe"(此时必填):报告是否有工作正在运行的路由。
workload.started.inactiveStatusnumber204用于 type: "probe":没有工作在运行时,探测路由返回的状态码。
workload.started.activeStatusnumber200用于 type: "probe":有工作在运行时,探测路由返回的状态码。必须与 inactiveStatus 不同。
workload.started.intervalMsnumber50用于 type: "probe":两次探测轮询之间的间隔,范围 10–10000 ms。
shutdownobject—发送 SIGTERM 后,一次合格的优雅关闭必须满足的条件。
shutdown.signal"SIGTERM""SIGTERM"要发送的信号。1.0 版本只支持 SIGTERM。
shutdown.deadlineMsnumber10000从发送信号算起,工作必须完成、进程必须退出的时限,范围 100–300000 ms。
shutdown.exitCodenumber0进程退出时必须使用的退出码,范围 0–255。
shutdown.readinessWithdrawalbooleanfalse要求在 SIGTERM 之后、截止时间之前,就绪检查路由不再返回就绪状态码(或直接拒绝连接)。
shutdown.newRequestsobject—在排空期间发送一个新的 GET 请求,并要求服务拒绝它。需要设置 readinessWithdrawal: true。
shutdown.newRequests.path*string—一个安全、仅供测试使用的路由。旧工作排空期间,新请求会发到这里。
shutdown.newRequests.rejectStatusesnumber[][503]算作拒绝新请求的状态码。
shutdown.newRequests.allowConnectionRefusedbooleantrue连接被拒绝时是否也算作拒绝。超时一律不算。
shutdown.repeatSignalAfterMsnumber—在第一次 SIGTERM 之后等待这段时间,趁工作仍在运行时再发送一次 SIGTERM,确认重复的信号不会中断工作。范围 10–300000 ms,且必须小于 deadlineMs。

上表是自动生成的,列出了每个字段的类型和默认值。下面几节说明这些字段如何配合使用,以及在真实服务中该怎么选。

command、cwd 和 env

shutdown-check.json
{
  "command": ["node", "dist/server.js"],
  "cwd": ".",
  "env": {
    "PORT": "3510",
    "NODE_ENV": "production"
  }
}

command

把可执行文件和每个参数分别写成数组中的一项。shutdown-check 不经过 shell 执行命令。

尽量直接启动真正的服务进程:

{ "command": ["node", "dist/server.js"] }

能不用 shell 字符串或包装进程,就尽量不用:

{ "command": ["npm", "start"] }

包装进程收到 SIGTERM 后可能不会转发给服务。它也可能自己先退出,而子进程里的服务仍占着端口,这会导致 SC302。

cwd

cwd 基于配置文件所在的目录解析。不写时,命令就在该目录下运行。

如果配置文件是 config/shutdown-check.json,下面的写法会让命令从项目根目录启动:

{ "cwd": ".." }

env

env 会在 CLI 继承的环境变量基础上新增或覆盖变量。所有值都必须是字符串。可以用它给每个测试分配专用端口,或者开启仅供测试使用的路由。

{
  "env": {
    "PORT": "3510",
    "ENABLE_TEST_ROUTES": "1"
  }
}

baseUrl

baseUrl 是就绪检查、测试请求、探测路由和新请求这些路径共用的源地址(origin),必须是本机上的普通 HTTP 地址。

接受拒绝
http://127.0.0.1:3510https://127.0.0.1:3510
http://localhost:3510http://example.com:3510
http://[::1]:3510http://0.0.0.0:3510
http://127.0.0.1:3510/http://127.0.0.1:3510/api

为一个测试专门预留这个端口。shutdown-check 要求启动前端口是空闲的,进程退出后端口已关闭。并行运行的测试必须使用不同端口。

就绪检查

就绪检查(readiness)用来告诉 shutdown-check 服务什么时候启动完成。

{
  "readiness": {
    "path": "/health",
    "status": 200,
    "timeoutMs": 30000,
    "intervalMs": 100
  }
}
字段默认值允许的取值
path必填以 / 开头的本地路径
status200100 到 599 之间的 HTTP 状态码
timeoutMs10000100 到 300000
intervalMs10010 到 10000

shutdown-check 会不断轮询这个路由,直到它返回的状态码恰好等于 status。连接被拒绝或返回其他状态码时都会重试。如果进程一直在运行却始终没有就绪,结果是 SC100;如果进程在就绪前就退出了,结果是 SC101。

启用撤回就绪状态的检查后,发送 SIGTERM 之后也会使用这个路由。

测试请求

测试请求(workload)是 SIGTERM 到达时必须仍在进行中的请求。

{
  "workload": {
    "path": "/reports/export",
    "method": "POST",
    "headers": { "content-type": "application/json" },
    "body": "{\"rows\":5000}",
    "status": 200,
    "bodyIncludes": "export complete",
    "concurrent": 3,
    "started": {
      "type": "response-headers",
      "timeoutMs": 5000
    }
  }
}
字段默认值作用
path必填执行这项工作的本地路由
methodGETHTTP 方法
headers{}请求头,值必须是字符串
body无原始请求体
status200期望的最终响应状态码
bodyIncludes无响应中必须包含的文本,区分大小写
concurrent1请求数量,1 到 20
started必填用来确认测试请求已在进行中的规则

选一个可以安全调用、而且耗时足够长的路由,保证请求还没结束时信号就能到达。如果仅凭成功的状态码不足以证明工作已经完成,就加上 bodyIncludes。shutdown-check 对每个响应体最多捕获 1 MiB。

发送 SIGTERM 后,每个请求都必须在截止时间前完成。超时、请求被中断、状态码不对或响应体不对,分别会产生 SC200 到 SC203 中的相应诊断码。

启动确认

启动确认(start barrier)用来避免误判为通过:如果测试请求在信号到达前就已经结束,检查不应该通过。启动确认有两种类型,选其中一种。

response-headers

{
  "started": {
    "type": "response-headers",
    "timeoutMs": 5000
  }
}

每个测试请求都必须先收到响应头,同时响应体仍未结束。这种启动确认支持 1 到 20 个并发请求。测试路由通常会先调用 response.flushHeaders(),稍后再发送完响应体。

probe

{
  "started": {
    "type": "probe",
    "path": "/test/work-active",
    "inactiveStatus": 204,
    "activeStatus": 200,
    "timeoutMs": 5000,
    "intervalMs": 50
  }
}

发出测试请求之前,探测路由必须返回 inactiveStatus。请求还在进行时,它必须改为返回 activeStatus。这两个状态码必须不同。

probe 只支持一个测试请求,适用于工作全部完成后才发送响应头的处理函数。如果这个路由会暴露应用的内部状态,请只在测试环境中开放它。

完整的服务端示例和失败时的输出,见进行中的工作与启动确认。

关闭

shutdown 对象定义了第一次 SIGTERM 之后的关闭约定。

{
  "shutdown": {
    "signal": "SIGTERM",
    "deadlineMs": 15000,
    "exitCode": 0,
    "readinessWithdrawal": true,
    "newRequests": {
      "path": "/test/new-work",
      "rejectStatuses": [503],
      "allowConnectionRefused": true
    },
    "repeatSignalAfterMs": 500
  }
}

signal

只支持 SIGTERM。除非你想在配置里显式写明信号,否则可以省略这个字段。

deadlineMs

默认值是 10000 ms。计时从第一次发送信号开始,涵盖进行中的响应、排空流量相关的检查以及进程退出。应把它设得大于最慢的正常请求加上清理所需的时间,同时小于部署平台强制终止进程的截止时间。

exitCode

默认值是 0。进程必须以这个退出码退出,完全一致才算通过。被信号终止的进程没有退出码,因此不算匹配。

readinessWithdrawal

设为 true 时,就绪检查路由必须在截止时间前不再返回 readiness.status。返回其他状态码或拒绝连接都算通过,超时则不算。

newRequests

这项检查要求 readinessWithdrawal: true。就绪状态撤回之后、原来的测试请求仍在进行时,shutdown-check 会向 newRequests.path 发送一个 GET 请求。

如果这个请求返回的状态码在 rejectStatuses 中,或者 allowConnectionRefused 为 true 且连接被拒绝,检查通过。超时或返回正常的成功响应都会失败。

这个请求是真实发出的,所以请使用一个可以安全调用的测试路由。就绪检查与排空指南介绍了返回 503 和关闭监听这两种做法。

repeatSignalAfterMs

设置后,shutdown-check 会在工作仍在进行时再发送一次 SIGTERM。这个值必须小于 deadlineMs,也必须小于测试请求的持续时间。用它可以发现那些收到重复信号后就提前强制退出的处理函数。

一份严格的完整配置

这个示例会检查三个请求、响应内容、撤回就绪状态、拒绝新请求、重复信号、进程退出和端口关闭:

shutdown-check.json
{
  "command": ["node", "dist/server.js"],
  "cwd": ".",
  "env": { "PORT": "3510", "NODE_ENV": "production" },
  "baseUrl": "http://127.0.0.1:3510",
  "readiness": { "path": "/health", "status": 200, "timeoutMs": 30000 },
  "workload": {
    "path": "/slow",
    "status": 200,
    "bodyIncludes": "work complete",
    "concurrent": 3,
    "started": { "type": "response-headers", "timeoutMs": 5000 }
  },
  "shutdown": {
    "deadlineMs": 15000,
    "exitCode": 0,
    "readinessWithdrawal": true,
    "newRequests": {
      "path": "/test/new-work",
      "rejectStatuses": [503],
      "allowConnectionRefused": true
    },
    "repeatSignalAfterMs": 500
  }
}

测试请求保持进行的时间要足够长,覆盖就绪检查轮询、新请求和重复信号这几步,同时又要在截止时间前完成。

常见校验错误

消息修复方法
command must be a non-empty array of strings, for example ["node", "server.js"]把可执行文件和参数拆成数组
baseUrl must use http://localhost, http://127.0.0.1 or http://[::1]使用本地的普通 HTTP 地址
readiness.path must be a local path beginning with one /写成 /health,不要写 URL 或 health
workload.started.type must be response-headers or probe从支持的两种启动确认中选一种
workload.concurrent above 1 requires the response-headers start barrier, which verifies every request individually改用这种启动确认,或只发一个请求
shutdown.newRequests requires shutdown.readinessWithdrawal: true so rejection is tested after drain begins开启撤回就绪状态
shutdown.repeatSignalAfterMs must be shorter than shutdown.deadlineMs调小重复信号的延迟,或调大截止时间
v0.1 supports only SIGTERM删除 signal,或将它设为 SIGTERM

在 1.0.1 版本的包中,报错信息里的确写的是 v0.1。按症状分类的错误说明见故障排查。

可以用 TypeScript 代替 JSON 吗?

CLI 只读取 JSON。在测试或脚本中,可以用 defineConfig() 创建一个带类型的配置对象,再传给 checkShutdown():

shutdown.test.ts
import { checkShutdown, defineConfig } from "shutdown-check";
 
const config = defineConfig({
  command: ["node", "dist/server.js"],
  baseUrl: "http://127.0.0.1:3510",
  readiness: { path: "/health" },
  workload: {
    path: "/slow",
    started: { type: "response-headers" },
  },
  shutdown: { readinessWithdrawal: true },
});
 
const result = await checkShutdown(config);

这个对象遵循同样的规则和默认值。校验和结果处理见 Node API。