您的位置:  首页 > 技术杂谈 > 正文

K8S优雅升级系列(中) | 如何“优雅”滚动发布?看这篇就够了

2022-09-01 10:00 https://my.oschina.net/u/4580203/blog/5571941 汉得数字平台 次阅读 条评论

图片

什么是优雅升级?

首先,我们认为发布过程中如果遇到以下的问题都是“不优雅”的:

  • 发布过程中,出现正在执行的请求被中断;

  • 下游服务节点已经下线,上游依然继续调用已经下线的节点导致请求报错,进而导致业务异常;

  • 发布过程造成数据不一致,需要对脏数据进行修复。

那么,反之,优雅就是一种避免上述情况发生的手段,就是在服务升级的时候,不中断整个服务,让用户无感知,不影响用户体验

如何优雅滚动发布

前面一篇讲了微服务应用发布方式、应用优雅下线以及容器优雅关闭的相关内容,接下来就分析下K8S滚动更新中,在什么样的情况下服务会中断以及相关的解决方案。

分析

前面在应用发布章节描述了K8S滚动发布的原理,Deployment 滚动更新时会先创建新 Pod,等待新 Pod Running 后再删除旧 Pod。

在K8S的网络中,Service是借助于Endpoint资源来跟踪与其相关联的后端服务,Service会根据Selector直接创建同名的Endpoint对象,Endpoint对象会根据就绪状态把同名Service 对象标签选择器筛选出的后端端点的IP地址分别保存在Subsets.addresses字段和Subsets.notReadyAddresses字段中,它通过API Server持续、动态跟踪每个端点的状态变动,并即时反映到端点IP所属的字段。

图片

在Deployment对象对POD进行滚动更新的时候,根据Endpoint的机制,在新建POD以及删除POD的时候,会发生Endpoint的实时更新,大多数的服务的中断,就在这两部分Endpoint列表变更的时候发生。

 

POD新建导致服务中断

具体原因

Pod Running后被加入到Endpoint后端,容器服务监控到Endpoint变更后将Pod Ip加入到SLB后端。此时请求从SLB转发到Pod中,但是Pod业务代码还未初始化完毕,无法处理请求,导致服务中断。

解决方法

为Pod配置就绪检测,等待业务代码初始化完毕后后再将Node加入到SLB后端。

Hzero1.7之前,整个系统依赖的Springboot2.06,默认只有/actuator/health这一个健康检查端口,产线环境Liveness建议使用/actuator/info,可以提高服务稳定性。

图片

Hzero 1.7版本开始,整个系统依赖Springboot2.4.6,Springboot2.3.0之后,Spring在提供了/actuator/health/readiness和/actuator/health/liveness两个接口分别适配K8S的两个探针,建议健康。

图片

 

POD删除导致服务中断

在Deployment做滚动更新时,一旦有新版本的Pod启动,就会删除旧的Pod,一旦Kubernetes决定终止您的Pod,就会发生一系列事件,需要对多个对象(如 Endpoint、Ipvs/iptables、SLB)进行状态同步,并且这些同步操作是异步执行的。

Pod在删除的时候,大致的生命周期如图所示:

图片

1. Pod状态变更

将Pod设置为Terminating状态,并从所有Service的 Endpoints列表中删除。此时,Pod停止获得新的流量,但在Pod中运行的容器不会受到影响,将会继续处理之前的请求;

2. 执行PreStop Hook

Pod删除时会触发PreStop Hook,PreStop Hook支持Bash脚本、TCP或HTTP请求;

3. 发送SIGTERM信号:

向Pod中的容器发送SIGTERM信号;

4. 等待指定的时间:

TerminationGracePeriodSeconds字段用于控制等待时间,默认值为30秒。该步骤与PreStop Hook同时执行,因此TerminationGracePeriodSeconds需要大于PreStop的时间,否则会出现PreStop未执行完毕,Pod就被Kill的情况;

5. 发送SIGKILL信号:

等待指定时间后,如果容器在优雅终止宽限期后仍在运行,则会发送SIGKILL信号并强制删除。与此同时,所有的Kubernetes对象也会被清除。

具体原因

上述 1、2、3、4步骤同时进行,因此有可能存在Pod收到SIGTERM信号并且停止工作后,还未从Endpoints中移除的情况。此时,请求从Slb转发到Pod中,而Pod已经停止工作,因此会出现服务中断。

解决方法

为Pod配置PreStop Hook,使Pod收到SIGTERM时Sleep一段时间而不是立刻停止工作,从而确保从SLB转发的流量还可以继续被Pod处理,同时需要配合修改最大宽限时间(TerminationGracePeriodSeconds)

最大宽限时间修改示例:

  •  
最大宽限时间修改示例:apiVersion: v1kind: Podmetadata:  name: nginx  namespace: defaultspec:  containers:  - name: nginx    image: nginx  terminationGracePeriodSeconds: 50

 

K8S优雅关闭POD-解决方案

现在我们的目标就是如何增强我们的应用程序能力,让它以真正的零宕机更新版本,首先,实现这个目标的前提条件是我们的容器要能正确处理终止信号,即进程会在SIGTERM上优雅地关闭。

解决方案1

第一种思路,在K8S Deployment资源文件种配置响应PreStop Hook,并且将TerminationGracePeriodSeconds调整为30以上(比Prestop大即可)

图片

这里等待30s主要是用于等待处理残余流量,当然,Prestop如果是定位为30s,那么相应的TerminationGracePeriodSeconds得修改肯定要比30s大,比如40s。

