升级实例过程中实现业务不中断
应用场景
在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,直至全部替换完成。
操作步骤
本示例中,工作负载副本个数为2,并通过LoadBalance类型Service对外暴露服务。关联有Loadbalance类型Service/Ingress的工作负载滚动升级由于涉及跨服务调用,因此需要格外注意滚动升级参数的配置。
- 设置存活/就绪探针:在容器配置中选择“健康检查”,开启存活探针和就绪探针。示例中均为TCP端口检查,请根据应用实际情况进行设置。检测周期、延时时间、超时时间等数据需要合理设置,部分应用启动时间较长,如果设置的时间过短,会导致Pod反复重启。
本示例中配置就绪探针延迟探测时间为20s,用于控制工作负载批量滚动的时间间隔。
图2 存活/就绪探针
- 设置滚动升级:在高级配置中选择“升级策略”,升级方式设置为“滚动升级”,逐步用新版本实例替换旧版本实例。
本示例中配置最大无效实例数(maxUnavailable)为2%,最大浪涌(maxSurge)为2%,用于控制工作负载的滚动步长。配合就绪探针的延迟探测,控制每20s时间内,升级8个工作负载。
图3 滚动升级
- 设置优雅终止:
- 在容器配置中选择“生命周期”,设置停止前处理,建议设置为业务处理完所有剩余请求所需的时间,其中多为长连接请求。例如,您可以设置工作负载收到删除请求后休眠30s,能够有充足的时间来处理剩余的请求,保证服务的正常运行。
- 在高级配置中选择“升级策略”,设置缩容时间窗,即terminationGracePeriodSeconds参数,指定容器停止前命令执行的等待时间。缩容时间窗时间设置需大于“生命周期”的停止前处理时间,建议在容器停止前命令执行时间的基础上加30s。例如,停止前处理时间设置为30s,因此缩容时间窗设置为60s。
图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时,会同时执行两个动作:
- 从Service Endpoints中移除该Pod
- 向容器发送SIGTERM信号。
但由于kube-proxy刷新iptables规则和ELB侧摘除后端都存在延迟,可能仍有少量在途请求或已建立的连接被路由到即将终止的Pod。sleep 30s可以为这些请求提供处理时间窗口。
长连接场景:
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时的执行顺序为:
- 执行preStop Hook
- 发送SIGTERM信号
- 等待terminationGracePeriodSeconds超时
- 发送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 验证零中断更新
完成上述配置后,可通过压测工具在滚动升级期间持续发送请求,分别对对照组和实验组进行压测对比,验证零中断配置的效果。
准备工作
- 已配置好kubeconfig的节点,可正常执行kubectl命令
- 节点上已安装压测工具hey,可通过hey -h验证是否可用。
- 已分别部署已配置零中断的工作负载示例和未配置零中断的工作负载示例的工作负载。
对已配置零中断的工作负载压测验证
- 获取已配置零中断工作负载的Service访问地址:
export NGINX_ENDPOINT=$(kubectl get service nginx-demo-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}{":"}{.spec.ports[0].port}') echo $NGINX_ENDPOINT - 使用hey进行压力测试,以200并发执行100000次请求:
hey -c 200 -n 100000 -disable-keepalive http://$NGINX_ENDPOINT
- 在压测执行的同时,立即开启新终端触发滚动升级:
kubectl rollout restart deployment nginx-demo
- 观察滚动升级状态:
kubectl rollout status deployment nginx-demo
- 等待压测完成后,查看hey输出结果。
预期结果,流量没有中断:

对未配置零中断的工作负载压测验证
- 获取未配置零中断工作负载的Service访问地址:
export NGINX_ENDPOINT2=$(kubectl get service nginx-demo2-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}{":"}{.spec.ports[0].port}') echo $NGINX_ENDPOINT2 - 使用hey进行压力测试,以200并发执行100000次请求:
hey -c 200 -n 100000 -disable-keepalive http://$NGINX_ENDPOINT2
- 在压测执行的同时,立即开启新终端触发滚动升级:
kubectl rollout restart deployment nginx-demo2
- 观察滚动升级状态:
kubectl rollout status deployment nginx-demo2
- 等待压测完成后,查看hey输出结果。
预期结果,流量产生中断:
