Skip to content

滚动更新发版偶发 502

01 前置理论与核心机制

1. 为什么配了优雅停机依然会 502?

  • 两条链路的时间差:K8s 下达删除 Pod 指令时,控制面(摘除路由)和数据面(关闭容器)是并行推进的,但生效时间完全不在一个节奏上。
  • 数据面(关进程极快):Kubelet 收到指令后,会立刻向容器内的 1 号进程发送 SIGTERM 信号。业务服务收到信号,瞬间关闭端口,闭门谢客。
  • 控制面(摘路由极慢):K8s 需要先更新 Endpoints,再通知所有节点的 Kube-proxy 修改 iptables/IPVS,最后同步给 Ingress/网关。这条链路走完可能需要 2~3 秒。

2. 竞态条件引发的 502

  • 时间差酿成事故:因为网关摘除路由慢了这两三秒,它还以为旧容器活着,继续把前端进来的新流量往里打。但此时旧容器的端口已经关了,网关连不上,直接向用户抛出 502 Bad Gateway 或 Connection Refused。

02 故障排查与恢复闭环 SOP

遇到发版瞬间流量轻微跌零或报 502,不需要去查网络玄学或修改就绪探针,直接干预 Pod 的退出流程。

步骤一:观测端点摘除的真实状态

查看服务背后的端点映射,了解路由变更过程:

# 持续观察 Service 的 Endpoints 变动
kubectl get endpoints demo-svc -w
  • 观测点:看起来只是一个 Pod 被删了,实际上 K8s 内部发生了一连串的端点解绑动作,这段时间足以让旧容器提早关机。

步骤二:给容器配置增加 preStop 钩子

核心解决思路:让旧 Pod 收到终止信号后别急着退出,在原地强行等上十几秒,给网关留足时间把路由摘干净。

编辑业务的 Deployment YAML(如 app-v2.yaml),在容器的 lifecycle 中加入 preStop 配置:

        demoapp
         demoappv2

步骤三:调整宽限期参数防强杀

加了 sleep 15 后,必须确保 K8s 的强制宽限期 terminationGracePeriodSeconds 足够长(默认是 30 秒)。如果你的业务自身优雅停机需要 20 秒,加上 sleep 的 15 秒,总共需要 35 秒,就会被 K8s 提前强杀。


步骤四:执行滚动更新并验证平滑度

重新应用配置,让它开始滚动更新:

kubectl apply -f app-v2.yaml

# 查看滚动更新进度
kubectl rollout status deployment demo-app

  • 验证结果:此时能看到新 Pod 已经起来接流量了,旧 Pod 会处于 Terminating 状态并卡住 15 秒。在这 15 秒内,网关完成了路由摘除,旧 Pod 才真正开始关闭进程。流量实现 100% 平滑过渡。

03 真实生产故障现象与案例库

案例 1:Nginx Ingress 刷新延迟导致的 502

  • 故障现象:每次 Jenkins 发布新版本,API 监控面板上总会出现 1~2 秒的 502 报错尖峰。
  • 排查结果:Nginx Ingress Controller 依赖 K8s Endpoints 的变更来重载配置(Reload)。在大规模集群中,Endpoints 的变更通知推送到 Ingress 耗时达到 3 秒,而 Java 进程在 0.5 秒内就关闭了 Tomcat 端口。
  • 根因与处置:典型的路由摘除时间差。加入 preStop: sleep 15 后,故障彻底消除。

案例 2:注册中心 (Nacos/Eureka) 摘除延迟

  • 故障现象:微服务架构下,滚动更新时 RPC 消费者疯狂报错 Connection Refused。
  • 排查结果:提供者 Pod 已经退出,但 Nacos 注册中心里的节点信息还没剔除,消费者依然拿到旧的 IP 发起调用。
  • 根因与处置:除了加 sleep 等待 K8s 内部路由摘除,还需在 preStop 中加入主动调用注册中心下线 API 的命令,强制让 Nacos 提前摘除该节点,然后再 sleep。

案例 3:长连接业务 (WebSocket) 被无情切断

  • 故障现象:带有 WebSocket 聊天的服务发版时,旧连接全被暴力掐断,导致客户端大面积重连。
  • 排查结果:sleep 只能解决新流量不再进入,但无法解决已经建立的长连接。
  • 根因与处置:除了 preStop 延迟关闭外,还需要在业务代码中捕获 SIGTERM 信号。收到信号后停止接受新连接,并主动向已有的客户端发送“请重连”指令,确保长连接平滑迁移。

04 生产排障速查表(发版 502)

遇到滚动更新掉线问题,按序排查配置:顺序排查层级执行命令 / 观测点异常定性处置动作01策略核查检查 strategy: RollingUpdatemaxUnavailable 配成了 100%调整为 25% 或 1,避免一口气全删02探针核查检查 readinessProbe就绪探针未配或时间太短补充就绪探针,确保新 Pod 真活了再引流03钩子核查检查 lifecycle.preStop未配置停机钩子增加 sleep 15 抵消网络与网关延迟04宽限期核查检查 terminationGracePeriodSeconds默认 30 秒,但 sleep + 停机耗时超过 30调大该参数至 60 甚至 120 秒防强杀05注册中心检查 RPC 报错日志K8s 内正常,但微服务网关报错在 preStop 先执行 curl 下线 API 再休眠

