就绪探针调优保障 Kubernetes 集群升级时服务流量零中断:K8s 应用健康检查优化策略
2026/6/6 0:34:51 网站建设 项目流程

就绪探针调优保障 Kubernetes 集群升级时服务流量零中断:K8s 应用健康检查优化策略

前言

"涵姐,凌晨 Rolling Update 的时候线上报警了!用户反馈有几十秒的 502,监控上的错误率飙到 8% 了。"

周二早上我刚到工位,Ping 正蹲在我的键盘上舔爪子,小周就顶着一对黑眼圈跑过来了。我瞥了一眼他的 Deployment YAML——strategy.rollingUpdate.maxSurge=25%, maxUnavailable=25%,Readiness Probe 的配置是默认值,PreStop Hook 也没配。

"你这是裸更新的啊?旧 Pod 还在处理请求的时候就被 Kill 了,新 Pod 还没 Ready 就开始接流量,不出问题才怪。"

小周挠了挠头:"Readiness Probe 我配了啊,只是用的默认参数……"

"默认参数就 3 秒 initialDelaySeconds 加 1 秒 periodSeconds,你服务启动就要 15 秒,探针早挂了。还有,Pod 被删的时候 SIGTERM 发完一秒就被强制 Kill 了,连接全断了。"

我把 Ping 从键盘上抱走,打开终端:"今天给你系统讲一下 Readiness Probe 的调优,配合 PreStop Hook 和 PDB,彻底解决集群升级时的流量中断问题。"

一、底层原理:健康检查机制与集群升级流量中断根因分析

1.1 两种探针的职责边界

Kubernetes 提供了三种探针(Probe),但最容易混淆的就是 ReadinessProbe 和 LivenessProbe:

维度ReadinessProbe(就绪探针)LivenessProbe(存活探针)StartupProbe(启动探针)
核心职责判断 Pod是否准备好接收流量判断 Pod是否还活着,需不需要重启判断 Pod是否已完成启动(v1.16+)
失败后果从 Service Endpoint 中移除,不重启Kubelet 重启容器,强制拉起重部署屏蔽 Liveness 检测,给慢启动应用缓冲期
检测时机整个 Pod 生命周期持续检测整个 Pod 生命周期持续检测只在启动阶段检测,成功后交给 Liveness
典型场景应用启动慢、热加载中、依赖未就绪死锁、OOM、goroutine 泄漏、进程 Hang大型 Java 应用、AI 模型加载、Warmup 期
对流量影响直接影响流量路径,移除后 Service 不再转发间接影响,重启后 Readiness 会重新检测启动期间接管Liveness 判定

关键认知:滚动更新时流量中断的核心原因不是 Liveness 失败,而是Readiness 状态切换与 Pod 终止流程的时序错配

1.2 集群升级时流量中断的完整链路分析

集群升级分为控制面升级节点升级两类场景。节点升级会触发节点上所有 Pod 的重新调度,下面以最常见的节点升级为例:

flowchart TD subgraph "阶段一:驱逐开始" A["运维触发节点升级\nkubectl drain node"] --> B["Node Controller\n标记 Node 为\nNode.Spec.Unschedulable=true"] B --> C["Eviction API\n创建 Pod 驱逐请求"] end subgraph "阶段二:Pod 终止流程(默认配置)" C --> D["kubelet 收到\nPod 删除请求"] D --> E{"Readiness Probe\n是否已失败?"} E -->|"未配置 PreStop\n直接进入"| F["SIGTERM 发送\nterminationGracePeriodSeconds\n默认 30s"] E -->|"配置了 PreStop\n执行 Hook"| F F --> G["一旦 PreStop 完成\n或超时(默认 30s)"] G --> H["SIGKILL 强制终止\n容器进程"] end subgraph "阶段三:流量中断发生(问题所在)" H --> I["旧 Pod 进程死亡\n正在处理的请求\n被硬中断"] I --> J["客户端收到\nConnection Reset / 502"] J --> K["新 Pod 启动\nReadiness Probe\n还未通过"] K --> L["Service Endpoint\n无可用后端\n流量黑洞"] end style E fill:#fff3e0,stroke:#f57c00 style J fill:#fce4ec,stroke:#d32f2f style L fill:#fce4ec,stroke:#d32f2f

中断时间线(默认配置,未优化)

