237|Kubernetes 健康检查与发布:探针、滚动更新、PDB 和优雅终止

DevOps、容器与云原生约 21 分钟阅读12,238 字符
知识库文档 238 / 251

阅读时间:约 45 分钟 | 难度:中级 → 高级 | 适用对象:Java 后端工程师、DevOps/SRE 工程师、架构师


一、为什么需要健康检查与精细发布?

在 Kubernetes 上运行生产级 Java 应用,仅仅把 Pod 拉起来是不够的。一个 Pod 进入了 Running 状态,并不意味着它已经准备好接受流量——Spring 容器可能还在初始化、数据库连接池可能还没建立、缓存预热可能还没完成。同样,当你执行滚动更新时,如果不加控制,新 Pod 还没准备好就被挂上流量,老 Pod 又已经被干掉了,造成请求失败。

Kubernetes 提供了一套完整的机制来应对这些场景:

  • 健康检查(Probes):让 K8s 知道 Pod 什么时候"真的"可以干活、什么时候"真的"出问题了、什么时候该重启
  • 滚动更新(Rolling Update):用一种可控的方式,逐步替换旧版本 Pod
  • Pod 中断预算(PDB):在集群维护或缩容时,保证至少有一定数量的 Pod 存活
  • 优雅终止(Graceful Shutdown):让 Pod 在被干掉之前,有足够的时间把手头的工作做完、把连接关掉

下面我们逐一深入。


二、三种探针:Liveness、Readiness、Startup

2.1 探针的本质

探针(Probe)是 kubelet 对 Pod 内容器执行的周期性诊断。kubelet 根据探针的返回结果决定是重启容器、还是把 Pod 从 Service 的 Endpoints 中摘除。

三种探针各有分工

文章配图
文章配图
:

探针 用途 失败后果
Liveness Probe 判断容器是否还活着(是否需要重启) 重启容器
Readiness Probe 判断容器是否准备好接收流量 从 Service Endpoints 中移除
Startup Probe 判断容器是否已完成启动 启动期间失败则重启;成功后 Liveness 接管

2.2 Liveness Probe:你死了吗?

Liveness 回答的是"进程是否还处于健康状态"——注意,不是"进程是否还在",OOM 或进程退出 kubelet 本来就会重启。Liveness 解决的是进程还活着但已经废了的场景:死锁、无限循环卡死、JVM Full GC 导致 STW 过长等。

三种探测方式:

code
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3
  • httpGet:最常用,应用暴露一个健康检查端点
  • tcpSocket:检查端口是否可连接,适用于非 HTTP 服务
  • exec:在容器内执行命令,根据退出码判断

关键参数:

  • initialDelaySeconds:容器启动后多久开始探测(给应用启动时间)
  • periodSeconds:探测间隔
  • failureThreshold:连续失败多少次才算失败
  • timeoutSeconds:单次探测超时

2.3 Readiness Probe:你能干活了吗?

Readiness 回答的是"容器是否准备好接收请求"。一个典型的 Spring Boot 应用,Running 状态可能在 3 秒内就达到了,但真正能处理请求可能要 30 秒——数据库连接、缓存加载、RPC 客户端初始化都需要时间。

code
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 3

Readiness 与 Liveness 的核心区别:

  • Readiness 失败 → Pod 从 Service 摘除,但不重启容器
  • Liveness 失败 → 容器被重启

2.4 Startup Probe:启动慢怎么办?

如果你的应用启动需要 90 秒,而 Liveness 的 initialDelaySeconds 设为 30 秒,那 Liveness 会在应用还没启动完的时候就判定失败,把容器杀了——这就是"启动雪崩"。

Startup Probe 专门解决这个问题:

code
startupProbe:
  httpGet:
    path: /actuator/health
    port: 8080
  failureThreshold: 30
  periodSeconds: 3
  • 在 Startup Probe 成功之前,Liveness 和 Readiness 不会执行
  • failureThreshold * periodSeconds = 30 * 3 = 90 秒,给足启动时间
  • Startup Probe 一旦成功,Liveness 接管

最佳实践: 对于启动时间超过 30 秒的 Java 应用,务必配置 Startup Probe,将 Liveness 的 initialDelaySeconds 设为 0,完全交给 Startup 控制预热期。

