先看输出的第一行。如果是 FAIL SC…,说明测试已经运行,并发现了关闭问题。如果以 shutdown-check: 开头,说明某个参数、文件或配置值无效,测试没能启动。
按这个顺序排查
- 阅读诊断码,或环境、配置错误的消息。
- 找到
check failed之前最后一个成功的事件。 - 查看
Service stderr和Service stdout。 - 先修最早出现的那个失败。
- 改下一个设置之前,先用同样的命令再跑一次。
后面的阶段可能也有问题,但 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 | 第二次信号会来得太晚 | 缩短重复发送的延迟 |
校验每次只报告一个字段。改好显示的值后再运行一次。支持的写法和取值范围见配置参考。
端口已被占用
SC001 出现在服务启动之前。说明有其他进程在 baseUrl 上做出了响应,或者无法确认端口空闲。
检查以下情况:
- 有开发服务器没关
- 另一个测试在用同一个端口
- 并行的 CI 任务共用了一份配置
- 该端口上有服务容器或代理
- 之前的包装命令留下了子进程
给测试分配一个专用端口,并把它传给服务:
{
"env": { "PORT": "3510" },
"baseUrl": "http://127.0.0.1:3510"
}如果不清楚端口被谁占用,参见 SC001。
服务在就绪前退出
SC101 表示启动的进程崩溃、结束或无法启动。
先看 Service stderr。常见原因有:
- 构建产物不存在
command或cwd指向了错误的位置- 缺少必需的环境变量
- 服务有语法错误或导入错误
- 服务尝试绑定另一个已被占用的端口
- 启动器在后台拉起真正的服务器后自己退出了
在 shutdown-check 之外,从配置的 cwd 运行完全相同的命令。确认它能持续运行、并在正确的端口上监听后,再重新运行测试。
服务一直没有就绪
SC100 表示进程一直活着,但就绪检查路由始终没有返回预期的状态码。
先检查完整的 URL:
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 - 在探测路由的状态变化之前,让测试请求的响应保持打开
- 只使用一个测试请求
不要用响应很快的健康检查接口作为测试请求。两种方式的完整示例见启动确认指南。
请求被中断
SC201 表示进行中的响应还没完成,连接就关闭了。看 process exited 这一行:
code=null, signal=SIGTERM通常说明没有生效的信号处理函数。- 收到信号后立即正常退出,往往说明过早调用了
process.exit()。 - 如果进程没有退出,可能是关闭过程中有代码销毁了 socket。
使用 server.close() 并等待进行中的请求完成。不要销毁活跃的 socket,也不要强制退出进程。Node.js 关闭指南提供了 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 通常说明启动命令是一个包装进程,真正的服务器作为它的子进程还在运行。
把:
{ "command": ["npm", "start"] }换成直接启动的命令:
{ "command": ["node", "dist/server.js"] }如果必须使用包装进程,它必须转发 SIGTERM 并等待子进程结束。容器命令应使用 exec 形式,而不是 shell 形式。
就绪检查或新请求检查失败
SC310 表示就绪检查一直报告就绪。收到 SIGTERM 后立即设置排空标志,让就绪检查路由返回 503,或者关闭监听器。
SC311 表示就绪状态变化之后,新请求仍被接受,或者超时了。应返回 newRequests.rejectStatuses 中的某个状态码(通常是 503);在允许这种行为时,也可以直接拒绝连接。
SC312 表示新请求还没来得及测试,原来的测试请求就已经结束了。把测试请求的耗时拉长,或者更早撤回就绪状态。
完整的服务器示例和通过时的时间线,见就绪检查与排空。
结果是 SC999
SC999 是意外的内部错误或操作系统错误。先读原始错误消息。如果你直接调用 runCheck(),请先用 parseConfig() 校验配置。
如果有效的配置反复返回 SC999,请提交 issue,并附上:
- shutdown-check 和 Node.js 的版本
- 操作系统
- 去掉敏感信息后的配置
- 完整的结果消息和时间线
- 最小复现(如果可以提供)
测试在 CI 中不稳定
在把所有超时都调大之前,先检查这些:
- 给每个并行任务分配单独的端口。
- 先构建服务,再运行测试。
- 使用隔离的数据库和队列资源。
- 让测试请求明显长于启动确认和信号延迟的时间。
- 把测试运行器的超时设得比就绪、启动确认和关闭的超时都大。
- 不要依赖互不相关的进程之间固定的毫秒级先后顺序。
对比通过和失败两次运行的时间线,找出耗时发生变化的那个阶段。