文档首页/ 云容器引擎 CCE/ 最佳实践/ 容器/ 升级实例过程中实现业务不中断
更新时间:2026-07-10 GMT+08:00
分享

升级实例过程中实现业务不中断

应用场景

在Kubernetes集群中,应用通常采用Deployment + LoadBalancer类型Service的方式对外提供访问。应用更新或升级时,Deployment会创建新的Pod并逐步替换旧的Pod,这个过程中可能会导致服务中断。

解决方案

避免服务中断可以从Deployment和Service两类资源入手:

  • Deployment可以采用滚动升级的升级方式,为对各个实例逐个进行更新,而不是同时对所有实例进行全部更新,可以控制Pod的更新速度和并发数,从而确保了升级过程中业务不中断。例如,可以设置maxSurge和maxUnavailable参数,控制同时创建的新Pod数量和同时删除的旧Pod数量。确保升级过程中始终有工作负载能够提供服务。
    此外,还需要配合以下机制确保升级全过程的流量平滑:
    • 启动探针(startupProbe):为启动时间较长的应用提供独立的启动检测窗口,避免因存活探针或就绪探针过早检测导致Pod反复重启。
    • 就绪探针(readinessProbe):控制Pod何时可以接收流量,未就绪的Pod会从Service Endpoints中摘除,确保只有真正就绪的Pod才会被分配请求。
    • readinessGates:在LoadBalancer类型Service场景下,Pod就绪不代表ELB后端已完成注册。readinessGates引入自定义就绪条件,确保Pod在ELB侧也完成注册后才开始接收流量。
    • 优雅终止(preStop Hook + terminationGracePeriodSeconds):Pod删除时,通过preStop Hook留出时间处理已建立的连接(尤其是长连接),配合terminationGracePeriodSeconds确保优雅关闭流程不会被SIGKILL强制中断。
  • LoadBalancer类型的Service存在两种服务亲和模式:
    • 集群级别的服务亲和(externalTrafficPolicy: Cluster):Cluster模式下,如果当前节点没有业务Pod,会将请求转发给其他节点上的Pod,在跨节点转发会丢失源IP。
    • 节点级别的服务亲和(externalTrafficPolicy: Local):Local模式下,请求会直接转发给Pod所在的节点,不存在跨节点转发,因此可以保留源IP。但是在Local模式下,如果实例滚动升级时Pod所在节点发生变化,导致ELB侧后端服务器会同步变化,可能会出现服务中断。这种情况下可以通过实例原地升级的方式避免服务中断,即保证ELB后端的节点上存在一个正常状态的Pod。

综上,实现升级实例过程中的业务不中断的方案可参考下表:

场景

Service

Deployment

不需要保留源IP

选用集群级别的服务亲和模式

滚动升级 + 启动探针/存活探针/就绪探针 + readinessGates + 优雅终止

需要保留源IP

选用节点级别的服务亲和模式

滚动升级 + 启动探针/存活探针/就绪探针 + readinessGates + 优雅终止 + 节点亲和(保证更新过程中每个节点上至少有一个Running Pod)

滚动升级过程中流量处理流程

  • 新Pod上线:启动探针通过后,就绪探针开始检测,同时readinessGates检查ELB后端是否注册完成,全部通过后Pod才被标记为Ready并开始接收流量。
  • 旧Pod下线:新Pod就绪且满足maxUnavailable约束后,Endpoints会摘除旧Pod,同时执行preStop Hook等待流量规则生效,preStop结束后发送SIGTERM信号通知应用优雅关闭,若超过terminationGracePeriodSeconds仍未退出则强制终止。
  • 循环更新:旧Pod退出后,若仍有旧Pod待更新,则继续创建新Pod,直至全部替换完成。
图1 滚动升级过程

操作步骤