2.5 Spring Boot Actuator 的探针端点

Spring Boot 2.3+ 内置了探针端点:

yaml
management:
  endpoints:
    web:
      exposure:
        include: health
  endpoint:
    health:
      probes:
        enabled: true
      group:
        liveness:
          include: livenessState
        readiness:
          include: readinessState, db, redis
  • /actuator/health/liveness:只检查 livenessState(进程本身是否存活)
  • /actuator/health/readiness:检查 readinessState + 外部依赖(DB、Redis、MQ 等)

Readiness 的 HealthIndicator 设计原则: 把外部依赖(数据库、Redis、消息队列)放进 Readiness 检查组,但不要放进 Liveness——DB 挂了不应该导致 Pod 重启,而是应该从 Service 摘除,等 DB 恢复后自动恢复。

文章配图
文章配图


三、滚动更新(Rolling Update)机制

3.1 Deployment 的更新策略

Deployment 有两种更新策略:

  • Recreate:先删光所有旧 Pod,再创建新 Pod → 有停机时间
  • RollingUpdate:逐步替换,新旧共存 → 零停机(默认)

滚动更新的核心是两个参数:

yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 25%
      maxSurge: 25%

3.2 maxUnavailable 与 maxSurge

这两个参数控制滚动更新的"节奏":

参数 含义 示例(replicas=10)
maxUnavailable 最多有多少 Pod 可以处于不可用状态 25% → 最多 2.5 个,向下取整 = 2
maxSurge 最多可以超出期望副本数多少个 Pod 25% → 最多 2.5 个,向上取整 = 3

滚动更新过程(replicas=10, maxUnavailable=25%, maxSurge=25%)

文章配图
文章配图
:

(见 fig1:滚动更新时序图)

关键公式:

  • 更新期间最大 Pod 数 = replicas + maxSurge = 10 + 3 = 13
  • 更新期间最小可用 Pod 数 = replicas - maxUnavailable = 10 - 2 = 8

3.3 滚动更新的完整流程

  1. Deployment Controller 检测到 PodTemplate 变更
  2. 创建新的 ReplicaSet(RS-v2),RS-v1 保持不变
  3. 逐步扩容 RS-v2(每次最多 maxSurge 个),同时缩容 RS-v1(每次最多 maxUnavailable 个)
  4. 新 Pod 必须通过 Readiness Probe 才会被计入"可用"
  5. 旧 Pod 收到 SIGTERM 后优雅终止
  6. 全部替换完成后,RS-v1 的 replicas 变为 0(保留历史 RS 用于回滚)

3.4 minReadySeconds

minReadySeconds 是新人 Pod 的"冷静期"——Pod 通过 Readiness 后,还要等待 minReadySeconds 秒才被视为"可用"。这个参数防止 Pod 刚通过 Readiness 就立刻被认为是健康的,实际上可能还有瞬态问题。

yaml
spec:
  minReadySeconds: 10

在这个期间,如果 Pod 变得不健康,它不会被计入可用 Pod,滚动更新会暂停。

3.5 progressDeadlineSeconds

文章配图
文章配图

如果滚动更新在 progressDeadlineSeconds 内没有取得进展(比如新 Pod 一直无法通过 Readiness),Deployment 会被标记为 Progressing=False,状态为 ProgressDeadlineExceeded。这比无限期卡住好得多。


四、Pod 中断预算(PDB)

4.1 PDB 解决什么问题?

PDB 是"自愿中断"的防护网。自愿中断包括:

  • 节点 Drain(kubectl drain,用于节点维护)
  • Deployment 缩容
  • 集群自动缩容器(Cluster Autoscaler)移除节点
  • HPA 缩容

PDB 保证在这些操作发生时,至少有一定数量的 Pod 保持可用。

4.2 PDB 配置

yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myapp-pdb
spec:
  minAvailable: 2
  # 或使用 maxUnavailable: 1
  selector:
    matchLabels:
      app: myapp
  • minAvailable: 2:任何自愿中断操作,必须保证至少 2 个 Pod 可用
  • maxUnavailable: 1:任何自愿中断操作,最多允许 1 个 Pod 不可用

