1、实践背景
现在java主流的微服务技术栈毫无疑问是SpringCloud,这也是经销商技术部微服务实践采用的技术栈。注册中心采用公司技术部的nacos。在SpringCloud实践中大家普遍遇到的问题是应用默认是无法做到无损下线的,需要更多的辅助措施才能得到无损下线的效果,本文主要分享我们团队解决应用无损下线的一些实践。
2、有损下线
有损下线指的是应用在下线过程中,部分请求没有被妥善处理,出现请求异常进而影响应用可用性,影响用户使用。有损下线原因分析:
损失原因1:springboot实例默认接收到停止信号TERM时,马上停止服务。如下图
当springboot实例收到TERM信号立即关闭的时候,很有可能请求队列中还有请求,还有一部分正在处理的请求。如果立即关闭这些请求都会损失掉。
解决方案:在springboot 2.3版本以上,提供了优雅关闭(Graceful Shutdown)配置。如下:
server:
shutdown: graceful
配置了优雅关闭后,实例关闭过程如下图所示:
springboot实例在收到TERM信号后,不会立即关闭应用,而是进入优雅处理阶段,这个优雅时间段时长可以配置,默认为30秒。
springboot在网络层拒绝新的请求进入的同时,会等队列中的请求和正在处理请求都处理完成或者优雅时间耗尽才关闭应用。这样只要给应用配置合适的优雅关闭阶段时长就可以避免这类请求损失。
损失原因2:在使用注册中心的默认情况下,服务下线状态无法实时通知给调用方。
在微服务引入注册中心的场景下,provider(服务提供方),registry(注册中心),consumer(服务调用方)正常服务调用过程如下:
服务下线时的时序图如下:
当provider收到TERM信号进入Graceful shutdown阶段的时候也就是图中的1时间点,provider在网络层便不再接收新的请求,然后直到时间点4,consumer才停止向provider发送请求。所以损失时间 = 时间点4 – 时间点1。
3、解决方案
方案1:provider下线尽快通知到consumer。
Ribbon默认采用轮询拉取服务列表的方式,时间间隔默认为30秒,也就是说
时间点3 – 时间点2 = 30秒 (最长)
对一个服务来说,如果30秒不可用,情况是相当糟糕的,改进方法有两个,一是缩短轮询间隔到5秒左右;二是可以实现注册中心事件推送方式通知consumer更新服务列表,尽量缩短损失时长。但是这只能治标不治本,因为虽然损失时长缩短了,但是仍然还有几秒损失,每次上线都需要损失请求仍然是不可接受的。
方案2:在provider关闭前通知到consumer下线。
如果是可以人工干预下线过程,我们大概会这么干:首先把要下线的provider实例从负载中摘除掉确保不再有流量打到下线实例后,才真正执行下线命令。这样就不会有任何流量损失了。只需要将这个流程自动化就可以完美解决了。
好在k8s提供了preStop配置,在k8s平台中,preStop和TERM信号关系是这样的:preStop会先执行完,然后k8s才会给pod发送TERM信号,k8s会给pod的停止设置一个宽限时长,超过宽限时长会强杀掉,这个宽限时长从preStop调用开始计时。采用这个方案的话,服务下线时序图如下:
可以看到当consumer不再往provider打流量后,provider才开始执行shutdown,这样不会有任何损失。
4、实现步骤
1) 缩短轮询间隔
在springboot应用中增加如下配置:
ribbon:
ServerListRefreshInterval: 5000 # 服务列表刷新间隔
2)springboot应用增加preStop hook实现
扩展springboot actuator节点,具体代码如下。
其中sleepSeconds可以根据项目配置。
3)在云平台配置preStop
4)安全加固
由于暴露了prestop hook接口,如果服务直接暴露至公网,有可能被恶意扫描调用,对接口进行安全加固是很有必要的。我们在prestop hook接口增加token参数,例如:
prestop-hook-api?token=xxxxxxxxxxxxxx
每个项目可以约定不同token值,并对token值进行校验,token值不对的访问将被拒绝,这样恶意扫描就无法访问到prestop hook 接口了。
代码实现我们已经封装在harmless-starter中并集成到项目模板里,这样使用者只需要配置sleepSeconds即可。
5、总结
本文主要记录了经销商技术部在保证SpringCloud应用无损下线的一些实践探索和总结,其中对SpringCloud服务下线造成损失的原因进行了比较全面的分析并提供了最终解决方案和实现,希望对其他人也有所帮助。