本示例中,工作负载副本个数为2,并通过LoadBalance类型Service对外暴露服务。关联有Loadbalance类型Service/Ingress的工作负载滚动升级由于涉及跨服务调用,因此需要格外注意滚动升级参数的配置。

  1. 设置存活/就绪探针:在容器配置中选择“健康检查”,开启存活探针和就绪探针。示例中均为TCP端口检查,请根据应用实际情况进行设置。检测周期、延时时间、超时时间等数据需要合理设置,部分应用启动时间较长,如果设置的时间过短,会导致Pod反复重启。

    本示例中配置就绪探针延迟探测时间为20s,用于控制工作负载批量滚动的时间间隔。

    图2 存活/就绪探针
  2. 设置滚动升级:在高级配置中选择“升级策略”,升级方式设置为“滚动升级”,逐步用新版本实例替换旧版本实例。

    本示例中配置最大无效实例数(maxUnavailable)为2%,最大浪涌(maxSurge)为2%,用于控制工作负载的滚动步长。配合就绪探针的延迟探测,控制每20s时间内,升级8个工作负载。

    图3 滚动升级
  3. 设置优雅终止:
    1. 在容器配置中选择“生命周期”,设置停止前处理,建议设置为业务处理完所有剩余请求所需的时间,其中多为长连接请求。例如,您可以设置工作负载收到删除请求后休眠30s,能够有充足的时间来处理剩余的请求,保证服务的正常运行。
    2. 在高级配置中选择“升级策略”,设置缩容时间窗,即terminationGracePeriodSeconds参数,指定容器停止前命令执行的等待时间。缩容时间窗时间设置需大于“生命周期”的停止前处理时间,建议在容器停止前命令执行时间的基础上加30s。例如,停止前处理时间设置为30s,因此缩容时间窗设置为60s。
    图4 停止前命令
  4. 设置节点亲和:Service为节点级别的服务亲和模式时建议设置。在高级配置中选择“调度策略”,设置节点亲和性,在添加调度策略时,指定工作负载需要亲和的节点。
    图5 节点亲和性

配置1:配置启动探针(startupProbe)

启动探针用于判断应用容器是否已经启动完成。对于启动时间较长的应用,如果直接配置存活探针或就绪探针,可能会在应用尚未启动完成时就被判定为不健康而反复重启。startupProbe成功之前,存活探针和就绪探针都不会生效,因此可以为应用提供足够的启动时间。

示例中为HTTP端口检查,请根据应用实际情况进行设置。

startupProbe:
  httpGet:
    path: /
    port: 80
  failureThreshold: 30   # 最多尝试30次,最长等待300s启动
  periodSeconds: 10

failureThreshold × periodSeconds决定了应用的最大允许启动时间。

本示例中为 30 × 10s = 300s,适用于启动时间较长的应用。请根据实际应用启动时间调整参数,避免设置过短导致Pod反复重启。

配置2:配置就绪探针(readinessProbe)

就绪探针用于判断Pod是否已经准备好接收流量。当就绪探针检测失败时,Pod会从Service的Endpoints中摘除,不再接收新请求,但Pod不会被删除或重启。这确保了滚动升级过程中,只有真正就绪的Pod才会被分配流量。

示例中为HTTP端口检查,请根据应用实际情况进行设置。

readinessProbe:
  httpGet:
    path: /
    port: 80
  initialDelaySeconds: 5  # 容器启动后5秒开始探测
  periodSeconds: 5        # 每5秒探测一次
  timeoutSeconds: 2       # 探测超时时间
  successThreshold: 1     # 1次成功即标记为就绪
  failureThreshold: 3     # 连续3次失败即标记为不就绪

检测周期、延时时间、超时时间等数据需要合理设置,部分应用启动时间较长,如果设置的时间过短,会导致Pod尚未就绪就接收流量,引发请求失败。

配置3:配置readinessGates

在LoadBalancer类型Service(尤其是externalTrafficPolicy: Local模式)的场景下,Pod就绪并不等同于ELB后端已经完成注册。Kubernetes默认只根据readinessProbe判断Pod是否就绪,但Pod就绪后,ELB侧仍需要时间完成健康检查和后端服务器注册。如果此时就将流量转发到该Pod,可能会出现连接失败。

readinessGates允许引入自定义的就绪条件,使Pod只有在readinessProbe通过所有readinessGate条件都满足时,才会被标记为Ready并加入Service Endpoints。CCE中可以通过设置readinessGates配合ELB健康检查,确保Pod在ELB侧也完成注册后才开始接收流量。

readinessGates:
  - conditionType: target-health.elb.k8s.cce/<Service名称>
  • 设置readinessGates后,Pod需要同时满足readinessProbe和readinessGates中的所有条件才会被标记为Ready。
  • 这可以有效避免Pod已就绪但ELB尚未完成注册导致的流量丢失问题,在Local模式下尤为重要。
  • conditionType格式为target-health.elb.k8s.cce/<Service名称>,其中target-health.elb.k8s.cce 为固定前缀,<Service名称> 需替换为实际关联的Service名称。
  • Service上需通过 kubernetes.io/elb.health-check-flag: 'on' 开启ELB健康检查,readinessGates才能正常生效。

配置4:配置滚动升级策略

