系列导航: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 后应:

  1. 停止接收新请求。
  2. 等待正在处理的请求完成。
  3. 停止消息监听器和调度任务。
  4. 提交或回滚事务。
  5. 关闭连接池并退出。

通过日志验证,而不是只相信配置:

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。

无感发布不是把三个探针都调大,而是让流量进入、流量排空与进程生命周期严格对齐。