# 故障排查

> 按症状排查 shutdown-check 的问题：配置错误、端口被占用、服务一直不就绪、没有进行中的工作、请求挂起或被中断。

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

先看输出的第一行。如果是 `FAIL SC…`，说明测试已经运行，并发现了关闭问题。如果以 `shutdown-check:` 开头，说明某个参数、文件或配置值无效，测试没能启动。

## 按这个顺序排查

1. 阅读诊断码，或环境、配置错误的消息。
2. 找到 `check failed` 之前最后一个成功的事件。
3. 查看 `Service stderr` 和 `Service stdout`。
4. 先修最早出现的那个失败。
5. 改下一个设置之前，先用同样的命令再跑一次。

后面的阶段可能也有问题，但 shutdown-check 报告的是第一个有用的失败。每次只改一处，结果才清晰。

## CLI 以退出码 2 退出

退出码 `2` 表示关闭测试根本没有运行。按 `shutdown-check:` 后面打印的消息修复即可。

| 消息或模式                                                                 | 含义                                   | 修复方法                                  |
| -------------------------------------------------------------------------- | -------------------------------------- | ----------------------------------------- |
| `Cannot read JSON config … ENOENT`                                         | 文件路径错误                           | 在正确的目录下运行，或传入 `--config`     |
| `Cannot read JSON config … SyntaxError`                                    | 文件不是有效的 JSON                    | 删除注释、末尾逗号或错误的引号            |
| `Unknown command "run"`                                                    | 不支持这个命令名                       | 使用 `init` 或 `test`                     |
| `Unknown option "--verbose"`                                               | 不支持这个参数                         | 查看 `shutdown-check --help`              |
| `--config requires a file path`                                            | 参数缺少值                             | 补上配置文件路径                          |
| `--junit requires a file path`                                             | 参数缺少值                             | 补上报告路径                              |
| `EEXIST: file already exists`                                              | `init` 不会覆盖已有文件                | 编辑或重命名已有文件，或换一个文件名      |
| `command must be a non-empty array of strings`                             | 缺少 `command`，或写成了一个字符串     | 使用 `["node", "dist/server.js"]`         |
| `baseUrl must use http://localhost, http://127.0.0.1 or http://[::1]`      | URL 是远程地址、HTTPS 或 `0.0.0.0`     | 使用受支持的本地 HTTP 源                  |
| `readiness.path must be a local path beginning with one /`                 | 路由缺少开头的 `/`，或写成了完整 URL   | 使用 `/health`                            |
| `workload.concurrent above 1 requires the response-headers start barrier…` | 探测路由无法跟踪多个请求               | 只发一个请求，或使用 `response-headers`   |
| `shutdown.newRequests requires shutdown.readinessWithdrawal: true…`        | 没有排空阶段，就无法确定何时测试拒绝   | 启用撤回就绪状态                          |
| `shutdown.repeatSignalAfterMs must be shorter than shutdown.deadlineMs`    | 第二次信号会来得太晚                   | 缩短重复发送的延迟                        |