4.3 PDB 与滚动更新的关系

PDB 不影响滚动更新——滚动更新是 Deployment Controller 自己控制的,PDB 只管"自愿中断"(Eviction API)。但滚动更新期间如果同时发生节点 Drain,PDB 会阻止 Drain 操作过多 Pod。

4.4 PDB 的实际效果

文章配图
文章配图

(见 fig2:PDB 阻止过量驱逐示意图)

code
场景:3 副本,minAvailable=2
- 节点 Drain → 一次只能驱逐 1 个 Pod(保留 2 个)
- 如果已经驱逐了 1 个,新 Pod 还没 Ready → 第 2 个驱逐被拒绝
- 驱逐被拒绝后,kubectl drain 会重试,直到新 Pod Ready 或超时

五、优雅终止(Graceful Shutdown)

5.1 整个链路的优雅终止

文章配图
文章配图

当一个 Pod 被删除时(无论是手动删除、滚动更新、还是节点 Drain),完整的终止流程是:

(见 fig3:Pod 终止完整时序图)

code
1. API Server 收到删除请求
2. Pod 状态变为 Terminating
3. 同时发生两件事:
   a. Endpoints Controller 将 Pod 从 Service Endpoints 中移除
   b. kubelet 向容器发送 SIGTERM
4. 应用收到 SIGTERM 后开始优雅关闭
5. 等待 terminationGracePeriodSeconds(默认 30 秒)
6. 超时后 kubelet 发送 SIGKILL 强制杀死

5.2 terminationGracePeriodSeconds

yaml
spec:
  terminationGracePeriodSeconds: 60

这个参数决定了 Pod 从收到 SIGTERM 到被 SIGKILL 之间的时间窗口。对于 Java 应用,这个值应该足够大,以覆盖:

  • 正在处理的请求完成
  • 连接池关闭
  • 注册中心(Nacos/Eureka)下线
  • 日志刷新

典型值:

  • Web 应用:30-60 秒
  • 批处理任务:120-300 秒
  • 长连接服务:60-120 秒

5.3 preStop Hook

preStop 钩子在 SIGTERM 发送之前执行,可以用于自定义清理逻辑:

yaml
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]

preStop 的典型用途:

  • 额外的延迟,确保 Endpoints 变更已经传播到所有 kube-proxy
  • 调用应用的健康检查"下线"端点
  • 等待正在处理的请求完成

一个关键细节:preStop 的执行时间包含在 terminationGracePeriodSeconds 里。如果 preStop 花了 10 秒,那应用实际处理 SIGTERM 的时间就少了 10 秒。

5.4 Spring Boot 的优雅关闭

Spring Boot 2.3+ 内置了优雅关闭:

yaml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

当收到 SIGTERM 时:

  1. Spring Boot 停止接受新请求
  2. 等待正在处理的请求完成(最多 timeout-per-shutdown-phase)
  3. 关闭 @PreDestroy 方法
  4. 关闭 Bean 容器
  5. 进程退出

5.5 Endpoints 传播延迟——为什么需要 preStop sleep?

当 Pod 被标记为 Terminating 时,Endpoints Controller 会同时将它从 Endpoints 中移除。但这个变更需要传播到每个节点上的 kube-proxy,而 kube-proxy 更新 iptables/IPVS 规则需要时间。

这个延迟通常在 1-5 秒。 如果应用在 Endpoints 变更还没传播完就退出了,kube-proxy 仍然会把请求路由到这个已经退出的 Pod,导致连接被拒绝。

经典的解决方案:

yaml
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5"]

让 Pod 在 SIGTERM 之前先等 5 秒,确保 iptables 规则已经更新。

5.6 连接排空与优雅关闭的顺序

文章配图
文章配图

完整的优雅关闭链路:

  1. preStop sleep 5s → 等待 Endpoints 变更传播
  2. SIGTERM → Spring Boot 收到信号
  3. 停止接受新请求 → Tomcat/Netty 关闭 acceptor 线程
  4. 等待现有请求完成 → 最多 timeout-per-shutdown-phase
  5. 关闭外部连接 → 数据库连接池、Redis 连接、MQ 连接
  6. 注销服务注册 → Nacos/Eureka 下线
  7. 进程退出 → 或超时后被 SIGKILL