升级方式设置为“滚动升级”,逐步用新版本实例替换旧版本实例。

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: "25%"  # 更新过程中最多允许25%的Pod副本处于不可用状态
    maxSurge: "25%"        # 更新过程中允许创建的Pod总数最多可超出期望副本数的25%

maxUnavailable和maxSurge用于控制工作负载的滚动步长,配合就绪探针控制升级节奏,确保升级过程中始终有工作负载能够提供服务。

配置5:配置preStop Hook处理已建立的连接

当Pod被删除时,Kubernetes会先向容器发送SIGTERM信号,同时执行preStop Hook。preStop Hook的关键作用是在Pod从Service Endpoints摘除后,仍留出时间处理已经建立的连接(尤其是长连接),确保这些连接上的请求能够正常完成。

推荐的preStop Hook配置如下:

lifecycle:
  preStop:
    exec:
      # 建议根据业务自行设置钩子方法,等待存量连接处理完成然后关闭应用
      command: ["sh", "-c", "sleep 30 && /usr/sbin/nginx -s quit"]

由于当Kubernetes删除Pod时,会同时执行两个动作:

  1. 从Service Endpoints中移除该Pod
  2. 向容器发送SIGTERM信号。

但由于kube-proxy刷新iptables规则和ELB侧摘除后端都存在延迟,可能仍有少量在途请求或已建立的连接被路由到即将终止的Pod。sleep 30s可以为这些请求提供处理时间窗口。

长连接场景

如果应用存在WebSocket等长连接,preStop Hook中应主动通知应用关闭长连接,或等待长连接自然结束。例如:
lifecycle:
  preStop:
    exec:
      command:
        - /bin/sh
        - -c
        - |
          # 向应用发送优雅关闭信号(如调用管理接口)
          curl -X POST http://localhost:8080/admin/shutdown 2>/dev/null || true
          # 等待已建立连接处理完成
          sleep 30

配置6:配置terminationGracePeriodSeconds

设置缩容时间窗(terminationGracePeriodSeconds),指定Pod终止前的宽限期。

terminationGracePeriodSeconds: 60

terminationGracePeriodSeconds必须大于preStop Hook的执行时间。因为Kubernetes在删除Pod时的执行顺序为:

  1. 执行preStop Hook
  2. 发送SIGTERM信号
  3. 等待terminationGracePeriodSeconds超时
  4. 发送SIGKILL强制终止

推荐配置:terminationGracePeriodSeconds = preStop Hook执行时间 + 应用优雅关闭时间 + 缓冲时间。

  • 如果设置过小:preStop Hook可能尚未执行完就被SIGKILL强制终止,导致已建立的连接被中断,请求处理失败。
  • 如果设置过大:会导致Pod删除速度变慢,影响滚动升级的整体效率。

本示例中preStop Hook为30s,应用优雅关闭通常需要10-20s,因此设置terminationGracePeriodSeconds为60s。

配置7:配置节点亲和(Local模式下需要)

Service为节点级别的服务亲和模式时建议设置。在添加调度策略时,指定工作负载需要亲和的节点。详情请参见设置节点亲和调度(nodeAffinity)

配置示例

本文通过两个配置示例进行说明。

已配置零中断的工作负载示例

以下为包含Deployment和Service的完整YAML示例,涵盖滚动升级、健康检查探针、readinessGates、优雅终止等全部优化配置:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-demo
  # 滚动更新策略:保证更新期间服务不中断
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: "25%"  # 更新过程中最多允许25%的Pod副本处于不可用状态
      maxSurge: "25%"        # 更新过程中允许创建的Pod总数最多可超出期望副本数的25%
  template:
    metadata:
      labels:
        app: nginx-demo
      # Pod就绪状态检查,使用了ELB的功能
      # target-health.elb.k8s.cce为固定前缀,后缀值与关联的Service名称相同
      readinessGates:
        - conditionType: target-health.elb.k8s.cce/nginx-demo-service
    spec:
      # 优雅下线的最大时长,超过设定的秒数会强制关闭Pod
      # 需大于preStop Hook处理已有连接的时长
      terminationGracePeriodSeconds: 60
      containers:
      - name: nginx
        image: nginx:perl
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 500m
        # 启动探针 (Startup Probe):确保容器内应用已完成启动
        # startupProbe成功之前,存活探针和就绪探针不会生效
        startupProbe:
          httpGet:
            path: /
            port: 80
          failureThreshold: 30   # 最多尝试30次,最长等待300s启动
          periodSeconds: 10
        # 就绪探针 (Readiness Probe):判断容器是否准备好接收流量
        # 未就绪的Pod会从Service Endpoints中摘除,不会被分配请求
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 5  # 容器启动后5秒开始探测
          periodSeconds: 5        # 每5秒探测一次
          timeoutSeconds: 2       # 探测超时时间
          successThreshold: 1     # 1次成功即标记为就绪
          failureThreshold: 3     # 连续3次失败即标记为不就绪
        # 服务优雅下线配置
        lifecycle:
          preStop:
            exec:
              # 建议根据业务自行设置钩子方法,等待存量连接处理完成然后关闭应用
              command: ["sh", "-c", "sleep 30 && /usr/sbin/nginx -s quit"]
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo-service
  labels:
    app: nginx-demo
  namespace: default
  annotations:
    kubernetes.io/elb.class: performance  # 表示使用独享型ELB
    kubernetes.io/elb.id: xxxxxxxxxx  # 替换为实际ELB的id
    kubernetes.io/elb.health-check-flag: 'on'
    kubernetes.io/elb.health-check-option: '{"protocol":"TCP","delay":"5","timeout":"10","max_retries":"3"}'
