会员服务优雅上下线实践
01
异常情况分析
业务系统当前使用的是 Spring Boot 和 Spring Cloud 框架,服务发布流程如下图所示:
通过梳理服务流程发现,引起服务可用性降低、响应时间突增的原因有以下几点:
过早销毁对象:服务正在处理请求,但此时对象被销毁导致请求报错。
服务未及时下线:调用方不能及时感知服务已在下线中,仍会发送请求过来,但此时对象可能已经被销毁导致请求报错。
过早注册服务:服务未初始化完成就被注册到了注册中心,导致接口响应时间突增甚至超时。
优化方向
基于上面的分析,可以通过以下方式解决相关的问题:
在上线过程中,当服务把依赖的资源都初始化完成后,才将实例注册到注册中心。
在下线过程中,服务调用方可以排除正在下线的实例,保证在一定的时间窗口内请求不会打到这个实例上。
02
解决方案
优雅上线
通过预热功能实现资源初始化,预热模块是可插拔的,可全使用或者仅使用其中一个模块:
自定义预热:由业务方自行扩展实现预热逻辑。
线上请求回放预热:配置预热接口,拉取线上请求对本地服务预热,当接口调用达到配置的预热次数后,再将服务注册到注册中心。
预热
在原生Spring Cloud Netflix基础上,定制开发了服务注册组件 GracefulServiceRegistration 并抽象了预热组件 WarmUp。在 Spring 容器初始化过程中,会扫描所有 WarmUp 实现类并注入到容器中,启动完成后由 GracefulServiceRegistration 组件调用 WarmUp 接口所有实现类的预热方法进行服务预热,预热完成之后再进行服务注册。
图例说明:
VClientAutoConfiguration:服务注册配置类,负责初始化GracefulServiceRegistration
GracefulServiceRegistration:服务注册类,触发服务预热逻辑执行
WarmUp:预热组件,由业务方自行扩展实现预热逻辑。框架默认实现:延迟5s(可配置)再执行服务注册、线上请求回放预热
优雅下线
优雅下线通过延迟下线和可靠负载功能组合实现,在下线过程中,服务实例需要先去取消注册并将自己标记为已下线,后续的接口请求都将获取到该实例的已下线标记。服务调用方根据下线标记把该实例从可用服务列表中剔除,保证在后续一定时间窗口内的请求都不会再打到这个实例上。具体交互流程见下图:
延迟下线
上文提到在原生Spring Cloud Netflix基础上,定制开发了 GracefulServiceRegistration、WarmUp 等组件,在解决优雅下线问题时,我们又增加了调用插件 InvokePlugin。当 JVM 监听到 SIGTERM 信号时,下线钩子线程开始工作,先执行取消注册,然后通过 GracefulServiceRegistration 标记当前服务为下线中状态,并阻塞当前线程 5s(可配置)来保证当前正在处理的请求能够成功返回。如果此时收到调用方请求,InvokePlugin 会检查当前服务状态是否为下线中,如果是,直接返回下线标记。最后下线钩子线程被唤醒,再执行对象销毁逻辑。
图例说明:
VClientAutoConfiguration:服务注册配置类,负责初始化 InvokePlugin、GracefulServiceRegistration 等组件
GracefulServiceRegistration:服务注册类,负责延迟销毁对象、触发服务预热逻辑执行
InvokePlugin:请求调用插件类,负责执行请求时检查服务实例状态是否在下线中,如果在下线中,直接返回下线标记
可靠负载
微服务框架使用Ribbon作为负载均衡策略,默认是轮询机制,BaseLoadBalancer 中维护了两个注册表集合:全量注册表 allServerList、可用注册表 upServerList,但是原生只使用了全量注册表,通过循环判断获取可用实例,这种方式可能会获取到不可用的实例,所以我们对逻辑进行了优化,新增一个路由规则,使用可用注册表保存可用服务实例,并增加任务剔除标记已下线的实例。
负载策略实现方式为,服务调用方接收到实例的下线标记时,将该实例加入失活队列,独立的任务线程处理失活队列,并维护可用注册表,且失活队列的另一个任务是在同步注册中心最新注册表的时候,不要把已排除的实例恢复到可用注册表中。通过重试 + 排除下线实例的方式,使业务得到更高的可用性。具体设计如下:
03
成果与总结
对接优雅上下线功能的服务在上线过程中,服务成功率可以提升到99.99%以上,有效解决了服务上线成功率的问题。对比数据见下图示例:
无优雅上下线(并行1台滚动上线)
开启优雅上下线(并行1台滚动上线)