K8S优雅升级系列(中) | 如何“优雅”滚动发布?看这篇就够了
什么是优雅升级?
首先,我们认为发布过程中如果遇到以下的问题都是“不优雅”的:
发布过程中,出现正在执行的请求被中断;
下游服务节点已经下线,上游依然继续调用已经下线的节点导致请求报错,进而导致业务异常;
发布过程造成数据不一致,需要对脏数据进行修复。
那么,反之,优雅就是一种避免上述情况发生的手段,就是在服务升级的时候,不中断整个服务,让用户无感知,不影响用户体验。
如何优雅滚动发布
前面一篇讲了微服务应用发布方式、应用优雅下线以及容器优雅关闭的相关内容,接下来就分析下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: v1
kind: Pod
metadata:
name: nginx
namespace: default
spec:
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/
如有疑问登录开放平台提单反馈:
▲ 更多精彩内容,扫码关注 “四海汉得” 公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
【案例回顾】春节一次较波折的MySQL调优
春节长假某日,阳光明媚,春暖花开,恰逢冬奥会开幕,想着一定是一个黄道吉日,必能顺风顺水。没想到却遇到一个有点小波折的客户报障。 01 故障起因 故障起因是客户前一天从自建MySQL迁移到云上RDS,在执行某个并发较高的业务时出现了大量锁等待,客户当时升级了实例到最高规格,但故障依旧。客户反馈升级后的实例规格比自建实例高了一倍,自建实例上从未发生过类似情况。后客户根据当时的业务故障模拟了现场,主要是并发执行如下存储过程的时候性能很差: 02 初步诊断 从存储过程的逻辑看,比较简单,主要涉及两个SQL,一个从表t(隐藏了真实表名)中meeting_id根据传入参数值查询,具体的入参由字符型变量p_meeting_id带入;另外一个根据meeting_id和刚查出的phone_id去更新t中的phone_id为phone_id+3。表t数据量约40w左右。 第一感觉这是个简单问题,估计两个SQL的meeting_id索引没有生效,查询表上索引后果然发现meeting_id和phone_id上没有索引,建议客户在两个字段上分别创建了索引,且meeting_id为主键。此时用户执行模拟的并发脚...
- 下一篇
亚马逊云科技实时数仓相关产品的特点和优势
近年来,各级政府和企业响应数字化转型的号召,都已开始或者即将开始数字化转型。各类企业通过前期的业务线上化、信息化,积累了大量数据,而数字化转型就是要聚合这些数据,进行深入挖掘分析,用数据来驱动业务,用数据来支撑决策、用数据来推动业务和商业模式创新、推动业务流程优化,进而实现降本增效。 要实现数据价值,建设数据仓库是在数字化转型过程中不得不面对的一项任务。数据仓库汇聚各个业务部门数据,避免数据孤岛,使数据真正成为整个企业的数据,而不是某个部门的数据。 数据仓库的技术架构包括离线数仓和实时数仓或准实时数仓。离线数仓已发展多年,当前已无法完全满足企业在竞争中脱颖而出的发展需求,实时数仓越来越多成为企业建设数据仓库的首选。然而由于实时数仓对实时性的严格要求,实现实时数仓的技术难度远远大于离线数仓,一些现有的实时数仓架构,只能实现准实时,而且无法解决削峰平谷、无感扩展等问题。 本文为大家提供一种高效的实时数仓架构:基于亚马逊云科技 Serverless 架构的实时数仓架构。 实时数仓常见场景与亚马逊云科技的做法赏析 我们先来赏析一下常见的实时数仓场景,以及亚马逊云科技Serverless架构的实...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS关闭SELinux安全模块
- CentOS8编译安装MySQL8.0.19
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Red5直播服务器,属于Java语言的直播服务器
- Windows10,CentOS7,CentOS8安装Nodejs环境