spec:
  selector:
    app: nginx-demo
  externalTrafficPolicy: Cluster
  ports:
    - name: cce-service-0
      targetPort: 80
      nodePort: 0
      port: 8080
      protocol: TCP
  type: LoadBalancer
  • readinessGates中的conditionType格式为target-health.elb.k8s.cce/<Service名称>,其中<Service名称>需替换为实际关联的Service名称。Service上需通过 kubernetes.io/elb.health-check-flag: 'on' 开启ELB健康检查。
  • preStop Hook中sleep 30用于等待ELB侧摘除后端和iptables规则刷新完成,nginx -s quit用于优雅关闭Nginx并处理已建立的连接。请根据实际应用替换关闭命令。
  • terminationGracePeriodSeconds 需大于preStop Hook执行时间(本示例中sleep 30s + nginx quit),建议预留30s缓冲,设置为60s。

未配置零中断的工作负载示例

以下为未配置任何零中断优化参数的Deployment和Service示例,仅包含基础配置,用于后续压测对比:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-demo2
  template:
    metadata:
      labels:
        app: nginx-demo2
    spec:
      containers:
      - name: nginx
        image: nginx:perl
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 500m
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo2-service
  labels:
    app: nginx-demo2
  namespace: default
  annotations:
    kubernetes.io/elb.class: performance  # 表示使用独享型ELB
    kubernetes.io/elb.id: xxxxxxxxxx  # 替换为实际ELB的id
    kubernetes.io/elb.health-check-flag: 'on'
    kubernetes.io/elb.health-check-option: '{"protocol":"TCP","delay":"5","timeout":"10","max_retries":"3"}'
spec:
  selector:
    app: nginx-demo2
  externalTrafficPolicy: Cluster
  ports:
    - name: cce-service-0
      targetPort: 80
      nodePort: 0
      port: 8082
      protocol: TCP
  type: LoadBalancer

验证零中断更新

完成上述配置后,可通过压测工具在滚动升级期间持续发送请求,分别对对照组和实验组进行压测对比,验证零中断配置的效果。

准备工作

对已配置零中断的工作负载压测验证

  1. 获取已配置零中断工作负载的Service访问地址:

    export NGINX_ENDPOINT=$(kubectl get service nginx-demo-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}{":"}{.spec.ports[0].port}')
    echo $NGINX_ENDPOINT

  2. 使用hey进行压力测试,以200并发执行100000次请求:

    hey -c 200 -n 100000 -disable-keepalive http://$NGINX_ENDPOINT

  3. 在压测执行的同时,立即开启新终端触发滚动升级:

    kubectl rollout restart deployment nginx-demo

  4. 观察滚动升级状态:

    kubectl rollout status deployment nginx-demo

  5. 等待压测完成后,查看hey输出结果。

    预期结果,流量没有中断:

对未配置零中断的工作负载压测验证

  1. 获取未配置零中断工作负载的Service访问地址:

    export NGINX_ENDPOINT2=$(kubectl get service nginx-demo2-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}{":"}{.spec.ports[0].port}')
    echo $NGINX_ENDPOINT2

  2. 使用hey进行压力测试,以200并发执行100000次请求:

    hey -c 200 -n 100000 -disable-keepalive http://$NGINX_ENDPOINT2

  3. 在压测执行的同时,立即开启新终端触发滚动升级:

    kubectl rollout restart deployment nginx-demo2 

  4. 观察滚动升级状态:

    kubectl rollout status deployment nginx-demo2 

  5. 等待压测完成后,查看hey输出结果。

    预期结果,流量产生中断:

相关文档