RPC:优雅关闭,如何避免服务停机带来的业务损失

关闭为什么有问题

现象:需要时不时重启服务器

  • 我们知道,在“单体应用”复杂到一定程度后,我们一般会进行系统拆分,也就是微服务化。
  • 服务拆分之后,自然就需要协同,于是RPC框架就出来了,它用来解决各个子系统之间的通信问题。
  • 那系统为啥需要拆分?只说一个原因的话就是拆分之后可以更方便,更快速的迭代业务。说人话就是需要经常更新应用系统,时不时重启服务器

这个时候就必须考虑怎么优雅关闭了。

  • 优雅关闭时你在系统研发过程中,必须要考虑的问题:
    • 因为如果暴力的停止服务,那么已经发送给服务端的请求,来不及处理服务就被杀掉了,就会造成这部分请求失败,服务就会有波动
    • 所以,服务在退出的时候,都需要先停掉流量,再停止服务,这样服务的关闭才会更平滑。比如说,消息队列处理器就是要将所有,已经从消息队列中读出的消息,处理完之后才能退出

提问:重启服务时,RPC怎么做才能让调用方系统不出问题呢?

具体到RPC体系里,就要考虑,在重启服务的过程中,RPC怎么做才能让调用方系统不出问题呢?

我们必须先了解下上线的大概流程:

  • 当服务提供方要上线的时候,一般是通过部署系统完成实例重启
  • 在这个过程中,服务提供方的团队并不会事先告诉调用方他们需要操作哪些机制,从而让调用方去事先切走流量
  • 而对调用方来说,他也无法预测到服务提供方要对哪些机器重启上线,因此负载均衡就有可能会把正在重启的机器选出来,这样就会导致把请求发送到正在重启中的机器中,从而导致调用方不能拿到正确的响应结果。

在服务重启的时候,对于调用方来说,这时候可能会存在如下几种情况:

  • 调用方发请求之前,目标服务已经下线
    • 对于调用方来说,跟目标节点的连接会断开
    • 这时候调用方可以立即感知到,并且在其健康列表中会把这个节点挪掉,自然也不会被负载均衡选中
  • 调用方发请求的时候,目标服务正在关闭
    • 但是调用方不知道它正在关闭,而且两者之间的连接也没有断开
    • 所以这个节点还会存在健康列表里面,因此这个节点有一定概率会被负载均衡选中

对于第二种情况我们应该选择解决呢?是不是可以在重启机器之前,先通过“某种方式”把要下线的机器从调用方维护的“健康列表”中删除就可以了,这样负载均衡机制就选不中这个节点的?确实如此,但是这个“某种方式”到底是什么呢?

  • 最没有效率的办法就是人工通知调用方,让他们手动摘除要下线的机器

    • 这种方式很原始也很直接。
    • 但这样对于提供方上线的过程来说太繁琐了,每次上线都要通知到所有调用我接口的团队,整个过程既浪费时间又没有意义,显然不能被正常接受。
  • 我们知道,RPC里面的服务发现的作用就是“实时”感知服务提供方的状态。那么能不能先将RPC服务从注册中心的服务列表删除掉,然后观察RCP服务端没有流量之后,再将服务端停掉

在这里插入图片描述

这样不就可以实现不通过“人肉”的方式,从而达到一种自动化方式,但这么做就能完全保证实现无损上下线吗?

  • 如上图所示,整个关闭流程中依赖了两次RPC调用,一次是调用提供方通知注册中心下线操作,一次是注册中心通知服务调用方下线节点操作。
  • 注册中心通知服务调用方都是异步的,服务发现只保证最终一致性,并不保证实时性,所以注册中心在收到服务提供方下线的时候,并不能成功保证把这次要下线的节点推送到所有的调用方。所以这么来看,通过服务发现并不能做到应用无损关闭。

不能强依赖“服务发现”来通知调用方要下线的机器,那服务提供方自己来通知行不行?

  • 因为在RPC里面调用方跟服务提供方之间是长连接,我们可以在提供方应用内存里面维护一份调用方连接集合,当服务要关闭的时候,挨个去通知调用方下线这台机器。这样整个调用链路就变短了,对于每个调用方来说就一次RPC,可以保证调用的成功率很高
  • 大部分场景下,这么做确实没有问题。但是偶尔还是会有问题:出问题请求的时间点跟收到服务提供方关闭通知的时间点很接近,只比关闭通知的时间早不到 1ms,如果再加上网络传输时间的话,那服务提供方收到请求的时候,它应该正在处理关闭逻辑。这就说明服务提供方关闭的时候,并没有正确处理关闭后接收到的新请求

优雅关闭

知道了根本原因,问题就很好解决了。

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

  • 一个生活中的例子就是银行在交接班或者有其他要事情处理的时候,银行柜台工作人员会拿出一个纸板,放在窗口前,上面写到“该窗口已关闭”。在该窗口排队的人虽然有一万个不愿意,也只能换到其它窗口办理业务,因为柜台工作人员会把当前正在办理的业务处理完后正式关闭窗口。

  • 基于这个思路,我们可以这样处理:

    • 当服务提供方正在关闭,如果这之后还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方(比如shutdownException)。
    • 这个异常就是告诉调用方“我已经收到这个请求了,但是我正在关闭,并没有处理这个请求”
    • 然后调用方收到这个异常响应后,RPC框架把这个节点从健康列表中拿出,并把请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全的重试到其他节点,这样就可以实现对业务无损。
  • 但是如果只是靠等待被动,就会让这个关闭流程整体有点漫长。因为有的调用方那个时刻没有业务请求,就不能及时的通知调用方了,所以我们可以加上主动通知流程,这样既可以保证实时性,也可以避免通知失败的情况。

问题:要怎么捕获到关闭事件呢

  • 可以通过捕获操作系统的进程信号来获取
  • 在RPC启动的时候,我们提前注册关闭钩子,并在里面添加两个处理程序,一个负责关闭标识,一个负责安全关闭服务对象:
    • 服务对象在关闭的时候会通知调用方下线节点
    • 同时需要我们在调用链里面加上挡板处理器,当新的请求来的时候,会判断关闭表示,如果正在关闭,则抛出异常

问题:关闭过程中已经在处理的请求会不会被影响呢?

  • 如果进程结束过快会造成这些请求还没有来得及应答,同时调用方也会抛出异常。 为了尽可能的完成正在处理的请求,首先我们要把这些请求标识出来。
  • 这就好比日常生活中,我们经常看见停车场指示牌上提示还有多少剩余车位,这个是如何做到的呢?如果仔细观察一下,你就会发现它是每进入一辆车,剩余车位就减一,每出来一辆车,剩余车位就加一。
  • 我们也可以利用这个原理在服务对象加上引用计数器,每开始处理请求之前加一,完成请求处理减一,通过该计数器我们就可以快速判断是否有正在处理的请求
  • 服务对象在关闭过程中,会拒绝新的请求,同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭
  • 但考虑到有些业务请求可能会处理时间长,或者存在被挂住的情况,为了避免一直等待造成应用无法正常退出,我们可以在整个shutdownHook里面,加上超时时间控制,当超过了指定时间还没结束,就强制退出应用。超时时间建议10s,基本可以确保请求都处理完了。

整个流程如下图
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值