# 工作原理

> shutdown-check 如何测试优雅关闭：启动你真实的服务，保持一个请求处理中，发送 SIGTERM，再检查响应、进程退出和端口。

Source: https://shutdown.jscrate.dev/zh/docs/how-it-works
Last updated: 2026-09-23

shutdown-check 像部署平台一样对待你的服务：运行真实的启动命令，发送 HTTP 流量，发出 `SIGTERM`，然后在进程外部观察结果。它不会 import 你的应用，也不会直接调用关闭处理函数。

## 什么是黑盒关闭测试？

黑盒关闭测试只使用服务对外可见的行为：启动命令、端口、HTTP 路由、信号处理和退出状态。单元测试常常漏掉的问题都能覆盖到，包括 shell 包装进程、框架行为、真实的 socket、进程退出和子进程。

代价是可见性有限。shutdown-check 能看到 HTTP 响应和进程事件，但除非响应本身能证明，否则它无法知道内部的数据库写入或队列消息是否已经完成。具体边界见[兼容性与限制](https://shutdown.jscrate.dev/zh/docs/compatibility)。

## 一次运行会经过哪些阶段？

各阶段总是按以下顺序执行：

| 阶段  | shutdown-check 做什么                     | 主要失败码    |
| ----- | ----------------------------------------- | ------------- |
| 1     | 确认端口空闲                              | `SC001`       |
| 2     | 启动配置的命令                            | `SC002`       |
| 3     | 等待服务就绪                              | `SC100–101`   |
| 4     | 发出测试请求，并确认请求正在处理          | `SC110–111`   |
| 5     | 确认进程和测试请求仍然存活                | `SC112`       |
| 6     | 发送 `SIGTERM`                            | `SC113`       |
| 7     | 可选：重复发送信号                        | `SC114`       |
| 8     | 可选：检查就绪状态撤回和新请求拒绝        | `SC310–312`   |
| 9     | 等待进行中的响应完成                      | `SC200–203`   |
| 10    | 检查进程退出                              | `SC300–301`   |
| 11    | 确认端口已关闭                            | `SC302`       |

所有必需的等待结束后，报告的第一个失败就是最早出现的有效问题。通过时的诊断码是 [SC000](https://shutdown.jscrate.dev/zh/docs/codes/sc000)。

## 1. 检查端口是否空闲

启动服务之前，shutdown-check 会向就绪检查 URL 发一个短请求，这时连接必须被拒绝。只要收到任何响应、超时，或者出现无法判断的网络错误，都说明无法确认端口空闲，结果为 [SC001](https://shutdown.jscrate.dev/zh/docs/codes/sc001)。

这样可以避免请求打到一个已经在运行的服务上，得出虚假的通过结果。同时也让并行测试的结果可预期：每个测试都需要自己的端口。

## 2. 启动真实命令

命令数组直接启动，不经过 shell。命令在独立的进程组中运行，`cwd` 和 `env` 取自配置。

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

尽可能使用与生产环境相同的入口。直接启动服务，还能避开那些吞掉 `SIGTERM`、或者先于子进程中的服务退出的包装进程。

## 3. 等待服务就绪

shutdown-check 会轮询 `baseUrl + readiness.path`，直到拿到预期的状态码。连接被拒绝或返回其他状态码时会一直重试，直到超过 `readiness.timeoutMs`。

- 进程一直存活，但始终没有就绪：[SC100](https://shutdown.jscrate.dev/zh/docs/codes/sc100)。
- 进程退出或无法启动：[SC101](https://shutdown.jscrate.dev/zh/docs/codes/sc101)。

就绪检查通过之前不会发送任何测试请求。这样可以把启动失败和关闭失败区分开。

## 4. 确认请求正在处理

测试会发出一个或多个测试请求，然后等待配置的启动确认（start barrier）。

### response-headers 启动确认

每个请求都必须在响应体尚未结束时收到响应头。适用于流式路由，或者在处理完成前就先发出响应头的可控接口。

### probe 启动确认

测试请求保持处理中时，另一个独立路由必须从 `inactiveStatus` 变为 `activeStatus`。适用于处理函数要等全部工作完成后才发送响应的场景。

如果探测路由一开始就处于活跃状态，结果为 [SC110](https://shutdown.jscrate.dev/zh/docs/codes/sc110)；如果无法确认请求正在处理，结果为 [SC111](https://shutdown.jscrate.dev/zh/docs/codes/sc111)。完整示例见[进行中的工作指南](https://shutdown.jscrate.dev/zh/docs/in-flight-work)。

## 5. 再次确认进程和请求

从启动确认通过到发出信号，中间有一个很短的间隔。shutdown-check 会确认在这段间隔里，服务和每个测试请求都仍然存活。只要有一个已经结束，结果就是 [SC112](https://shutdown.jscrate.dev/zh/docs/codes/sc112)。

这样，即使测试请求只是勉强慢到能通过启动确认，测试也不会因此误判为通过。

## 6. 发送 SIGTERM

shutdown-check 把 `SIGTERM` 发给它启动的那个进程本身。这一阶段不会向整个进程组发信号。正因为如此，包装进程的问题会暴露出来：启动器必须把信号转发给真正的服务。

如果操作系统拒绝发送信号，结果为 [SC113](https://shutdown.jscrate.dev/zh/docs/codes/sc113)。发送成功后，关闭截止时间开始计时，时间线记录 `signal sent`。

## 7. 按需重复发送信号

设置了 `repeatSignalAfterMs` 时，只有在进程和最初的请求都仍在处理中时，shutdown-check 才会再发一次 `SIGTERM`。这是为了确认第二个信号不会让进程提前退出。

如果无法测试或无法发送重复信号，结果为 [SC114](https://shutdown.jscrate.dev/zh/docs/codes/sc114)。重复发送的延迟要比测试请求的耗时和 `shutdown.deadlineMs` 都短。

## 8. 检查流量排空

这些检查都是可选的。

设置 `readinessWithdrawal: true` 后，shutdown-check 会在发出信号后轮询就绪检查路由。返回非就绪状态码或连接被拒绝，都算作已撤回就绪状态；超时不算。如果就绪状态一直没有变化，结果为 [SC310](https://shutdown.jscrate.dev/zh/docs/codes/sc310)。

设置 `newRequests` 后，测试接着会在最初的请求仍在处理时，再发一个新的 `GET`。这个请求必须返回配置的拒绝状态码；如果配置允许，也可以是连接被拒绝。

- 新请求被接受或超时：[SC311](https://shutdown.jscrate.dev/zh/docs/codes/sc311)。
- 最初的请求在拒绝测试之前就已结束：[SC312](https://shutdown.jscrate.dev/zh/docs/codes/sc312)。

在这段时间内返回 `503` 的服务示例，见[就绪检查与排空](https://shutdown.jscrate.dev/zh/docs/guides/readiness-and-draining)。

## 9. 等待每个响应完成

每个测试请求都必须在 `shutdown.deadlineMs` 之前完成，并返回配置的状态码，以及可选的响应体文本。

| 结果                                            | 诊断码                     |
| ----------------------------------------------- | -------------------------- |
| 请求一直没有结束                                | [SC200](https://shutdown.jscrate.dev/zh/docs/codes/sc200) |
| 连接被重置或提前关闭                            | [SC201](https://shutdown.jscrate.dev/zh/docs/codes/sc201) |
| 最终状态码与 `workload.status` 不一致           | [SC202](https://shutdown.jscrate.dev/zh/docs/codes/sc202) |
| 响应体中不包含 `workload.bodyIncludes`          | [SC203](https://shutdown.jscrate.dev/zh/docs/codes/sc203) |

并发请求会逐个检查。按请求顺序，第一个失败的请求决定诊断码。

## 10. 检查进程退出

服务必须在同一个关闭截止时间之前退出。

- 进程仍在运行：[SC300](https://shutdown.jscrate.dev/zh/docs/codes/sc300)。
- 退出码与 `shutdown.exitCode` 不一致，或者进程被信号终止：[SC301](https://shutdown.jscrate.dev/zh/docs/codes/sc301)。

默认的预期退出码是 `0`。对进程管理器和监控系统来说，一次计划内的关闭通常应该表现为正常退出。

## 11. 确认端口已关闭

启动的进程退出后，shutdown-check 会再请求一次就绪检查 URL，这时连接必须被拒绝。如果仍有响应，多半是某个子进程中的服务还在运行，结果为 [SC302](https://shutdown.jscrate.dev/zh/docs/codes/sc302)。

测试正是靠这一步发现 `npm start`、shell 脚本或其他启动器自己退出了，却没有停掉真正的服务。

## 失败后如何清理？

无论结果如何，shutdown-check 都会关闭自己发起的请求并清除定时器。如果服务或子进程还在，工具会向它创建的进程组发送 `SIGKILL`。清理可以避免失败的测试一直占用端口，影响下一次运行。

清理不等于关闭通过。只有服务在需要清理之前自己完成工作、以预期的退出码退出并关闭端口，才算通过。

## 怎样使用时间线？

从上往下看，找到 `check failed` 之前最后一个成功的事件。例如：

- 没有 `service ready`：启动或就绪检查失败
- 没有 `work confirmed active`：启动确认失败
- 有 `signal sent`，但之后请求一直没有完成：排空过程卡住了
- 有 `process exited`，但端口仍然开着：有子进程中的服务没有退出

每个事件以及 JSON 中可用的字段，见[输出参考](https://shutdown.jscrate.dev/zh/docs/output)。

## 相关内容

- [快速开始](https://shutdown.jscrate.dev/zh/docs/quick-start)
- [配置参考](https://shutdown.jscrate.dev/zh/docs/configuration)
- [诊断码](https://shutdown.jscrate.dev/zh/docs/codes)
- [故障排查](https://shutdown.jscrate.dev/zh/docs/troubleshooting)
- [Node.js 中的优雅关闭](https://shutdown.jscrate.dev/zh/docs/guides/graceful-shutdown-nodejs)
