系列导航:Kubernetes 重建迁移教程系列。示例应用、域名和镜像均为占位符。
滚动发布期间出现少量 Connection refused、Connect timed out,不应该简单归类为“正常”。它通常说明新 Pod 接流量太早、旧 Pod 退出太快、应用没有优雅停机,或业务实际上只有一个可用副本。
一次 Pod 替换发生了什么
sequenceDiagram
participant D as Deployment
participant N as New Pod
participant S as Service
participant O as Old Pod
D->>N: 创建新 Pod
N->>N: startupProbe
N->>N: readinessProbe
N->>S: Ready 后加入 EndpointSlice
D->>O: 请求终止旧 Pod
O->>S: readiness 变为 false
O->>O: preStop 与优雅停机
O-->>D: 在 grace period 内退出
无感发布依赖整个顺序,而不是某一个 YAML 参数。
三种探针各管一件事
Kubernetes 官方说明:配置 startupProbe 后,在它成功前不会执行 readiness 和 liveness。Liveness、Readiness 和 Startup Probes 是设计探针的基线。
startupProbe:应用是否完成初始化
适合 Spring 上下文初始化、数据库迁移、缓存预热较慢的服务:
startupProbe:
httpGet:
path: /actuator/health/readiness
port: management
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
最大启动容忍时间大约是:
initialDelaySeconds + periodSeconds × failureThreshold
上例约为 300 秒。timeoutSeconds 是单次探测超时,不应把它简单乘进总时间。
readinessProbe:此刻能不能接请求
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: management
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
readiness 失败不会重启容器,只会把 Pod 从 Service 的 EndpointSlice 中移除。数据库连接池耗尽、必要依赖不可用时,readiness 可以失败;但要避免因某个可降级依赖不可用就让整个应用进入 NotReady。
livenessProbe:进程是否已经无法自愈
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: management
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 12
10/5/12 表示每 10 秒发起一次探测,单次最多等 5 秒,连续 12 次失败后重启容器。粗略故障窗口约 120 秒,不是 60 秒。
不要让 liveness 深度访问数据库、Redis、Nacos 或外部 HTTP。依赖阻塞时,liveness 会重启所有 Pod,可能把局部故障放大成重启风暴。
先让 Spring Boot 正确优雅停机
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
应用收到 SIGTERM 后应:
- 停止接收新请求。
- 等待正在处理的请求完成。
- 停止消息监听器和调度任务。
- 提交或回滚事务。
- 关闭连接池并退出。
通过日志验证,而不是只相信配置:
kubectl -n <namespace> delete pod <test-pod>
kubectl -n <namespace> logs <test-pod> -f
terminationGracePeriodSeconds 是总预算
spec:
terminationGracePeriodSeconds: 120
它覆盖 preStop、应用优雅停机和容器退出的总时间。超过预算后,kubelet 会强制终止进程。
设置为 120 秒不代表每次删除都必须等待 120 秒;进程提前退出时,Pod 会立即结束。
如果应用最慢的请求需要 45 秒,preStop 等待 10 秒,消息监听器停止需要 20 秒,60 秒的总预算就可能过短。
preStop 用来排空,不是掩盖慢启动
最简单的做法:
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
这给 EndpointSlice 传播和上游连接池一点排空时间,但这 10 秒也属于 grace period。
更成熟的应用可以在 preStop 端点中主动完成以下操作:
- 将 readiness 置为 false。
- 停止从 Kafka/RabbitMQ 拉取新消息。
- 等待当前任务完成。
不要用 180 秒固定 sleep 代替正确的应用生命周期。
Deployment 策略决定同时有几个可用 Pod
spec:
replicas: 2
minReadySeconds: 15
progressDeadlineSeconds: 600
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
含义:
- 发布时允许临时多创建一个 Pod。
- 新 Pod Ready 之前,不允许减少现有可用副本。
- 新 Pod Ready 后还要稳定 15 秒才算可用。
前提是节点池能容纳 surge Pod。资源已满时,新 Pod 会一直处于 Pending,发布进度停滞,但旧服务仍可继续提供服务。
单副本即使使用 maxUnavailable: 0,也无法抵御节点故障;某些使用 ReadWriteOnce 本地卷的应用还无法同时启动新旧 Pod。
PDB 不是 Deployment 滚动策略
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-api
spec:
minAvailable: 1
selector:
matchLabels:
app.kubernetes.io/name: app-api
PDB 主要约束使用 Eviction API 的自愿中断,例如 kubectl drain。Kubernetes 官方明确指出,Deployment 和 StatefulSet 自身的滚动升级不受 PDB 限制;滚动升级由工作负载策略控制。Disruptions
因此同时需要:
- Deployment 的
maxSurge/maxUnavailable。 - PDB 保护节点维护。
- 反亲和或 topology spread 防止副本同节点。
一份完整模板
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-api
spec:
replicas: 2
minReadySeconds: 15
progressDeadlineSeconds: 600
strategy:
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
template:
spec:
terminationGracePeriodSeconds: 120
containers:
- name: app
image: <image-registry>/app-api:<version>@sha256:<digest>
ports:
- { name: management, containerPort: 8081 }
lifecycle:
preStop:
exec: { command: ["sh", "-c", "sleep 10"] }
startupProbe:
httpGet: { path: /actuator/health/readiness, port: management }
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: management }
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: management }
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 12
参数不能无脑复制,应根据应用真实的启动分位数、请求耗时、消息处理耗时和依赖恢复时间来计算。
如果数十个服务都要复用这套发布结构,应继续阅读统一 Spring Boot Deployment 模板:它只保留一份模板源码,用参数区分资源分档、节点池和旧应用兼容例外。
发布前容量检查
kubectl -n <namespace> get deployment app-api -o wide
kubectl describe node <target-node> | sed -n '/Allocated resources:/,$p'
kubectl get events -A --field-selector reason=FailedScheduling
发布需要额外一个 Pod 的 requests 容量。不要因为 kubectl top 当前显示使用率低,就误以为调度容量充足。
如何验证是否真正无感
在独立终端持续发请求:
while true; do
curl -fsS -o /dev/null -w '%{http_code} %{time_total}\n' \
https://app.example.com/<business-read-api>
sleep 0.2
done
开始滚动:
kubectl -n <namespace> set image deployment/app-api \
app=<image-registry>/app-api:<new-version>@sha256:<new-digest>
kubectl -n <namespace> rollout status deployment/app-api --timeout=15m
kubectl -n <namespace> get pod -l app.kubernetes.io/name=app-api -w
同时观察:
kubectl -n <namespace> get endpointslice \
-l kubernetes.io/service-name=app-api -w
kubectl -n <namespace> logs deployment/app-api --since=15m \
| grep -Ei 'timeout|refused|shutdown|killed|oom'
验收不只看 HTTP 200,还要看错误率、P95/P99、消息积压、数据库连接和线程池。
常见错误
| 现象 | 根因 | 修复方向 |
|---|---|---|
| 新 Pod Ready 后立即报错 | readiness 探针太浅,依赖尚未初始化 | 将真正必要的初始化纳入 readiness |
| 旧 Pod 仍有请求却退出 | grace period 太短或没有优雅停机 | 延长预算并验证 SIGTERM |
| 所有 Pod 同时重启 | liveness 深度访问共享中间件 | liveness 只判断进程自愈能力 |
| 新 Pod 一直 Pending | 没有 surge 容量 | 释放或增加节点容量 |
| 已配置 PDB 仍出现发布中断 | PDB 不控制 Deployment rollout | 配置滚动策略和副本数 |
完成定义
- 至少两个可用副本分布在不同节点。
- startup、readiness、liveness 各自只承担一个职责。
- 应用接收 SIGTERM 后能在 grace period 内退出。
maxUnavailable: 0,且节点有 surge 容量。- PDB 能阻止不安全的节点 drain。
- 连续业务请求在发布期间无 5xx、拒绝连接和异常超时。
- 发布失败时能使用
kubectl rollout undo恢复上一 Revision。
无感发布不是把三个探针都调大,而是让流量进入、流量排空与进程生命周期严格对齐。
DISCUSSION
讨论与反馈