【转载】解决发版痛点:k8s配置滚动更新pod发版上线时,用户访问系统无感知
本文转载自:
https://www.heimajinpai.cn/article/15
一、问题背景
之前有个运维场景,每次发版都得要很晚,因为生产环境发版时,应用 Pod 正在滚动更新,但新的请求仍然被路由到正在销毁的 Pod,导致大量 502 Bad Gateway 和 Connection Refused 报错。用户反馈“严重影响使用”,运维侧则收到大量告警。
二、根因分析
在 K8s 滚动更新过程中,Pod 的生命周期是这样的:
-
新 Pod 启动并注册到 Nacos(注册中心)
-
旧 Pod 开始销毁(
SIGTERM信号) -
K8s 将旧 Pod 从 Service Endpoint 中移除
-
但 Nacos 服务端仍保留旧 Pod 的实例信息(默认 30 秒心跳超时)
关键问题在于:K8s 已经把 Pod 杀掉了,但 Nacos 还没同步下线。这 30 秒的窗口期内,上游服务(OpenFeign/Dubbo)仍然从 Nacos 获取到即将被销毁的实例列表,请求打到旧 Pod → 连接失败 → 报错。
三、解决方案设计
核心思路:在 Pod 销毁前,主动通知 Nacos 下线该实例。
实现方案:
-
编写一个 Nacos 服务下线脚本
-
在 Deployment 的
preStop生命周期钩子中调用该脚本 -
确保脚本在 Pod 收到
SIGTERM之前完成执行(强制等待)
四、代码实现
1. Nacos 服务下线脚本
#!/bin/sh # 使用空格分隔多个地址 NACOS_ADDRESSES="nacos地址" # 获取POD_IP SERVER_IP=$(printenv POD_IP) if [ -z "$SERVER_IP" ]; then echo "无法获取 POD_IP,请确保已注入该环境变量" exit 1 fi NAMESPACE=nacos命名空间 # 构建请求URL的基本部分 BASE_DOWN_URL="/nacos/v1/ns/instance?namespaceId=${NAMESPACE}&serviceName=${APP_NAME}&ip=${SERVER_IP}&port=${APP_PORT}&enabled=false&weight=0" # 遍历每个地址 success=0 for NACOS_URL in $NACOS_ADDRESSES; do DOWN_URL="http://${NACOS_URL}${BASE_DOWN_URL}" echo "请求地址: ${DOWN_URL}" # 调用Nacos API设置服务实例下线 curl -s -X PUT "${DOWN_URL}" > /dev/null if [ $? -eq 0 ]; then echo "${APP_NAME} 成功标记为下线状态" success=1 break else echo "警告:调用 ${NACOS_URL} 失败,尝试下一个地址..." fi done if [ $success -ne 1 ]; then echo "所有Nacos地址尝试失败,请检查网络或服务注册状态" exit 1 fi # 输出提示信息并等待一段时间让负载均衡器感知变更 echo "${APP_NAME} 正在下线,请稍等40秒..." sleep 40 echo "优雅终止流程已完成"

2. 将脚本打包到容器镜像
在 Dockerfile 中添加:
COPY nacos_deregister.py /scripts/nacos_deregister.py RUN chmod +x /scripts/nacos_deregister.py
3. Deployment YAML 配置 preStop 钩子
apiVersion: apps/v1 kind: Deployment metadata: labels: app: syty-system name: syty-system namespace: syty-prod spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 #关键点 maxSurge: 1 #关键点 selector: matchLabels: app: syty-system template: metadata: labels: app: syty-system spec: terminationGracePeriodSeconds: 400 containers: - image: dockerhub.kubekey.prod:9443/sywn/syty-system:<DOCKER_IMAGE_TAG> name: syty-system env: - name: APP_PORT value: "8083" - name: APP_NAME value: "syty-system" - name: NAMESPACE value: "syty-prod" - name: JAVA_OPTS value: "-Dserver.shutdown=graceful -Dspring.lifecycle.timeout-per-shutdown-phase=120s -Dspring.cloud.nacos.discovery.ephemeral=true" # ===== 关键:preStop 钩子 =====开始 - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP lifecycle: preStop: exec: command: ["sh", "/opt/scripts/nacos下线实例脚本名"] ports: - containerPort: 8083 name: http protocol: TCP readinessProbe: httpGet: path: /v1/common/health/info port: 8083 scheme: HTTP initialDelaySeconds: 60 timeoutSeconds: 5 periodSeconds: 5 successThreshold: 1 failureThreshold: 2 livenessProbe: httpGet: path: /v1/common/health/info port: 8083 scheme: HTTP initialDelaySeconds: 120 timeoutSeconds: 60 periodSeconds: 30 successThreshold: 1 failureThreshold: 3 # ===== 关键:preStop 钩子 =====结束 resources: limits: cpu: "2" memory: "5Gi" requests: cpu: "1" memory: "2Gi" volumeMounts: - mountPath: /logs name: sywn-logs - name: readiness-flag mountPath: /tmp volumes: - name: sywn-logs hostPath: path: /var/lib/docker/sywn-logs/syty-prod/syty-system type: DirectoryOrCreate - name: readiness-flag emptyDir: {} imagePullSecrets: - name: syty-secret --- apiVersion: v1 kind: Service metadata: name: syty-system-svc namespace: syty-prod labels: app: syty-system-svc micrometer-prometheus-discovery: "true" annotations: service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" spec: ports: - port: 9200 protocol: TCP targetPort: http name: metrics selector: app: syty-system type: ClusterIP
六、效果验证
-
发版前:查看 Nacos 控制台,当前服务有 3 个实例(对应 3 个 Pod IP)。
-
执行发版:触发滚动更新,旧 Pod 进入
Terminating状态。 -
触发钩子:
preStop执行脚本,主动调用 Nacos 注销接口。 -
Nacos 同步:服务实例数变为 2,上游请求不再路由到即将销毁的 Pod。
-
发版完成:全部 Pod 更新完成,全程零报错,用户无感知。
七、总结
通过 preStop 钩子 + Nacos 主动下线脚本,我们实现了:
-
发版过程零报错
-
服务实例平滑下线
-
用户请求无感知切换
这套方案已经稳定运行在生产环境半年,上线前可以使用压测工具进行压测的时候手动停止pod,如果没有报错说明成功,覆盖了所有通过 Nacos 注册的微服务。如果你也在使用 Nacos + K8s,强烈建议配置此机制。
本文转载自:
https://www.heimajinpai.cn/article/15

浙公网安备 33010602011771号