# 方案对比

> 优雅关闭测试工具对比：把 shutdown-check 与 terminus、http-terminator、单元测试、kill 加 curl 脚本以及只在预发布环境检查的做法放在一起比较。

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

关闭库和关闭测试解决的是不同的问题。库负责给你的服务加上信号处理和清理逻辑；shutdown-check 则启动真实的服务，从外部测试这些逻辑是否真的生效。

## shutdown-check 适合做什么？

如果你已经有信号处理函数（不管是自己写的、框架提供的，还是来自某个库），并且想验证进程完整的生命周期，就可以用 shutdown-check。

它会检查：

- 接近生产环境的启动命令
- 就绪检查
- 一个真实的、正在处理的 HTTP 请求
- `SIGTERM` 的送达
- 响应是否完成
- 可选的流量排空
- 进程退出和退出码
- 最终端口是否关闭

它不会替你注册信号处理函数，也不会替你关闭应用的资源。

## 关闭库

`@godaddy/terminus`、`http-terminator`、`lightship`、`close-with-grace` 这类库运行在应用内部。它们可能会注册信号监听器、关闭 HTTP 服务器、管理超时，并提供清理钩子。

需要一套关闭实现时，就用库；配置好之后，再用 shutdown-check 测试最终效果。库本身没问题，也可能被用错：关闭的可能不是正确的 server 对象，清理的顺序可能不对，或者生产环境的启动命令可能让信号到不了 Node.js。

## 框架的关闭钩子

Fastify、NestJS 等框架都提供了关闭相关的 API。它们同样是实现，而不是端到端测试。

框架层面的测试通常直接调用 close 方法，而部署时是向进程发送信号。shutdown-check 把信号监听器、启动命令、框架、打开的 socket 和最终退出放在一起覆盖。

## 单元测试

单元测试可以调用关闭函数，并断言 mock 的依赖都已经关闭。

优点：

- 快
- 失败时能精确定位
- 容易覆盖错误分支
- 适合测试数据库和队列的清理逻辑

局限：

- 通常没有真实的子进程
- 没有操作系统信号
- HTTP 连接是 mock 的
- 无法证明生产环境的启动命令会退出
- 不检查是否留下孤儿子进程

单元测试要保留。再补一个黑盒测试，覆盖单元测试测不到的进程行为。

## 用 kill 和 curl 写的 shell 脚本

自己写脚本也可以：启动应用，用 `curl` 发请求，发送 `kill -TERM`，再检查退出情况。

这种做法行得通，但很难做到可靠的同步。固定的 sleep 并不能证明信号到达时请求正在处理。脚本还得自己处理并发请求、响应体、超时、进程组、清理、退出码和 CI 报告。

shutdown-check 把这些细节封装成一份可重复运行的配置和一套固定不变的诊断码。

## 预发布环境与 Kubernetes 测试

集群层面的行为只能在预发布环境（staging）里测试，比如 endpoint 的传播、ingress 的时序、sidecar 和 `preStop` 钩子。

但预发布测试也更慢，更难复现。终止时不一定有请求正在处理，而一次成功的发布也无法证明没有丢请求。

部署前，先用 shutdown-check 测试服务本身的行为，结果稳定、可复现；之后再到预发布环境测试基础设施层面的行为。

## 对比表

| 能力                                    | shutdown-check | 关闭库           | 单元测试   | Shell 脚本   | 预发布测试   |
| --------------------------------------- | -------------- | ---------------- | ---------- | ------------ | ------------ |
| 添加关闭逻辑                            | 否             | 是               | 否         | 否           | 否           |
| 运行真实的启动命令                      | 是             | 不适用           | 通常否     | 是           | 是           |
| 发送真实的 `SIGTERM`                    | 是             | 负责处理         | 通常否     | 是           | 是           |
| 证明发信号前请求正在处理                | 是             | 否               | 借助 mock  | 困难         | 困难         |
| 验证响应状态码和响应体                  | 是             | 否               | 是         | 可以做到     | 可以做到     |
| 检查就绪状态撤回                        | 可选           | 可能提供实现     | 借助 mock  | 可以做到     | 是           |
| 检查进程退出和最终端口                  | 是             | 否               | 否         | 可以做到     | 间接         |
| 直接检查数据库和队列                    | 否             | 可能负责关闭     | 是         | 需自行实现   | 需自行实现   |
| 覆盖集群路由和 sidecar                  | 否             | 否               | 否         | 否           | 是           |
| 输出固定的失败码                        | 是             | 否               | 测试名称   | 需自行实现   | 需自行实现   |

## 应该选哪种方式？

大多数服务可以这样做：

1. 用框架自带的功能、某个库或一个简单的处理函数来实现关闭。
2. 用单元测试覆盖应用特有的清理逻辑和错误路径。
3. 在本地和 CI 中运行 shutdown-check，测试进程和 HTTP 的生命周期。
4. 在预发布环境中测试 Kubernetes、代理和负载均衡器的行为。

这几层是互补的。只用其中一种测试替代全部，会让一些重要行为得不到验证。

## shutdown-check 不够用的情况

如果正确性依赖以下内容，还需要补充其他测试：

- HTTP 响应之后的数据库事务
- 队列消息确认（ack）
- 后台任务
- WebSocket 或 HTTP/2
- 与远程服务的协调
- 容器或集群的终止顺序

具体边界见[兼容性说明](https://shutdown.jscrate.dev/zh/docs/compatibility)。

## 相关内容

- [shutdown-check 的工作原理](https://shutdown.jscrate.dev/zh/docs/how-it-works)
- [Node.js 中的优雅关闭](https://shutdown.jscrate.dev/zh/docs/guides/graceful-shutdown-nodejs)
- [Kubernetes 与容器](https://shutdown.jscrate.dev/zh/docs/guides/kubernetes)
- [兼容性与限制](https://shutdown.jscrate.dev/zh/docs/compatibility)
- [在 CI 中运行](https://shutdown.jscrate.dev/zh/docs/ci)