六、各组件协同:一个完整的发布流程

(见 fig4:完整发布流程各组件协同图)

当你执行 kubectl apply -f deployment.yaml 更新镜像版本时,以下组件协同工作:

组件 职责
Deployment Controller 创建新 RS,控制滚动节奏
ReplicaSet Controller 确保 Pod 数符合期望
Scheduler 为新 Pod 选择节点
kubelet 启动容器、执行探针、管理生命周期
Endpoints Controller 管理 Service Endpoints
kube-proxy 更新 iptables/IPVS 规则
PDB 限制自愿中断

文章配图
文章配图

每个组件独立工作,但通过 Kubernetes 的声明式模型最终协同。理解它们之间的时序关系,才能在生产环境中排查"为什么我的滚动更新卡住了"或"为什么更新期间有请求失败"。


七、生产环境踩坑与最佳实践

7.1 探针配置反模式

文章配图
文章配图

反模式 1:Liveness 检查了外部依赖

code
# 错误:DB 挂了不应该重启 Pod
livenessProbe:
  httpGet:
    path: /actuator/health  # 包含了 DB 检查

修正: Liveness 只检查 livenessState,Readiness 检查外部依赖。

反模式 2:Liveness 的 initialDelaySeconds 太小

code
livenessProbe:
  initialDelaySeconds: 10  # 应用启动需要 60 秒

修正: 使用 Startup Probe 覆盖启动期。

反模式 3:Readiness 的 failureThreshold 太小

code
readinessProbe:
  failureThreshold: 1  # 一次失败就摘除

修正: 设为 3-5,避免瞬态故障导致 Pod 被频繁摘除/加入。

7.2 滚动更新配置指南

配置推荐:

yaml
spec:
  replicas: 3
  minReadySeconds: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0    # 绝对不能少 Pod
      maxSurge: 1          # 先扩容再缩容
  • maxUnavailable: 0 + maxSurge: 1:先创建新 Pod,等新 Pod Ready 后再删旧 Pod
  • 适用于对可用性要求极高的场景
  • 代价:需要更多资源(临时多出 1 个 Pod)

7.3 PDB 配置指南

  • minAvailable 通常设为 replicas - 1 或 ceil(replicas / 2)
  • 不要设成 replicas(等于禁止任何中断)
  • 单副本的 PDB 没有意义
  • PDB 标签选择器必须精确匹配,避免误匹配

7.4 优雅关闭配置清单

yaml
# Deployment
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: app
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 5"]
yaml
# Spring Boot application.yml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

八、总结

机制 核心问题 关键参数
Liveness Probe 容器是否还活着? failureThreshold, periodSeconds
Readiness Probe 容器能否接收流量? failureThreshold, periodSeconds
Startup Probe 启动是否完成? failureThreshold × periodSeconds
Rolling Update 如何逐步替换 Pod? maxUnavailable, maxSurge, minReadySeconds
PDB 维护时如何保证可用性? minAvailable / maxUnavailable
Graceful Shutdown 如何优雅退出? terminationGracePeriodSeconds, preStop

文章配图
文章配图

这些机制不是孤立的——它们共同构成了 Kubernetes 应用的"生命周期安全网"。一个生产级的 Java 应用,必须同时正确配置这六个方面,才能实现真正的零停机发布。


九、面试官视角提问

以下问题从面试官角度出发,考察候选人对 K8s 健康检查与发布机制的理解深度和实战经验。

Q1: 一个 Pod 的 Readiness Probe 失败了,Liveness Probe 也失败了,K8s 会怎么处理?这两个失败是先后发生还是同时发生?如果 Readiness 先失败,Liveness 后失败,Pod 最终会怎样?

Q2: 你的 Deployment 有 10 个副本,滚动更新时发现新 Pod 一直处于 ContainerCreating 状态,但旧 Pod 还在被逐步删除。这个过程中最多会损失多少个 Pod 的可用性?maxUnavailable=25% 和 maxSurge=25% 的情况下,实际可用 Pod 最少是多少?