t=0s Pod 收到删除请求,开始 Termination t=0.1s Endpoint Controller 从 Service 移除该 Pod IP └── 但!Endpoint 更新需要 time to propagate kube-proxy 同步 iptables/IPVS 规则需要时间 这期间仍在转发流量到该 Pod t=1s Readiness Probe(period=1s)可能还未检查 t=1~5s PreStop Hook 开始执行 / SIGTERM 发送 t=5~30s 应用处理 SIGTERM(如果实现了优雅关闭) t=30s terminationGracePeriodSeconds 超时 → SIGKILL └── 此时还在处理的请求全部中断 t=30~45s 新 Pod 启动,Readiness Probe 等待 initialDelaySeconds t=45~50s 新 Pod Readiness 通过,Endpoint 重新加入 └── 注意:45~50s 这段时间 Service 无可用后端

二、快速上手:Readiness Probe 的三种检测方式与参数配置

2.1 三种检测方式

Kubernetes 支持三种探针检测方式,适用场景各不相同:

HTTP Get 检测(最常用)
apiVersion: v1 kind: Pod metadata: name: web-app labels: app: web spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 readinessProbe: httpGet: path: /healthz port: 80 httpHeaders: - name: X-Health-Check value: "true" initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3
TCP Socket 检测(适合非 HTTP 服务)
readinessProbe: tcpSocket: port: 3306 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 2
Exec 命令检测(最灵活,适合复杂逻辑)
readinessProbe: exec: command: - sh - -c - | # 检查应用内部状态 curl -sf http://localhost:8080/ready && test -f /tmp/app_initialized && pgrep -f "main-process" initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3

2.2 探针参数详解与调优建议

参数默认值含义调优建议(服务端场景)
initialDelaySeconds0容器启动后等待多久开始首次检测设为服务平均启动时间 + 5s,避免启动阶段误判
periodSeconds10检测间隔生产建议5~15s,间隔太短增加 Kubelet 负载
timeoutSeconds1单次检测超时时间3~5s,避免网络抖动误报
successThreshold1连续成功几次视为就绪通常1即可,高可用要求可设为2
failureThreshold3连续失败几次视为不就绪2~5,配合 periodSeconds 控制总容忍时间

总失败判定时间计算

总容忍时间 = periodSeconds × (failureThreshold - 1) + timeoutSeconds

例如periodSeconds=10, failureThreshold=3, timeoutSeconds=3

  • 第1次失败:开始计数
  • 第2次失败:计数 +1(10s后)
  • 第3次失败:判定为不就绪(再过10s)
  • 总时间 ≈ 20~23s 后 Pod 从 Service 移除

2.3 PreStop Hook + 优雅关闭

Readiness Probe 只是管了"新请求别过来",但正在处理的请求必须靠 PreStop Hook 和应用的优雅关闭机制来保证不中断:

apiVersion: apps/v1 kind: Deployment metadata: name: api-server spec: replicas: 3 selector: matchLabels: app: api-server template: metadata: labels: app: api-server spec: terminationGracePeriodSeconds: 60 containers: - name: api-server image: api-server:latest ports: - containerPort: 8080 readinessProbe: httpGet: path: /healthz/ready port: 8080 initialDelaySeconds: 15 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 livenessProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 30 periodSeconds: 15 timeoutSeconds: 5 failureThreshold: 3 lifecycle: preStop: exec: command: - sh - -c - | # 1. 标记自身不再接受新请求(应用内) curl -sf -X POST http://localhost:8080/healthz/drain && \ # 2. 等待 Readiness Probe 检测到失败 # failureThreshold × periodSeconds = 3×5=15s sleep 15 && \ # 3. 等待现有请求处理完毕 sleep 10 # 总耗时 ≈ 25s # terminationGracePeriodSeconds 设为 60s 足够

三、核心 API 与深水区:探针调优的底层机制

3.1 Kubelet 探针执行引擎源码分析

Kubelet 中的probeManager是探针的调度和执行引擎。每个 Probe 由worker独立运行,循环执行检测逻辑:

