在 Kubernetes 中对 Node.js 做优雅关闭(graceful shutdown),服务器必须停止接收流量、完成进行中的请求,并在 Pod 的宽限期(grace period)结束前退出。shutdown-check 在本地或 CI 中测试的是服务端这一部分,不模拟 Kubernetes 控制平面。
Pod 停止时会发生什么?
在删除 Pod、滚动更新、缩容或节点排空(node drain)时:
- Kubernetes 把 Pod 标记为 terminating 状态。
- 开始移除 endpoint,让 Service 和负载均衡器不再把流量路由到这个 Pod。
- kubelet 执行配置好的
preStop钩子。 - 容器运行时向容器的主进程发送
SIGTERM。 - Kubernetes 等待容器退出。
terminationGracePeriodSeconds到期时,仍在运行的进程会收到SIGKILL。
endpoint 更新和容器终止几乎同时开始。kube-proxy、ingress 控制器和外部负载均衡器都需要一段时间才能感知到变化,所以收到信号之后仍可能有流量进来。
服务需要做什么?
服务器应当:
- 立即改变就绪状态
- 拒绝仍然到达的新工作
- 保住排空开始前已经进入的请求
- 等进行中的请求处理函数结束后,再关闭共享资源
- 在宽限期结束前以退出码
0退出
如果每个客户端都会重试,且流量路由更新得很快,立即关闭监听器也许就够了。更稳妥的零停机部署通常会留一个短暂的排空窗口:在关闭监听器之前先返回 503。
配置就绪探针和 preStop
spec:
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: example/api:1.4.2
command: ["node", "dist/server.js"]
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 2
failureThreshold: 1
lifecycle:
preStop:
exec:
command: ["sleep", "5"]preStop 的延迟让 endpoint 更新有时间在 SIGTERM 到来之前传播出去。这段延迟占用的是 30 秒宽限期的一部分,不会额外增加时间。
exec 形式要求镜像里有 sleep 可执行文件。部分 Kubernetes 版本提供内置的 sleep 动作。依赖它之前,先确认你的集群支持。
应用层面的排空可以代替 preStop,也可以与它配合使用:SIGTERM 一到,就绪检查路由就返回 503,服务器再继续监听一小段时间,然后才调用 server.close()。
Kubernetes 与 shutdown-check 的配置对应
| Kubernetes 设置或行为 | shutdown-check 设置 |
|---|---|
容器的 command | command |
readinessProbe.httpGet.path | readiness.path |
| Pod 宽限期 | shutdown.deadlineMs 的上限 |
preStop 时长 | 要从服务器可用时间中扣除的时长 |
| 终止期间到达的请求 | shutdown.newRequests |
| 最慢的正常业务请求 | workload |
假设宽限期是 30 秒、preStop 是 5 秒,那么把测试的截止时间设为 20 秒,大约还留有 5 秒的安全余量:
{
"command": ["node", "dist/server.js"],
"env": { "NODE_ENV": "production", "PORT": "3000" },
"baseUrl": "http://127.0.0.1:3000",
"readiness": { "path": "/health" },
"workload": {
"path": "/test/slow",
"bodyIncludes": "work complete",
"concurrent": 3,
"started": { "type": "response-headers" }
},
"shutdown": {
"deadlineMs": 20000,
"readinessWithdrawal": true,
"newRequests": {
"path": "/test/new-work",
"rejectStatuses": [503]
},
"repeatSignalAfterMs": 1000
}
}shutdown-check 从 SIGTERM 开始计时,所以它既不执行 preStop 钩子,也不计算钩子的耗时。截止时间应当设为钩子执行完之后留给应用的时间。
为什么 PID 1 很重要?
容器的启动命令会成为 PID 1。PID 1 在信号和子进程方面的行为与普通进程不同,所以要谨慎选择启动命令。
使用 exec 形式
推荐的 Dockerfile 写法:
CMD ["node", "dist/server.js"]避免使用 shell 形式:
CMD node dist/server.jsshell 形式会以 /bin/sh -c 作为 PID 1 启动,而 shell 不一定会把 SIGTERM 转发给 Node.js。
避免不必要的包管理器包装
下面的写法会在 Kubernetes 和服务器之间多出一个进程:
CMD ["npm", "start"]直接启动 node,信号由谁处理就一目了然。如果必须使用包装进程,包装进程就必须转发信号,并等待子进程退出。
考虑使用精简的 init 进程
tini 这类 init 进程可以转发信号,并回收成为孤儿的子进程。应用会创建子进程时,它很有用。要明确地配置它,并测试生产环境实际使用的容器启动命令。
排查包装进程的问题
在 shutdown-check 中使用生产环境的启动命令。在构建镜像之前,检查结果往往就能暴露问题:
在容器内运行 shutdown-check
只要 Linux 镜像里装有 Node.js 22 或更高版本以及这个包,你就可以在镜像内运行这个工具。shutdown-check 会把配置的服务作为子进程启动,并通过回环地址(loopback)连接它。
这样可以验证:
- 构建出的镜像能启动服务
- 运行所需的文件和环境变量都在
- Node.js 进程能处理
SIGTERM - 本地进行中的 HTTP 请求能排空
- 进程退出,端口关闭
这样无法验证:
- Kubernetes 移除 endpoint 的过程
- 真实的
preStop钩子 - ingress 或外部负载均衡器的时序
- sidecar 的终止顺序
- 特定集群中的宽限期行为
这些集成层面的检查,请在预发布(staging)集群中进行。
处理 sidecar 和服务网格
sidecar 代理可能独立于 Node.js 进程,自行继续或停止转发流量。shutdown-check 直接与本地服务通信,不模拟 sidecar 的关闭顺序。
使用服务网格时,请确认:
- 就绪状态同时反映代理和应用的状态
- 应用排空期间,代理仍会继续转发已有连接
- sidecar 的终止不会缩短应用的截止时间
- 重试不会掩盖反复出现的失败
设置合理的宽限期
把整个终止窗口的时间都算进去:
preStop + 路由更新延迟 + 最慢的进行中请求 + 资源清理 + 余量如果总和超过 terminationGracePeriodSeconds,Kubernetes 会在服务完成关闭之前发送 SIGKILL。调大宽限期可以是合理的做法,但也要修复那些可能永远挂起的处理函数、查询或清理操作。