Q3: 在不使用 Service Mesh 的情况下,如何彻底消除滚动更新期间的请求失败?请从 Pod 生命周期、Endpoints 传播、应用层连接排空三个层面给出完整方案。

Q4: PDB 的 minAvailable 设为 2,但 Deployment 只有 2 个副本。现在要执行 kubectl drain 一个节点,上面恰好有 1 个 Pod。这个 drain 操作会成功吗?如果会失败,有什么办法绕过?

Q5: 你的 Spring Boot 应用配置了 server.shutdown=graceful,timeout-per-shutdown-phase=30s,terminationGracePeriodSeconds=30。但运维反馈说 Pod 重启时经常有 502 错误。可能的原因是什么?如何排查?

Q6: 一个多副本的 StatefulSet 应用,既有 Liveness Probe 又有 Readiness Probe,还有 Startup Probe。现在执行滚动更新,新 Pod 启动时间约 120 秒。请设计完整的探针参数,并说明为什么这样设置。


参考答案

A1:Readiness 与 Liveness 同时失败的场景分析

Readiness Probe 和 Liveness Probe 是两套独立运行的探测机制,kubelet 对它们各自维护独立的状态机。它们可能先后失败,也可能在同一探测周期内都失败——取决于应用究竟出了什么问题。

场景分析:

  1. Readiness 先失败,Liveness 后失败(最常见):比如数据库连接池耗尽。Readiness 检查 /actuator/health/readiness 时发现 DB 连不上,先标记为 NotReady,Pod 被从 Service Endpoints 摘除。此时 Pod 还在运行,但 Liveness 只检查 /actuator/health/liveness(只看 livenessState),livenessState 可能仍然是 UP,所以 Liveness 暂时不会失败。如果问题持续恶化——比如 DB 连接池耗尽导致线程全部阻塞,JVM 进入死锁状态——后续 Liveness 检查也会失败。此时 Pod 被 kubelet 重启。
  2. 同时失败:如果应用进程整体 hang 住(比如 Full GC 的 STW 时间过长),Readiness 和 Liveness 的 HTTP 请求都会超时,两者在同一周期内同时失败。Readiness 失败 → 摘除流量;Liveness 失败 → 重启容器。两者独立执行,不存在"谁先谁后"的互斥关系。
  3. 最终结果:Pod 会被重启(Liveness 失败触发),重启后如果问题根因没解决(比如 DB 仍然不可用),新 Pod 启动后 Readiness 可能再次失败,但 Liveness 正常(因为进程本身活得好好的)。这就是典型的"Pod Running 但不 Ready"状态。

关键点:面试官想听到的是——你能否区分"进程级故障"(Liveness 管)和"依赖级故障"(Readiness 管),以及两者独立运作的机制。


A2:滚动更新中 Pod 可用性损失计算

已知条件:replicas=10,maxUnavailable=25%,maxSurge=25%。

  • maxUnavailable = floor(10 × 25%) = floor(2.5) = 2
  • maxSurge = ceil(10 × 25%) = ceil(2.5) = 3

滚动更新期间,新 Pod 一直 ContainerCreating(比如镜像拉取失败或节点资源不足),这意味着新 Pod 永远不会通过 Readiness,不会被计入"可用"。

过程推演:

  • 初始状态:10 个旧 Pod 全部可用
  • 第一轮:创建 3 个新 Pod(maxSurge=3),但全部卡在 ContainerCreating。同时可以删除 2 个旧 Pod(maxUnavailable=2)。此时可用 Pod = 10 - 2 = 8。
  • 第二轮:新 Pod 仍然没 Ready,但旧 Pod 已被删了 2 个。此时控制器发现"可用 Pod 数 = 8,小于期望的 10",同时新 Pod 也没 Ready,滚动更新会暂停——因为最多只能有 2 个不可用,再删就超过 maxUnavailable 了。

实际可用 Pod 最少是 8 个。控制器不会继续删除旧 Pod,因为那会违反 maxUnavailable 约束。

面试官追问:如果 maxUnavailable=0 呢?答案:一个旧 Pod 都不会被删,滚动更新会卡住不动,直到新 Pod 变成 Ready 或超时(progressDeadlineSeconds)。