校验每次只报告一个字段。改好显示的值后再运行一次。支持的写法和取值范围见[配置参考](https://shutdown.jscrate.dev/zh/docs/configuration#common-validation-errors)。

## 端口已被占用

`SC001` 出现在服务启动之前。说明有其他进程在 `baseUrl` 上做出了响应，或者无法确认端口空闲。

检查以下情况：

- 有开发服务器没关
- 另一个测试在用同一个端口
- 并行的 CI 任务共用了一份配置
- 该端口上有服务容器或代理
- 之前的包装命令留下了子进程

给测试分配一个专用端口，并把它传给服务：

```json title="shutdown-check.json"
{
  "env": { "PORT": "3510" },
  "baseUrl": "http://127.0.0.1:3510"
}
```

如果不清楚端口被谁占用，参见 [SC001](https://shutdown.jscrate.dev/zh/docs/codes/sc001)。

## 服务在就绪前退出

`SC101` 表示启动的进程崩溃、结束或无法启动。

先看 `Service stderr`。常见原因有：

- 构建产物不存在
- `command` 或 `cwd` 指向了错误的位置
- 缺少必需的环境变量
- 服务有语法错误或导入错误
- 服务尝试绑定另一个已被占用的端口
- 启动器在后台拉起真正的服务器后自己退出了

在 shutdown-check 之外，从配置的 `cwd` 运行完全相同的命令。确认它能持续运行、并在正确的端口上监听后，再重新运行测试。

## 服务一直没有就绪

`SC100` 表示进程一直活着，但就绪检查路由始终没有返回预期的状态码。

先检查完整的 URL：

```text
baseUrl + readiness.path
http://127.0.0.1:3510 + /health
http://127.0.0.1:3510/health
```

再确认：

- 应用监听的是这个主机和端口
- 路由返回 `readiness.status`，通常是 `200`
- 路由没有被身份验证拦住
- 启动能在 `readiness.timeoutMs` 内完成
- 服务不是只监听在 Unix socket 上

只有确认启动确实很慢，才去调大超时。

## 请求从未处于进行中

`SC111` 表示在 shutdown-check 证明请求处于进行中之前，请求就已经结束了；或者启动确认始终没有进入活跃状态。

使用 `response-headers` 时：

- 在工作完成之前发送响应头
- 调用 `response.flushHeaders()`，或提前写出一段响应体
- 让响应体保持打开足够长的时间，覆盖到 `SIGTERM` 发出
- `concurrent` 大于 1 时也使用同样的启动确认

使用 `probe` 时：

- 请求开始前返回 `inactiveStatus`
- 只在操作运行期间返回 `activeStatus`
- 在探测路由的状态变化之前，让测试请求的响应保持打开
- 只使用一个测试请求

不要用响应很快的健康检查接口作为测试请求。两种方式的完整示例见[启动确认指南](https://shutdown.jscrate.dev/zh/docs/in-flight-work)。

## 请求被中断

`SC201` 表示进行中的响应还没完成，连接就关闭了。看 `process exited` 这一行：

- `code=null, signal=SIGTERM` 通常说明没有生效的信号处理函数。
- 收到信号后立即正常退出，往往说明过早调用了 `process.exit()`。
- 如果进程没有退出，可能是关闭过程中有代码销毁了 socket。

使用 `server.close()` 并等待进行中的请求完成。不要销毁活跃的 socket，也不要强制退出进程。[Node.js 关闭指南](https://shutdown.jscrate.dev/zh/docs/guides/graceful-shutdown-nodejs)提供了 node:http、Express 和 Fastify 的处理函数示例。

## 请求一直没有完成

`SC200` 表示响应一直保持打开，直到 `shutdown.deadlineMs`。

在以下位置加日志：

- 测试请求的处理函数
- 数据库和上游调用
- 锁和队列
- 每个关闭步骤的开始和结束
- `server.close()` 的回调

留意循环等待。常见的例子是：进行中的请求还在用数据库连接池，连接池就被关闭了。给操作加上超时，并在 HTTP 请求排空之后再关闭共享资源。

只有当操作本来就需要更长时间，并且新的截止时间仍在平台的关闭时限之内时，才调大截止时间。

## 响应状态码或响应体不对

`SC202` 表示最终状态码与 `workload.status` 不同。`SC203` 表示状态码匹配，但响应体中不包含 `workload.bodyIncludes`。

先正常调用一次测试请求，记下成功时的响应。然后检查：

- 关闭用的中间件只拒绝新请求，不拒绝进行中的请求
- 配置的预期状态码是正确的
- 响应体标记是稳定的，并注意它区分大小写
- 路由没有以 `200` 状态码返回错误页
- 响应体标记出现在前 1 MiB 之内

标记要能证明工作已经完成，比如 `"export complete"`，不要用时间戳或生成的 ID。

## 进程没有退出

`SC300` 表示请求都已完成，但 Node.js 还有未关闭的句柄。

常见的句柄有：

- HTTP 服务器
- 数据库连接池
- 队列消费者
- interval 和定时器
- 文件监听器
- 后台 worker
- 包装进程

给每个清理步骤打日志。关闭资源、清除 interval，并给可能阻塞的清理加上超时。让 Node.js 在事件循环为空时自然退出，不要调用 `process.exit()`。

## 进程退出码不对

`SC301` 会拿实际的退出情况和 `shutdown.exitCode` 比较。

- 非零退出码通常来自失败的清理或 `process.exitCode`。
- `code=null, signal=SIGTERM` 说明进程是被信号本身结束的。
- 如果是其他信号，可能是崩溃或被外部杀掉了。

在 stderr 中找原始错误。除非你的服务有意约定使用其他退出码，否则预期退出码保持为 `0`。

## 进程退出了，但端口仍然开着

`SC302` 通常说明启动命令是一个包装进程，真正的服务器作为它的子进程还在运行。

把：

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

换成直接启动的命令：

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

如果必须使用包装进程，它必须转发 `SIGTERM` 并等待子进程结束。容器命令应使用 exec 形式，而不是 shell 形式。

## 就绪检查或新请求检查失败

`SC310` 表示就绪检查一直报告就绪。收到 `SIGTERM` 后立即设置排空标志，让就绪检查路由返回 `503`，或者关闭监听器。

`SC311` 表示就绪状态变化之后，新请求仍被接受，或者超时了。应返回 `newRequests.rejectStatuses` 中的某个状态码（通常是 `503`）；在允许这种行为时，也可以直接拒绝连接。

`SC312` 表示新请求还没来得及测试，原来的测试请求就已经结束了。把测试请求的耗时拉长，或者更早撤回就绪状态。

完整的服务器示例和通过时的时间线，见[就绪检查与排空](https://shutdown.jscrate.dev/zh/docs/guides/readiness-and-draining)。

## 结果是 SC999

`SC999` 是意外的内部错误或操作系统错误。先读原始错误消息。如果你直接调用 `runCheck()`，请先用 `parseConfig()` 校验配置。

如果有效的配置反复返回 `SC999`，请提交 issue，并附上：

- shutdown-check 和 Node.js 的版本
- 操作系统
- 去掉敏感信息后的配置
- 完整的结果消息和时间线
- 最小复现（如果可以提供）

## 测试在 CI 中不稳定

在把所有超时都调大之前，先检查这些：

1. 给每个并行任务分配单独的端口。
2. 先构建服务，再运行测试。
3. 使用隔离的数据库和队列资源。
4. 让测试请求明显长于启动确认和信号延迟的时间。
5. 把测试运行器的超时设得比就绪、启动确认和关闭的超时都大。
6. 不要依赖互不相关的进程之间固定的毫秒级先后顺序。

对比通过和失败两次运行的时间线，找出耗时发生变化的那个阶段。

## 相关内容

- [诊断码](https://shutdown.jscrate.dev/zh/docs/codes)
- [配置参考](https://shutdown.jscrate.dev/zh/docs/configuration)
- [输出与报告](https://shutdown.jscrate.dev/zh/docs/output)
- [测试如何运行](https://shutdown.jscrate.dev/zh/docs/how-it-works)
- [用 node:test 和 Vitest 测试](https://shutdown.jscrate.dev/zh/docs/guides/test-runners)
