大家好,我是架构摆渡人。这是实践经验系列的第八篇文章,这个系列会给大家分享很多在实际工作中有用的经验,如果有收获,还请分享给更多的朋友。
服务部署,是一个避免不了的问题。按正常迭代的速度一般两周会发一个版本,此时就需要部署新的代码。发布方式,我相信主流的都是用滚动发布,因为这样的成本是最低的,机器数量是固定的,一台台机器轮流发布。
但是我们总会在发布过程中碰到一些报错信息,那是因为请求还没结束,某些组件已经强制停止了,比如我们的数据源,比如异步任务还没处理完。
那么如何解决这个问题呢?那就是服务优雅下线,估计大家都听过这个词,但我不知道有多少做到了随时发布都不影响功能的正常使用。
优雅下线涉及点
外部请求必须处理完
服务时时刻刻都在处理请求,一旦收到要停止的命令,那么必须等待当前的请求执行完毕才能去关闭一些资源,否则就会出现各种异常。
除了等待,还需要让外部的请求不要再过来,要告诉别人,我要下班了,不要来找我了,去找其他人吧。否则你永远都下不了班,是一样的道理。
异步任务必须处理完
这里的异步任务通常指我们放入线程池中进行处理的任务,如果强制进行程序的停止,那么线程池里的任务就会丢掉,所以除了同步被外部调用的逻辑要处理完,这种异步的逻辑也是要处理完的。
这里再提一点,就是如果异步任务丢失会对业务造成影响的这种场景,建议还是不要放到线程池里面进行处理,如果要放,那么必须有持久化,程序重启后可以继续执行。
消息必须消费完
消息也是异步任务的一种类型,我们的目标肯定也是需要让消息消费完才行。但是消息跟线程池里的任务最大的差别就在于:消息是有持久化的,并且有重试功能。
就算消息没消费完,程序强制停止,这条消息没有ACK,然后就会重试到另一台机器的实例上继续执行,前提是你的这个执行逻辑不能产生脏数据,一定要通过事务保证数据的一致性。
优雅下线解决方案
注销服务实例
下线最重要的一件事情就是注销自己的实例,这样才不会有后续的请求过来。注销实例主要是跟注册中心交互,将自己的实例从注册中心下线掉就行了。下线后服务消费者会重新从注册中心拉取最新的实例列表,也就不会将请求路由过来。
如果要下线的这个服务不是一个内部服务,而是网关呢?网关是流量的入口,客户端的请求过来的,客户端是自然不知道网关有多少实例,所以在网关前面都有一个负载均衡器,比如常用的Nginx。
那就需要将这个下线的网关实例从Nginx中进行下线操作,这样后续的流量才不会被转发过来,跟内部服务是一样的道理。
Nginx如果有独立的模块去对接注册中心的话,那么还是把注册中心的给下线掉,Nginx就能感知到下线动作。如果没有对接,而是固定的配置信息,那么就需要改Nginx的配置,然后重新加载即可。
注销MQ消费实例
通过下线注册中心里面的实例,外部流量就不会请求过来。此时还需要将MQ的实例进行下线操作,告诉MQ的服务端,不要再给我推消息了或者是客户端不再拉取消息。
实现思路
- 写一个停止流量的接口,在接口中将本身实例从注册中心,MQ进行下线操作。
- 写一个检测流量是否结束的接口,在接口中判断当前是否还有正在工作的线程,有没有正在处理的消息,有没有正在执行的异步任务等等。
- 当完全没有流量的时候,发布平台直接对当前进程进行kill操作,此时所有任务都已执行完并且没有新流量进来,无损操作。
- 执行发布流程。
这里其实涉及到一个点,就是假如3分钟了,还是有任务在处理,那么是否要强制中断?
这里其实可以这么做,就是我们的服务本身的实例一旦下线,正常的话几秒钟后就应该没有任务了,因为对外的接口基本上都是毫秒级响应。主要就怕异步任务,比如线程池里堆积了好多任务等待执行,所以大家需要去梳理下,如果有这种场景就调整,不要往线程池里堆积任务,这样才能保证在下流量的时候能够尽快执行完成。
总结
其实优雅下线的核心在于流量的切换,就是我要下线的这个服务必须把所有外部的流量都切走,然后再把没处理完的事情处理完,完成后就可以直接重新发布了。
如果你们上了容器的话,容器管理平台应该是能够提供优雅下线的方式,像K8s里面应该就有优雅停止Pod的方式,不过我对K8s不太熟,记得是有的,其实原理也很简单,先启动一个Pod,完成之后将流量切过去就行了,这种方式更简单,充分利用了容器的优势。
|