当然,这个时候也需要配合应用和容器本身的优雅关闭,这样才能从应用、容器、K8S滚动三个层面同时实现优雅,但是SpringCloud本身的流量控制也比较多环节,Sleep 30s也并不能做到100%的零宕机,只能说是能减少很大一部分的滚动更新造成的宕机问题。

Hzero1.7版本之后,Hzero底层使用的是SpringBoot 2.46,在Spring Boot 2.3.0中,优雅停机非常容易实现,并且可以通过在应用程序配置文件中设置两个属性来进行管理

  •  Server.shutdown:此属性可以支持的值有

    • Immediate:这是默认值,将导致服务器立即关闭。

    • Graceful:启用优雅停机,并遵守Spring.lifecycle.timeout-per-shutdown-phase属性中给出的超时。

  • Spring.lifecycle.timeout-per-shutdown-phase:采用java.time.Duration格式的值。

配置示例:

可以在JVM参数中添加JAVA_OPTS= -Dserver.shutdown=graceful -Dspring.lifecycle.timeout-per-shutdown-phase=30s

再配合将Prestop Sleep等待时间设置为40S,将TerminationGracePeriodSeconds设置为50s。

配置效果:

应用中没有正在进行的要求。在这种情况下,应用程序将会直接关闭,而无需等待宽限期结束后才关闭。

如果应用中有正在处理的请求,则应用程序将等待宽限期结束后才能关闭。如果应用在宽限期之后仍然有待处理的请求,应用程序将抛出异常并继续强制关闭,但是这个配置只是该SpringBoot服务本身的服务优雅关停,还没涉及到SpringCloud流量控制等问题,相比较之前的关闭方式,多了应用本身的等待处理,在此期间Hzero-register以及Hzero-gateway有充足的时间去刷新服务,降低报错率,理论上无法完全规避滚动更新中Hzero架构服务中断的风险,但是优点在于方便快捷,不用代码侵入,能解决绝大部分问题。

想要做到完美滚动,那就还需要对应的流量剔除的动作配合起来使用。

 

解决方案2

结合K8S的健康检查,和Springboot(2.3版本以上)中的自定义事件监听器,来构建自定义的健康检查结构,在Prestop的时候主动调用接口改变应用监听状态为拒绝接收流量,然后给参数约40秒事件去处理剩余流量,Readiness探针通过健康检查会提前主动将Endpoint剔除,然后POD再进行关闭,这样就避免了滚动更新时候的流量损耗,这里需要自定义代码开发,开发部分有两个最重要的逻辑。

Eureka流量剔除实现

就是以上说的EurekaAutoServiceRegistration可以实现​​​​​​​

@RestController@RequestMapping(value = "/graceful/registry-service")public class GracefulOffline {    @Autowired    private EurekaAutoServiceRegistration eurekaAutoServiceRegistration;    @RequestMapping("/online")    public String online() {        this.eurekaAutoServiceRegistration.start();        return "execute online method, online success.";    }    @RequestMapping("/offline")    public String offline() {        this.eurekaAutoServiceRegistration.stop();        return "execute offline method, offline success.";    }}

K8S流量剔除实现​​​​​​​

  •  
package com.adidas.token.api.v1;
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.availability.AvailabilityChangeEvent;import org.springframework.boot.availability.ReadinessState;import org.springframework.context.ApplicationEventPublisher;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
/** * control kubernetes readiness so we can stop traffic before kill pod * <p> * url could configure in k8s "preStop" */@RestController@RequestMapping("/readiness")public class ReadinessProbeController {
    private static final Logger logger = LoggerFactory.getLogger(ReadinessProbeController.class);
    @Autowired    private ApplicationEventPublisher applicationEventPublisher;
    @GetMapping("/out-of-rotation/{sleepTime}")    public String takeOOR(@PathVariable("sleepTime") Long sleepTime) {        logger.info("start to take service out of rotation");        AvailabilityChangeEvent.publish(applicationEventPublisher, "ReadinessProbeController", ReadinessState.REFUSING_TRAFFIC);        try {            Thread.sleep(sleepTime * 1000);        } catch (InterruptedException e) {            logger.warn("exception when take service out of rotation", e);        }        logger.info("finish to take service out of rotation");        return "REFUSING_TRAFFIC";    }
    @GetMapping("/take-into-rotation")    public String takeIntoRotation() {        logger.info("start to take service into rotation");        AvailabilityChangeEvent.publish(applicationEventPublisher, "ReadinessProbeController", ReadinessState.ACCEPTING_TRAFFIC);        logger.info("finish to take service into rotation");        return "ACCEPTING_TRAFFIC";    }}

目前对于Springcloud应用来说最保险的方式,就是需要走这样的流量剔除的一套流程,上面两部分代码逻辑可以结合起来使用,封装成两个方法进行调用,需要自开发相关代码去控制:

1. Eureka 节点剔除   ---  把服务从Eureka的服务清单里里面标记成下线状态;

2. K8s 流量剔除  ---把服务的Readiness状态改成下线状态(Springboot2.3以上可以用AvailabilityChangeEvent实现);

3. 等待服务处理剩余流量;

4. 等待时间到,服务停掉;

 

以上,就是总结的在K8S滚动更新中,零宕机目标的几种解决方案,大家在进行项目交付或者实施中可以根据实际情况来选择不同的方案。下篇我们将讲述如何在项目实战中进行配置?敬请期待。

​​​​​​​

联系我们

产品试用请登录开放平台。请在 PC 端打开:

https://open.hand-china.com/market-home/trial-center/

产品详情请登录开放平台:

https://open.hand-china.com/document-center/

如有疑问登录开放平台提单反馈

https://open.hand-china.com/

图片

图片

▲ 更多精彩内容,扫码关注 “四海汉得” 公众号

展开阅读全文
  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接