05 面试应答参考话术

面试官问:“K8s 滚动更新明明配了 0 宕机,也做了优雅停机,为什么每次发版还是会偶发 502?”

可以这样回答:这个问题不在 Pod,也不在探针,而是在于控制面和数据面之间有两三秒的时间差。发版的时候,K8s 会同时干两件事:一是去删容器进程,二是去更新 Endpoints 和网关路由。 这两件事生效时间完全不在一个节奏上。网关摘除路由是一条很长的链路,从 Endpoints 到 Kube-proxy 再到 Ingress,走完往往需要两三秒。 但容器那边收到 SIGTERM 信号后,服务立刻关掉端口闭门谢客。网关慢了这两三秒,还以为旧容器活着,继续往里打流量,502 就来了。解决起来很简单:直接在 Deployment 里给容器加上一个 preStop 停机钩子,里面写上 sleep 15。 它的作用就是让旧 Pod 收到终止信号后别急着退出,先在原地继续接客 15 秒。这 15 秒足够网关把这条路由彻底摘干净,然后再把容器进程平滑关掉。这样就能彻底消除滚动更新时那一瞬间的 502 报错。

06 面试高频追问与应答预案

追问 1:加了 sleep 15 之后,那 Pod 真正完全退出需要多久?

  • 回答要点:
  • 总耗时 = preStop 耗时 (15秒) + 业务框架处理旧请求的耗时。
  • 必须特别注意,K8s 有一个兜底参数 terminationGracePeriodSeconds(默认 30 秒)。如果你的 sleep 加上业务停机时间超过了 30 秒,K8s 会直接发 SIGKILL 把你强杀。所以加了 sleep 后,通常要把这个宽限期参数调高到 60 秒。

追问 2:我们的集群不用 Ingress,用的是 Service Mesh(比如 Istio),还需要配 preStop 吗?

  • 回答要点:
  • 依然需要。Istio 的 Envoy Sidecar 同样依赖控制面 (Istiod) 下发的 EDS (Endpoint Discovery Service) 更新路由。只要有路由分发机制,就必然存在网络延迟与时间差。加 preStop 是屏蔽这种分布式系统延迟的最通用解法。

07 自动巡检脚本(check_prestop.sh)

该脚本用于快速扫描指定 Namespace 下的所有 Deployment,找出未配置 preStop 钩子的潜在发版掉线风险对象。

#!/usr/bin/env bash
# ==============================================================================
# 脚本名称: check_prestop.sh
# 功能描述: 自动化扫描 K8s 集群中缺失 preStop 钩子的 Deployment (纯只读)
# 使用方式: 运行在具备 kubectl 权限的机器上,可传入 namespace 参数
# ==============================================================================

set -o pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PLAIN='\033[0m'

NAMESPACE=${1:-"default"}

echo -e "${BLUE}================================================================${PLAIN}"
echo -e "${BLUE}       Deployment 发版平滑度 (preStop 钩子) 安全巡检            ${PLAIN}"
echo -e "${BLUE}================================================================${PLAIN}"
echo -e "${YELLOW}[INFO] 当前检查的 Namespace: ${NAMESPACE}${PLAIN}\n"

# 获取指定 Namespace 下的所有 Deployment 名称
DEPLOYS=$(kubectl get deploy -n "$NAMESPACE" -o custom-columns=":metadata.name" --no-headers)

if [ -z "$DEPLOYS" ]; then
    echo -e "${GREEN}[OK] 该 Namespace 下暂无 Deployment 资源。${PLAIN}"
    exit 0
fi

RISK_COUNT=0

for deploy in $DEPLOYS; do
    # 提取 preStop 配置内容
    PRESTOP=$(kubectl get deploy "$deploy" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[*].lifecycle.preStop}' 2>/dev/null)

    if [ -n "$PRESTOP" ]; then
        echo -e "  -> ${GREEN}[OK] Deployment: ${deploy} 包含 preStop 配置,具备抗发版 502 能力。${PLAIN}"
    else
        echo -e "  -> ${RED}[WARN] Deployment: ${deploy} 缺失 preStop 钩子!发版时极易出现微秒级路由断层报错。${PLAIN}"
        ((RISK_COUNT++))
    fi
done

echo -e "\n${BLUE}================================================================${PLAIN}"
if [ "$RISK_COUNT" -gt 0 ]; then
    echo -e "${RED}[汇总] 发现 $RISK_COUNT 个无 preStop 防护的 Deployment,建议补充 lifecycle.preStop: sleep 15。${PLAIN}"
else
    echo -e "${GREEN}[汇总] 巡检通过!所有 Deployment 均已配置平滑下线钩子。${PLAIN}"
fi
echo -e "${BLUE}================================================================${PLAIN}"