RPC优雅关闭机制详解

时间:2023-02-11 11:57:32

1 关闭为什么有问题?

系统为啥非要拆分?更方便、更快速迭代业务,但也得经常更新应用系统,时不时还老要重启服务器。

重启服务过程中,RPC怎么做到让调用方系统不出问题?

2 上线流程

当服务提供方要上线,一般通过部署系统完成实例重启。此间,服务提供方的团队并不会事先告诉调用方他们需要操作哪些机器,从而让调用方去事先切走流量。

对调用方来说,它也无法预测服务提供方要对哪个机器重启,因此负载均衡就可能把要正重启的机器选出来,请求发到正重启的机器,导致调用方不能拿到正确响应结果。

RPC优雅关闭机制详解

在服务重启时,对调用方可能存在如下情况:

  • 调用方发请求前,目标服务已下线。对调用方来说,跟目标节点的连接会断开,这时调用方可立马感知到,并在健康列表里删除这个节点,就不会被负载均衡选中
  • 调用方发请求时,目标服务正在关闭,但调用方不知道啊,两者之间的连接也没断开,所以该节点还在健康列表里,会被负载均衡选中

3 关闭流程

RPC里怎么避免调用方业务受损呢?

重启前,先通过“某种方式”把要下线的机器从调用方维护的“健康列表”删除,这样负载均衡就选不到这个节点?是的,那“某种方式”咋完成的?

最没有效率的办法就是人工通知调用方,让他们手动摘除要下线的机器,这种方式很原始。但这对于提供方上线的过程来说太繁琐,每次上线都要通知到所有调用我接口的团队,整个过程浪费时间又没意义,不能接受。

RPC里面不是有服务发现吗?它的作用不就是用来“实时”感知服务提供方的状态吗?当服务提供方关闭前,是不是可以先通知注册中心进行下线,然后通过注册中心告诉调用方进行节点摘除?关闭流程如下:

RPC优雅关闭机制详解

这就达到一种自动化,但完全保证实现无损上下线吗?如上图,整个关闭过程依赖两次RPC调用:

  • 服务提供方通知注册中心下线操作
  • 注册中心通知服务调用方下线节点操作

注册中心通知服务调用方都是异步的,大规模集群里,服务发现只保证最终一致性,并不保证实时性,所以注册中心在收到服务提供方下线时,并不能成功保证把这次要下线的节点推送到所有调用方。so,通过服务发现不能做到应用无损关闭。

不能强依赖“服务发现”,通知调用方要下线的机器,那服务提供方自己通知行吗?因为RPC里,调用方跟服务提供方之间是长连接,可在提供方应用内存里维护一份调用方连接集,当服务要关闭,挨个去通知调用方下线这台机器。这样整个调用链路变短,对每个调用方来说就一次RPC,确保调用的成功率高。大部分场景下,这没问题,但线上还是会偶尔会出现,因为服务提供方上线而导致调用失败的问题。

分析了调用方请求日志跟收到关闭通知的日志,发现:出问题请求的时间点跟收到服务提供方关闭通知的时间点很接近,只比关闭通知的时间早不到1ms,若再加上网络传输时间,那服务提供方收到请求时,它应该正处理关闭逻辑。这就说明服务提供方关闭时,并没有正确处理关闭后接收到的新请求。

4 优雅关闭

因为服务提供方已开始进入关闭流程,那很多对象就可能已被销毁,关闭后再收到的请求按照正常业务请求来处理,肯定无法保证能处理。关闭时,可设置一个请求“挡板”,告诉调用方,我已经开始进入关闭流程了,我不能再处理你这请求。

因此,这么处理:当服务提供方正在关闭,若这之后还收到新的业务请求,服务提供方直接返回一个特定异常给调用方(如ShutdownException),告诉调用方:我已收到该请求,但我正在关闭,并没有处理这个请求!

然后调用方收到该异常响应后,RPC框架把该节点从健康列表挪出,并把请求自动重试到其他节点,因为这个请求没有被服务提供方处理过,可安全重试到其他节点,就实现了对业务无损。

但若只是靠等待被动调用,就会让这关闭过程整体较长。因为有的调用方那时没有业务请求,就不能及时通知调用方,可加上主动通知流程,这样既保证实时性,也可避免通知失败。

5 捕获关闭事件

