【转载】解决发版痛点:k8s配置滚动更新pod发版上线时,用户访问系统无感知

本文转载自:

https://www.heimajinpai.cn/article/15

 

一、问题背景

之前有个运维场景,每次发版都得要很晚,因为生产环境发版时,应用 Pod 正在滚动更新,但新的请求仍然被路由到正在销毁的 Pod,导致大量 502 Bad GatewayConnection Refused 报错。用户反馈“严重影响使用”,运维侧则收到大量告警。

二、根因分析

在 K8s 滚动更新过程中,Pod 的生命周期是这样的:

  1. 新 Pod 启动并注册到 Nacos(注册中心)

  2. 旧 Pod 开始销毁(SIGTERM 信号)

  3. K8s 将旧 Pod 从 Service Endpoint 中移除

  4. Nacos 服务端仍保留旧 Pod 的实例信息(默认 30 秒心跳超时)

关键问题在于:K8s 已经把 Pod 杀掉了,但 Nacos 还没同步下线。这 30 秒的窗口期内,上游服务(OpenFeign/Dubbo)仍然从 Nacos 获取到即将被销毁的实例列表,请求打到旧 Pod → 连接失败 → 报错。

三、解决方案设计

核心思路:在 Pod 销毁前,主动通知 Nacos 下线该实例

实现方案:

  1. 编写一个 Nacos 服务下线脚本

  2. 在 Deployment 的 preStop 生命周期钩子中调用该脚本

  3. 确保脚本在 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 "优雅终止流程已完成"

image

 

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

六、效果验证

  1. 发版前:查看 Nacos 控制台,当前服务有 3 个实例(对应 3 个 Pod IP)。

  2. 执行发版:触发滚动更新,旧 Pod 进入 Terminating 状态。

  3. 触发钩子preStop 执行脚本,主动调用 Nacos 注销接口。

  4. Nacos 同步:服务实例数变为 2,上游请求不再路由到即将销毁的 Pod。

  5. 发版完成:全部 Pod 更新完成,全程零报错,用户无感知。

七、总结

通过 preStop 钩子 + Nacos 主动下线脚本,我们实现了:

  1. 发版过程零报错

  2. 服务实例平滑下线

  3. 用户请求无感知切换

这套方案已经稳定运行在生产环境半年,上线前可以使用压测工具进行压测的时候手动停止pod,如果没有报错说明成功,覆盖了所有通过 Nacos 注册的微服务。如果你也在使用 Nacos + K8s,强烈建议配置此机制。

 

本文转载自:

https://www.heimajinpai.cn/article/15

 

posted @ 2026-09-01 10:08  北极之光的博客  阅读(10)  评论(0)    收藏  举报