服务关闭有什么问题?
在“单体应用”复杂到一定程度后,一般会进行系统拆分,也就是微服务架构。服务拆分之后,就需要协同,于是RPC框架就出来了,用来解决各个子系统之间的通信问题。
拆分系统的目的,快速地迭代业务,就会涉及到更新业务系统,重启服务器。
服务上线大概流程:
当服务提供方要上线的时候,一般是通过部署系统完成实例重启。过程中,服务提供方与服务调用方相互无法感知。
服务提供方并不会事先告诉调用方他们需要操作哪些机器,从而让调用方去事先切走流量。而对调用方来说,也无法预测到服务提供方要对哪些机器重启上线,因此负载均衡就有可能把要正在重启的机器选出来,这样就会导致把请求发送到正在重启中的机器里面,从而导致调用方不能拿到正确的响应结果。
在服务重启的时候,对于调用方来说,可能会存在以下几种情况:
- 调用方发请求前,目标服务已经下线。对于调用方来说,跟目标节点的连接会断开,调用方可以立马感知到,并且在其健康列表里面会把这个节点挪掉,自然也就不会被负载均衡选中。
- 调用方发请求的时候,目标服务正在关闭,但调用方并不知道它正在关闭,而且两者之间的连接也没断开,所以这个节点还会存在健康列表里面,因此该节点就有一定概率会被负载均衡选中,出现预期外的异常。
关闭流程
在重启服务机器前,先通过“某种方式”把要下线的机器从调用方维护的“健康列表”里面删除。
当服务提供方关闭前,可以先通知注册中心进行下线,然后通过注册中心告诉调用方进行节点摘除
。
上述流程存在一定问题。
第一:整个关闭过程中依赖了两次RPC调用,一次是服务提供方通知注册中心下线操作,一次是注册中心通知服务调用方下线节点操作。
第二:注册中心通知服务调用方都是异步的,服务发现只保证最终一致性,并不保证实时性,注册中心在收到服务提供方下线的时候,并不能成功保证把要下线的节点推送到所有的调用方。
服务提供方主动通知调用方要下线的机器
,RPC里面调用方跟服务提供方之间是长连接,可以在提供方应用内存里面维护一份调用方连接集合,当服务要关闭的时候,挨个去通知调用方去下线这台机器,由两次RPC变成一次RPC,成功率也会变高。
任存在问题。
第一:在提供方通知调用方前一瞬间,调用方发送请求,此次请求仍会失败。
第二:提供方处理请求时,此时服务关闭,请求也会失败。
优雅关闭
被动调用 + 主动通知
可以在关闭的时候,设置一个请求“挡板”
,挡板的作用:告诉调用方,服务已经进入关闭流程了,不能再处理请求了。
当服务提供方正在关闭,如果这之后还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方(比如ShutdownException)。然后调用方收到这个异常响应后,RPC框架把这个节点从健康列表挪出,并把请求自动安全地重试到其他节点,可以实现对业务无损。【被动调用
】
服务关闭,服务提供方主动通知调用方要下线的机器。【主动通知
】可以保证实时性,也可以避免通知失败的情况。
捕获关闭事件
通过捕获操作系统的进程信号来获取,在Java语言里面,对应的是Runtime.addShutdownHook
方法,可以注册关闭的钩子。
在RPC启动的时候,提前注册关闭钩子,并在里面添加了两个处理程序,一个负责开启关闭标识,一个负责安全关闭服务对象,服务对象在关闭的时候会通知调用方下线节点。同时需要在调用链里面加上挡板处理器
,当新的请求来的时候,会判断关闭标识,如果正在关闭,则抛出特定异常。
进程结束过快会造成这些请求还没有来得及应答,同时调用方会也会抛出异常。
在服务对象加上引用计数器,每开始处理请求之前加一,完成请求处理减一,通过该计数器快速判断是否有正在处理的请求
。
服务对象在关闭过程中,会拒绝新的请求,同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭。
考虑到有些业务请求可能处理时间长,为了避免一直等待造成应用无法正常退出,可以在整个ShutdownHook里面,加上超时时间控制
,当超过了指定时间没有结束,则强制退出应用。超时时间建议设定成10s,基本可以确保请求都处理完了。
“优雅关闭”流程如下:
从外层到里层逐层进行关闭,先保证不接收新请求,然后再处理关闭前收到的请求
。