可通过捕获os的进程信号来获取,Java里对应Runtime.addShutdownHook方法,可注册关闭的钩子。在RPC启动时,提前注册关闭钩子,并在里面添加两个处理程序:

  • 开启关闭标识
  • 安全关闭服务对象

服务对象在关闭时,会通知调用方下线节点。同时在我们的调用链里面加挡板处理器,当新请求来时,判断关闭标识,若正在关闭,则抛特定异常。

关闭过程中已在处理的请求会不会受到影响?

若进程结束过快会造成这些请求还没有来得及应答,同时调用方会也会抛异常。为尽可能完成正在处理的请求,先要把这些请求识别出来。好比停车场指示牌提示还有多少剩余车位,这如何做到?它是每进入一辆车,剩余车位就减一,每出来一辆车,剩余车位就加一。也可利用这个原理在服务对象加上引用计数器,每开始处理请求之前加一,完成请求处理减一,通过该计数器快速判断是否有正处理的请求。

服务对象在关闭过程中,会拒绝新请求,同时根据引用计数器,等待正在处理的请求全部结束后,才真正关闭。考虑到有些业务请求可能处理时间长或被夯住,为避免一直等待造成应用无法正常退出,可在整个ShutdownHook里加超时时间,超时后强制退出应用。超时时间建议设10s,基本可确保请求都处理完。

6 优雅关闭流程

RPC优雅关闭机制详解

7 总结

关闭由外到内;启动从内到外。

优雅停机,就是为了让服务提供方在停机应用的时候,保证所有调用方都能“安全”地切走流量,不再调用自己,从而做到对业务无损。其中实现的关键点就在于,让正在停机的服务提供方应用有状态,让调用方感知到服务提供方正在停机。

RPC里,关闭虽然看似不属于RPC主流程,但不处理好,可能导致调用方业务异常,从而需要很多额外运维。一个好的关闭流程,可确保使用我们框架的业务实现平滑上下线,而不用担心重启导致的问题。

优雅关闭在很多框架里面常见,Tomcat关闭也是先从外层到里层逐层关闭:

  • 先保证不接收新请求
  • 再处理关闭前收到的请求

FAQ

应用重启时,还涉及应用启动流程。如何做到优雅启动,避免请求分发到没有就绪的服务节点呢?

优雅启动,须保证内部服务启动正常后,才接受服务调用。由于现有 RPC 一般都与 Spring 结合,所以要等 Spring 容器启动完毕后,开始暴露服务。当内存 RPC 服务创建完成之后,才能向注册中心注册,此时就可以接受服务。

我们在启动服务时会由请求一个health check接口。这个接口会检查服务本身是否启动以及连接数据库等组件是否正常。只有检查通过才会注册到注册中心。

服务方万事具备后,上报信息到注册中心。

\1. 每个服务提供方方提供一个服务就绪探针 \2. 服务调用方可以周期性调用服务提供方的就绪探针来确保服务提供方已经就绪 \3. 服务端调用方通过负载均衡选出某服务节点的时候,只能从已经就绪的节点列表中选

从工程角度考虑可能不是很合适,资源有的浪费

优雅关闭:jvm中使用Runtime.addShutDownhook,关闭时执行以下流程 \1. 开启关闭挡板,拒绝新的请求 \2. 利用计数器来确保执行中的服务完整执行完 \3. 设置超时时间,保证服务正常关闭 \4. 执行关闭时,通知服务调用方列表。

对于非幂等接口,如果在关闭的时候因为超时而强行关闭,框架如果重试再调用其他服务提供方就有可能出现脏数据,所以非幂等接口不能重试。

优雅关闭: 第一不再接收新的请求,且提供一个正在关闭的异常,把请求重试到其他提供服务的机器 第二处理完已经接受的请求,为防止某些慢请求或程序挂住,设置一个超时时间 第三处理完已接受的请求或到超时时间了,则进行关闭动作,释放各种资源

优雅启动: 第一没有完全启动完毕,不去注册中心注册,不对外提供服务 第二检查各项资源准备完毕,可以对外提供服务了,则去注册中心注册 第三等待请求到来,开始一场业务逻辑的处理之旅

socket底层的如何优雅关闭socket连接。

启动成功,告诉注册中心,陆续加流量?

加流量的工作,一般都会内置在调用方逻辑。

启动的话,可以等服务接口、对象实例这些都初始化好后,再把服务注册到注册中心。是的,时机很重要!