A3:不使用 Service Mesh 实现零停机滚动更新

从三个层面给出完整方案:

第一层:Pod 生命周期

yaml
spec:
  terminationGracePeriodSeconds: 60
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      initialDelaySeconds: 10
      periodSeconds: 5
      failureThreshold: 3
  • preStop sleep 5:给 Endpoints 传播留出时间窗口
  • terminationGracePeriodSeconds: 60:确保应用有足够时间处理完现有请求
  • Readiness Probe 确保只有真正可用的 Pod 才接收流量

第二层:Endpoints 传播优化

  • 使用 kube-proxy 的 IPVS 模式(而非 iptables),IPVS 使用哈希表做规则匹配,规则更新是 O(1) 的,而 iptables 是 O(n) 的线性匹配
  • 配置 minReadySeconds: 10,让 Pod 通过 Readiness 后再等 10 秒才被视为可用,这间接给了 Endpoints 传播时间
  • 考虑使用 EndpointSlice(K8s 1.21+ 默认启用),它比传统 Endpoints 对象更高效,更新传播更快

第三层:应用层连接排空

yaml
# Spring Boot
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

应用层要做到:

  1. 收到 SIGTERM 后立即停止接受新连接(Tomcat/Netty 关闭 acceptor)
  2. 等待现有请求处理完成(timeout-per-shutdown-phase)
  3. 关闭数据库连接池、Redis 连接池
  4. 从注册中心(Nacos/Eureka)主动注销,而不是等心跳超时

完整的零停机保障链: preStop sleep 5s → Endpoints 摘除 → SIGTERM → 停止接受新请求 → 等待现有请求完成 → 关闭连接池 → 注销注册中心 → 进程退出

面试官想听到的关键词:preStop 延迟、IPVS vs iptables、EndpointSlice、Spring Boot graceful shutdown、注册中心主动注销、minReadySeconds。


A4:PDB drain 阻塞问题分析

结论:drain 操作会失败。

原因:PDB 的 minAvailable=2 意味着在任何自愿中断操作下,必须保证至少 2 个 Pod 可用。Deployment 只有 2 个副本,如果要驱逐其中 1 个 Pod,可用 Pod 变为 1,违反了 minAvailable=2 的约束。Eviction API 会拒绝这次驱逐,kubectl drain 收到拒绝后报错。

绕过方法(按推荐度排序):

  1. 临时调整 PDB(推荐):
bash
kubectl patch pdb myapp-pdb -p '{"spec":{"minAvailable":1}}'
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
kubectl patch pdb myapp-pdb -p '{"spec":{"minAvailable":2}}'

先降低 minAvailable,drain 完成后恢复。 2. 使用 --disable-eviction 强制 drain:

bash
kubectl drain <node> --disable-eviction --ignore-daemonsets --delete-emptydir-data

--disable-eviction 会绕过 Eviction API,直接 delete pod(不走 PDB 检查)。但这是危险的——它会忽略 PDB 保护,可能导致服务不可用。仅在维护窗口或确认可以接受短暂不可用时使用。 3. 先扩容再 drain:

bash
kubectl scale deployment myapp --replicas=3
# 等待新 Pod Ready
kubectl drain <node> --ignore-daemonsets

扩容到 3 副本,驱逐 1 个后还剩 2 个,满足 minAvailable=2。

面试官追问:如果 PDB 用的是 maxUnavailable: 1 呢?答案:minAvailable=2 意味着 2 副本下 maxUnavailable=0,两者等价。但用 maxUnavailable=1 的话,2 副本下允许 1 个不可用,drain 会成功——但这也意味着 PDB 实际没起到保护作用。所以 PDB 参数的选择需要和副本数匹配。


A5:502 错误排查

最可能的原因:Endpoints 传播延迟。

时间线分析:

  • Pod 收到 SIGTERM,terminationGracePeriodSeconds=30 开始倒计时
  • Spring Boot timeout-per-shutdown-phase=30s,等待现有请求完成
  • 但 Endpoints Controller 摘除 Pod 到 kube-proxy 更新 iptables 规则有 1-5 秒延迟
  • 在这个延迟窗口内,kube-proxy 仍然把新请求路由到这个正在关闭的 Pod
  • 如果 Pod 已经关闭了 Tomcat acceptor(停止接受新连接),新请求会收到连接被拒绝 → 502

