Shutdown Check

搜索文档

查找页面或章节

根据第一条错误和最后一个时间线事件,找到服务中需要处理的部分。

先看输出的第一行。如果是 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 existsinit 不会覆盖已有文件编辑或重命名已有文件,或换一个文件名
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 任务共用了一份配置
  • 该端口上有服务容器或代理
  • 之前的包装命令留下了子进程

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

shutdown-check.json
{
  "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 中不稳定

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

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

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