每次公司做版本滚动更新,Sentry 上的报错和用户投诉就准时像雪花一样飘过来,绝大多数都是网关报出来的 502 Bad Gateway。开发觉得很无辜:“我们明明用了 K8s 默认的 RollingUpdate 滚动发布,而且新旧容器也是交替启动的,为什么还会影响用户访问?”

这里面有一个新手最容易踩的大坑:把“容器跑起来了(Running 状态)”等同于“服务能对外提供访问了”。

对于那些稍微重一点的微服务应用(比如 Java Spring Boot,或者需要连接多个数据库、进行 Redis 预热的 Node.js 应用),容器从启动到能够处理第一个 HTTP 请求,中间往往有 10 到 40 秒的 cold start 时间。如果你的 YAML 里没有配置 readinessProbe,Kubernetes 只要看到容器的 Entrypoint 进程起来了,就会立马把这个 Pod 加进 Service 的 Endpoints 列表里,并把老 Pod 杀掉。于是大量的外部用户流量瞬间涌入一个还在加载依赖、初始化数据库连接的“空壳”容器中,不报 502 才怪!

不仅如此,有些开发为了防止服务还没起来就被 K8s 误杀,干脆把 livenessProbeinitialDelaySeconds 设得极大(比如 120 秒)。这又带来了另外一个心智负担:如果容器在启动阶段真的卡死了,K8s 却要干等两分钟才会去重启它,极大地拉长了故障恢复时间。

为了彻底摆脱 502 轰炸,我们必须合理配置 Kubernetes 的三大健康检查探针:startupProbe(启动探针)、readinessProbe(就绪探针)和 livenessProbe(存活探针)。

以下是我们团队为生产环境 Java 微服务总结出的一套高可用、黄金配比的 Deployment YAML 配置文件示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service-deployment
  namespace: production
  labels:
    app: payment-service
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0  # 确保发布过程中,可用副本数绝不低于期望值
  selector:
    matchLabels:
      app: payment-service
  template:
    metadata:
      labels:
        app: payment-service
    spec:
      containers:
        - name: payment-service-container
          image: registry.company.com/library/payment-service:v2.1.4
          ports:
            - containerPort: 8080
              name: http-port
          # 1. 启动探针:先由它来接管冷启动阶段。在它成功前,其他两个探针都处于禁用状态
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http-port
            # 每次检测间隔 5 秒,最多重试 30 次,一共给应用 150 秒的启动宽限期
            failureThreshold: 30
            periodSeconds: 5
            successThreshold: 1
            timeoutSeconds: 2
          # 2. 就绪探针:只决定 Pod 是否挂载到流量入口。如果失败,仅剔除流量,不重启容器
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: http-port
            # 启动探针通过后,每 10 秒检测一次,只要有 1 次失败就立刻从 Service Endpoints 中剔除
            periodSeconds: 10
            failureThreshold: 2
            successThreshold: 1
            timeoutSeconds: 3
          # 3. 存活探针:只负责决定是否重启容器。尽量使用轻量的端点,避免因数据库抖动导致容器被无脑重启
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http-port
            # 启动探针通过后,每 15 秒检测一次,连续 3 次失败则重启 Pod
            periodSeconds: 15
            failureThreshold: 3
            successThreshold: 1
            timeoutSeconds: 3

在这套配置中,startupProbe 作为前哨,负责处理漫长的类加载与初始化工作。当它成功之后,readinessProbelivenessProbe 才会启动。

这里还有个关键细节:readinessProbefailureThreshold 设置得比较敏感(为 2),这样一旦下游数据库发生抖动,或者应用内部线程池打满拒绝请求时,Pod 能够迅速被摘除,不再接收新流量。而 livenessProbe 的阈值设得比较宽松(为 3),这样能容忍短期的瞬时波动,避免容器在业务高峰期因为短暂响应慢而被无脑杀掉重启,从而引发雪崩效应。

引入这套三层防线配置之后,我们项目的滚动发布终于实现了真正的“0 故障无缝平滑过渡”。

你们的集群里有遇到过因为探针写得太重(比如就绪探针直接去 SELECT 数据库),导致数据库抖动引发所有 Pod 被 K8s 疯狂循环重启的情况吗?这种情况下你们是怎么做降级处理的?