具体排查步骤:

  1. 确认是不是 preStop 缺失:检查 Deployment 是否有 preStop hook。如果没有,SIGTERM 和 Endpoints 摘除几乎同时发生,没有给 iptables 规则更新留时间。
  2. 检查 terminationGracePeriodSeconds 是否够用:timeout-per-shutdown-phase=30 + terminationGracePeriodSeconds=30,preStop 的时间也包含在内。如果请求处理超过 30 秒,Pod 会被 SIGKILL 强制杀死。
  3. 查看应用日志:确认是否有 Graceful shutdown 相关日志,以及是否有请求在关闭期间被拒绝。
  4. 检查 kube-proxy 模式:iptables 模式规则更新慢,IPVS 模式更快。kubectl logs -n kube-system kube-proxy-xxx 查看。

修复方案:

yaml
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5"]  # 关键:给 Endpoints 传播留时间

spec:
  terminationGracePeriodSeconds: 45  # 5s preStop + 30s graceful + 10s buffer

面试官想听到的深度:能区分 502 的多种可能原因——Endpoints 延迟、preStop 缺失、terminationGracePeriodSeconds 不足、连接池未排空、注册中心未注销,并能给出系统性排查方案而不仅仅是"加个 preStop sleep"。


A6:StatefulSet 的完整探针参数设计

需求:多副本 StatefulSet,启动时间约 120 秒,需要滚动更新。

yaml
spec:
  containers:
  - name: app
    startupProbe:
      httpGet:
        path: /actuator/health
        port: 8080
      failureThreshold: 30     # 30 × 5 = 150 秒
      periodSeconds: 5
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      initialDelaySeconds: 0   # 完全交给 startupProbe
      periodSeconds: 10
      failureThreshold: 3      # 30 秒内连续 3 次失败才重启
      timeoutSeconds: 5
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      initialDelaySeconds: 0   # 交给 startupProbe
      periodSeconds: 5
      failureThreshold: 3      # 15 秒内连续失败才摘除
      timeoutSeconds: 5

设计理由:

  1. Startup Probe:failureThreshold × periodSeconds = 30 × 5 = 150 秒,超过 120 秒的启动时间,留 30 秒 buffer。在 Startup Probe 成功之前,Liveness 和 Readiness 都不会执行,避免了"启动雪崩"。
  2. Liveness Probe:initialDelaySeconds=0——因为 Startup Probe 已经覆盖了启动期,Liveness 不需要额外延迟。periodSeconds=10,不要过于频繁,避免给应用增加负担。failureThreshold=3——连续 30 秒不健康才重启,给应用自愈的机会。
  3. Readiness Probe:periodSeconds=5,比 Liveness 更频繁——流量切换需要更快的响应。failureThreshold=3——连续 15 秒不健康才摘除,避免瞬态抖动导致 Pod 被频繁摘除加入。
  4. StatefulSet 特殊考量:
    • StatefulSet 的 Pod 名称是固定的(如 app-0、app-1),滚动更新是逆序进行的(从 app-N 到 app-0)
    • StatefulSet 的 podManagementPolicy 默认是 OrderedReady,意味着 app-2 必须 Ready 后才会更新 app-1
    • 如果使用了持久卷(PVC),确保新 Pod 能正确挂载之前的 PVC

为什么不这样设计(常见错误):

  • 不用 Startup Probe,把 Liveness 的 initialDelaySeconds 设为 130:如果以后启动时间变长,又要手动调整,维护成本高,且容易遗漏
  • Readiness 的 failureThreshold 设为 1:瞬态故障(如短暂的 GC 停顿)会导致 Pod 被频繁摘除和加入,引起流量抖动
  • Liveness 检查了外部依赖:DB 宕机会导致所有 Pod 被连环重启,造成更大的故障

面试官追问:如果启动时间从 120 秒变成 300 秒怎么办?答:只需调整 Startup Probe 的 failureThreshold(比如改为 60,60 × 5 = 300),Liveness 和 Readiness 不用动。