// 源码路径: pkg/kubelet/prober/prober_manager.go // 核心逻辑简化示意 package prober import ( "time" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/klog/v2" ) type manager struct { workers map[probeKey]*worker // ... 其他字段 } type worker struct { pod *v1.Pod container v1.Container probeType probeType // readiness / liveness / startup prober *prober results resultsManager // 上次探测结果 lastResult Result // 连续成功/失败计数 consecutiveFailures int consecutiveSuccesses int } func (w *worker) run() { // 等待 initialDelaySeconds time.Sleep(w.getInitialDelay()) // 进入周期性探测循环 wait.Until(func() { w.probeAndRecord() }, w.probePeriod(), w.stopCh) } func (w *worker) probeAndRecord() { result, err := w.prober.probe(w.probeType, w.pod, w.container) if err != nil { klog.Errorf("Probe error: %v", err) result = Unknown } // 更新连续成功/失败计数 switch result { case Success: w.consecutiveSuccesses++ w.consecutiveFailures = 0 case Failure: w.consecutiveFailures++ w.consecutiveSuccesses = 0 case Unknown: w.consecutiveFailures++ w.consecutiveSuccesses = 0 } // 判断是否需要切换状态 // changeDetected 会检查 successThreshold / failureThreshold if w.changeDetected(result) { w.updatePodStatus(result) } }

3.2 Endpoint 同步延迟:流量中断的隐藏因素

很多人以为 Readiness Probe 失败后流量立刻就不来了,实际上 Endpoint 传播存在多级延迟

flowchart LR A["Pod Readiness\n状态变化"] -->|"延迟1: Kubelet\n探测周期 + 上报"| B["Endpoint Controller\nWatch 到变化"] B -->|"延迟2: EndpointSlice\n更新 + 写入 etcd"| C["kube-proxy\nWatch EndpointSlice"] C -->|"延迟3: kube-proxy\n同步 iptables/IPVS\n规则(默认 30s 全量同步)"| D["节点 iptables/IPVS\n规则生效"] style A fill:#e1f5fe,stroke:#0288d1 style D fill:#fce4ec,stroke:#d32f2f

各环节延迟估算:

延迟环节默认值优化方向
Kubelet 探测周期periodSeconds(默认 10s)缩短 periodSeconds 到 5s
Kubelet 状态上报默认 10s 上报周期调整nodeStatusUpdateFrequency(已弃用,v1.20+ 用nodeStatusUpdateFrequency
Endpoint Controller Watch实时(毫秒级)无需优化
kube-proxy 同步周期IPVS 模式增量同步(毫秒级),iptables 模式全量(30s)使用 IPVS 模式(关键)
节点 conntrack 更新微秒级确保 conntrack 参数合理

核心结论:切换到 IPVS 模式 + 调小periodSeconds是减少 Endpoint 传播延迟最有效的手段。

3.3 PodDisruptionBudget 配置

PDB 保证在节点升级时,指定数量的 Pod 始终可用:

apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: api-server-pdb spec: minAvailable: 2 # 最少保持 2 个 Pod 可用 # 或者使用 maxUnavailable # maxUnavailable: 1 # 最多允许 1 个不可用 selector: matchLabels: app: api-server

PDB 的工作逻辑:

  • kubectl drain执行时,Eviction API 会检查 PDB
  • 如果驱逐 Pod 会导致可用 Pod 数低于minAvailable,驱逐请求被拒绝
  • 控制器的kubectl drain会重试等待,直到有新的 Pod Ready 才继续驱逐

PDB 配合 Rolling Update Strategy 的完整时序:

flowchart TD Start["触发 Rolling Update\nkubectl set image deploy/api-server"] --> PDBCheck{"PDB 检查\nminAvailable=2\n当前可用 Pod >= 3?"} PDBCheck -->|"是"| NewPod["创建新 Pod v2\n等待 Readiness Probe 通过"] NewPod --> ProbeCheck{"新 Pod\nReadiness Probe\n通过了吗?"} ProbeCheck -->|"否"| Wait["等待探针周期检测\ninitialDelaySeconds +\nperiodSeconds × failureThreshold"] Wait --> ProbeCheck ProbeCheck -->|"是"| EndpointAdd["新 Pod IP\n加入 Service Endpoint"] EndpointAdd --> ScaleDown["开始删除旧 Pod v1\nPDB 保护仍生效"] ScaleDown --> DrainCheck{"可用 Pod >= minAvailable\n(2个)?"} DrainCheck -->|"是"| OldPodDelete["删除旧 Pod\n执行 PreStop Hook\n优雅关闭"] DrainCheck -->|"否"| WaitScale["等待更多新 Pod Ready\n直到满足 PDB 条件"] OldPodDelete --> Continue["继续下一轮\n直到所有旧 Pod 替换完毕"] style NewPod fill:#e8f5e9,stroke:#388e3c style OldPodDelete fill:#fff3e0,stroke:#f57c00 style DrainCheck fill:#e1f5fe,stroke:#0288d1

四、实战演练:从零构建零中断升级方案

4.1 场景设定

  • 集群:3 节点 K8s v1.28,kube-proxy 使用 IPVS 模式
  • 服务:API Server,3 副本,启动时间约 12s
  • 升级方式:kubectl drain节点升级 + Rolling Update
  • 目标:升级过程中零 502,零连接中断

4.2 完整 Production 级配置

--- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: api-server-pdb namespace: production spec: minAvailable: 2 selector: matchLabels: app: api-server --- apiVersion: apps/v1 kind: Deployment metadata: name: api-server namespace: production spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # 每次额外启动 1 个新 Pod maxUnavailable: 0 # 确保 0 个不可用,配合 PDB 双保险 selector: matchLabels: app: api-server template: metadata: labels: app: api-server spec: terminationGracePeriodSeconds: 70 containers: - name: api-server image: api-server:2.0.0 ports: - containerPort: 8080 protocol: TCP env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP readinessProbe: httpGet: path: /healthz/ready port: 8080 initialDelaySeconds: 15 periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 30 periodSeconds: 15 timeoutSeconds: 5 failureThreshold: 3 lifecycle: preStop: exec: command: - sh - -c - | echo "[$(date)] Pod $POD_NAME 开始优雅关闭..." # 步骤1:标记自身为 Drain 状态 # 应用内 /healthz/ready 返回 503 curl -sf -X POST "http://127.0.0.1:8080/healthz/drain" || true echo "[$(date)] 已触发 Drain 标记" # 步骤2:等待 Readiness Probe 检测到不就绪 # failureThreshold=3, periodSeconds=5 → 最多 15s # 实际可能更快(timeoutSeconds=3 内返回非 200) echo "[$(date)] 等待 Readiness 失效传播..." sleep 18 # 步骤3:等待已分发请求处理完成 # 假设 max request timeout = 10s echo "[$(date)] 等待请求处理完成..." sleep 12 echo "[$(date)] 优雅关闭完成,允许 SIGTERM 继续" # 总耗时约 30~35s # terminationGracePeriodSeconds=70 充裕 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1000m memory: 1Gi affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - api-server topologyKey: kubernetes.io/hostname

4.3 服务端优雅关闭实现(Go 示例)

Readiness Probe 的/healthz/ready端点需要感知到 Drain 状态,配合 PreStop Hook:

package main import ( "context" "encoding/json" "log" "net/http" "os" "os/signal" "sync" "syscall" "time" ) type HealthServer struct { mu sync.RWMutex draining bool ready bool startTime time.Time } func NewHealthServer() *HealthServer { return &HealthServer{ startTime: time.Now(), } } // Readiness 端点:drain 时返回 503 func (h *HealthServer) ReadinessHandler(w http.ResponseWriter, r *http.Request) { h.mu.RLock() defer h.mu.RUnlock() if h.draining { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]string{ "status": "draining", "message": "Pod is being drained, stop sending traffic", }) return } if !h.ready { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]string{ "status": "not ready", "message": "Application not fully initialized", }) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "ready", }) } // Liveness 端点:只判断进程是否存活 func (h *HealthServer) LivenessHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "alive", }) } // Drain 端点:由 PreStop Hook 调用,标记开始排空 func (h *HealthServer) DrainHandler(w http.ResponseWriter, r *http.Request) { h.mu.Lock() h.draining = true h.mu.Unlock() log.Println("[Health] Drain signal received, marking as not ready") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "draining started", }) } // 初始化完成标记 func (h *HealthServer) MarkReady() { h.mu.Lock() defer h.mu.Unlock() h.ready = true } func main() { health := NewHealthServer() mux := http.NewServeMux() mux.HandleFunc("/healthz/ready", health.ReadinessHandler) mux.HandleFunc("/healthz/live", health.LivenessHandler) mux.HandleFunc("/healthz/drain", health.DrainHandler) server := &http.Server{ Addr: ":8080", Handler: mux, } // 模拟启动耗时 log.Println("Application starting...") time.Sleep(5 * time.Second) health.MarkReady() log.Println("Application ready") // 启动 HTTP 服务 go func() { log.Printf("Health server listening on %s", server.Addr) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server error: %v", err) } }() // 等待操作系统信号 quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } log.Println("Server exited gracefully") }

4.4 节点升级脚本

配合 PDB 和 Readiness Probe 的节点升级操作流程:

#!/bin/bash # 零中断节点升级脚本 set -euo pipefail NODE_NAME=$1 DRAIN_TIMEOUT="5m" RETRY_INTERVAL="30s" echo "=== 开始节点升级: $NODE_NAME ===" # 步骤1:先 cordon 节点,防止新 Pod 调度 echo "[1/5] Cordon node $NODE_NAME..." kubectl cordon "$NODE_NAME" # 步骤2:检查节点上运行的重要 Pod 状态 echo "[2/5] Checking critical Pods on node..." kubectl get pods --all-namespaces --field-selector spec.nodeName="$NODE_NAME" \ -o json | jq -r '.items[] | select(.metadata.labels["app"] != null) | "\(.metadata.namespace)/\(.metadata.name) - \(.status.phase)"' # 步骤3:执行 drain,PDB 会保护关键服务 echo "[3/5] Draining node $NODE_NAME (timeout: $DRAIN_TIMEOUT)..." kubectl drain "$NODE_NAME" \ --ignore-daemonsets \ --delete-emptydir-data \ --grace-period=120 \ --timeout="$DRAIN_TIMEOUT" \ --force # 步骤4:确认节点已排空 echo "[4/5] Verifying node is fully drained..." while true; do POD_COUNT=$(kubectl get pods --all-namespaces \ --field-selector spec.nodeName="$NODE_NAME" \ --no-headers 2>/dev/null | wc -l) if [ "$POD_COUNT" -eq 0 ]; then break fi echo " Waiting for $POD_COUNT pods to be evicted..." sleep "$RETRY_INTERVAL" done echo " Node $NODE_NAME is fully drained." # 步骤5:执行节点升级操作(替换为实际升级命令) echo "[5/5] Upgrading node $NODE_NAME..." # 例如:ssh $NODE_NAME "sudo apt upgrade -y && sudo reboot" echo "=== Node $NODE_NAME upgrade completed ==="

4.5 验证升级过程中的流量中断

使用持续压测验证升级过程中的零中断效果:

# 终端1:启动持续压测 kubectl run -it load-generator --image=busybox --rm --restart=Never -- sh -c ' echo "Starting load test..." i=0 while true; do STATUS=$(wget -q -O- -S http://api-server.production:8080/healthz/ready 2>&1 | head -1) echo "[$i] $STATUS" i=$((i+1)) sleep 0.5 done ' # 终端2:执行 Rolling Update kubectl set image deployment/api-server api-server=api-server:2.0.1 -n production kubectl rollout status deployment/api-server -n production -w # 终端3:观察 Endpoint 变化 watch -n 1 'kubectl get endpoints api-server -n production' # 终端4:观察 Pod 滚动过程 watch -n 1 'kubectl get pods -n production -l app=api-server -o wide'

五、避坑指南

问题 1:StartupProbe 缺失导致慢启动应用被不断重启

现象:服务启动需要 20s,但 Liveness Probe 的initialDelaySeconds=10, failureThreshold=2, periodSeconds=10,在第 20s 时(第 2 次探测超时)直接判定失败,容器被重启,陷入"启动→被 Kill→重启"的死循环。

解决方案:对于启动慢的应用(尤其是 AI 模型加载、Java 应用),加 StartupProbe:

startupProbe: httpGet: path: /healthz/startup port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 30 # 最大等待 5 + 30×5 = 155s

StartupProbe 期间 LivenessProbe 和 ReadinessProbe 不会执行。

问题 2:Readiness 与 Liveness 使用同一个端点

现象:两个探针都指向/healthz,且该端点在应用忙碌时返回 503。Liveness 误判导致容器被杀重启。

根本原因:Readiness 和 Liveness 是不同维度的检查:

  • Readiness:检查"应用是否准备好处理请求"——可以返回 503 表示忙碌
  • Liveness:检查"进程是否还活着"——只要进程在运行就应该返回 200

解决方案:严格区分两个端点:

// Readiness:检查依赖是否就绪(数据库、缓存等) mux.HandleFunc("/healthz/ready", func(w http.ResponseWriter, r *http.Request) { if !dbReady || !cacheReady { w.WriteHeader(http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) }) // Liveness:只检查进程是否健康 mux.HandleFunc("/healthz/live", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })

问题 3:PreStop Hook 超时导致 SIGKILL 提前发送

现象:PreStop Hook 中写了一个复杂的清理逻辑,预计执行 45s,但terminationGracePeriodSeconds还是默认值 30s。第 30s 时 SIGKILL 直接发送,清理逻辑中断。

根因terminationGracePeriodSeconds涵盖了 PreStop Hook + SIGTERM 后的清理时间。默认 30s 远远不够。

解决方案

spec: # terminationGracePeriodSeconds 必须 > PreStop Hook 总耗时 terminationGracePeriodSeconds: 90 containers: - name: app lifecycle: preStop: exec: command: - sh - -c - | # 总预期待 45s echo "Draining connections..." sleep 40 # 实际应为 drain 等待逻辑 echo "Cleanup done"

计算方式:terminationGracePeriodSeconds ≥ PreStop 预估耗时 × 1.5

问题 4:failureThreshold 过大导致流量被转发到已死亡的 Pod

现象:Pod 内部进程已经 OOM killed 了,但 Readiness Probe 要等failureThreshold=5, periodSeconds=15即 75s 后才能从不就绪。这期间 Service 仍然在转发流量到该 Pod。

解决方案:Readiness Probe 的failureThreshold不宜过大:

readinessProbe: httpGet: path: /healthz/ready port: 8080 periodSeconds: 5 failureThreshold: 2 # 10s 内标记不就绪 # 总容忍时间 = 5 × (2-1) = 5s,加上 timeoutSeconds 约 8s

同时结合 Liveness Probe 兜底,确保真正死亡的 Pod 能被更快重启:

livenessProbe: httpGet: path: /healthz/live port: 8080 periodSeconds: 10 failureThreshold: 3 # 30s 内重启

问题 5:maxUnavailable 策略设置不当导致 Pod 全部下线

现象strategy.rollingUpdate.maxUnavailable: 25%,3 副本时有 25% × 3 = 0.75 → 向下取整 = 0,这没问题。但如果副本数只有 2,25% × 2 = 0.5 → 向下取整仍然为 0。但如果设置maxUnavailable: 1,且配合MaxSurge: 1,2 副本滚动更新时:K8s 先删一个旧 Pod(因为 maxUnavailable=1),再启动一个新 Pod,在替换期间只有 1 个 Pod 可用。

但如果没有 PDB 保护,且 Readiness Probe 还没通过新 Pod,就会导致短暂的服务不可用。

解决方案

strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # 先启动新 Pod maxUnavailable: 0 # 确保旧 Pod 不被删

配合 PDB 保护:

apiVersion: policy/v1 kind: PodDisruptionBudget spec: minAvailable: 2 # 最少 2 个 Pod 运行

这样滚动更新时:先创建新 Pod → 等 Readiness 通过 → 再删旧 Pod,始终保证至少 2 个 Pod 在服务。

六、总结

回到小周的凌晨故障,我们逐一排查后发现了四个问题:

  1. Readiness Probe 参数不合理initialDelaySeconds=3远小于服务启动时间 12s,导致新 Pod 刚启动就被标记就绪,实际上还没准备好
  2. 缺少 PreStop Hook:SIGTERM 发出后应用来不及处理已有请求,连接被硬中断
  3. 未配置 PDB:节点升级时 3 个 Pod 同时被驱逐,服务完全不可用
  4. 使用 iptables 模式:Endpoint 同步延迟 + 全量规则刷新,加剧了流量漂移问题

优化后的配置清单:

优化项配置建议解决的核心问题
Readiness Probe 调优initialDelaySeconds=15, periodSeconds=5, failureThreshold=3避免新 Pod 过早接流量
PreStop HookDrain 标记 + 等待 Readiness 失效 + 等待请求完成已有连接不断开
PDB 保护minAvailable: 2maxUnavailable: 1防止过多 Pod 同时下线
IPVS 模式kube-proxy mode: ipvs减少 Endpoint 规则同步延迟
优雅关闭服务端/healthz/drain端点 +server.Shutdown()应用层配合 Kubernetes 生命周期
RollingUpdate 策略maxSurge: 1, maxUnavailable: 0先建后删,零下线

写这篇文章的时候,Ping 又跳到我的膝盖上蜷成一团睡着了。它睡得很沉,偶尔尾巴还会轻轻抽动一下——大概在梦里追老鼠吧。如果集群升级也能像它睡觉这么安稳就好了。

不过配置好这些策略后,小周凌晨确实再没接到过升级报警了。他说:"涵姐,现在我可以安心睡觉了。"

我说:"那你睡得比 Ping 还香了吧?"

下篇文章见~🐱

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询