如何实现优雅关闭
大家可能存有疑问,RPC 里面有服务注册与发现功能, 注册中心的作用就是用来管理服务的状态, 当服务关闭时, 会先通知注册中心进行下线, 然后通过注册中心移除节点信息,这样不就可以保障服务不被调用吗?
那我们来看下关闭的流程:
整个关闭过程中依赖了两次 RPC 调用,一次是服务提供方通知注册中心下线操作,一次是注册中心通知服务调用方下线节点操作。并且注册中心通知服务调用方都是异步的,并不能保证完全实时性,通过服务发现并不能做到应用的无损关闭。
有没有好的解决方案呢?
服务提供方已经进入关闭流程,那么很多对象已经被销毁了,这个时候我们可以设置一个请求“挡板”,挡板的作用就是告诉调用方,服务提供方已经开始进入关闭流程了,不能再处理其他请求了。
这就好比我们去超市结账,在交接班或者下班的时候, 收银员会放一个提示牌在柜台, 提示“该通道已关闭”,不能进行结账, 这个时候客户只能转移到其他可用的柜台上进行结账。
处理流程:
当服务提供方正在关闭,如果还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方。这个异常就是告诉调用方“我正在关闭,不能处理这个请求”,然后调用方收到这个异常响应后,RPC 框架把这个节点从健康列表挪出,并把其他请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全地重试到其他节点,这样就可以实现对业务几乎无损的处理。如果要更为完善, 我们还可以加上主动通知机制,这样既可以保证实时性,也可以避免客户端出现重试情况。
如何捕获关闭事件呢?
操作系统的进程的关闭,如果不是强制结束,进程会接收到一个结束信号,Java应用程序,在接收到结束信号时, 会调用Runtime.addShutdownHook 方法触发关闭钩子。 我们在 RPC服务启动的时候,提前注册关闭钩子,在里面添加处理程序,先开启挡板, 然后通知调用方服务已下线。当接收到新来的请求时,挡板会进行拦截,抛出特定异常。为了尽可能地完成正在处理的请求, 我们可以加入计数器机制,把剩余请求纳入计数器当中, 每处理完一个请求, 就减少一个计数, 将所有剩余请求处理完成之后, 再真正结束服务。
在Dubbo框架中, 在以下场景中会触发优雅关闭:
JVM主动关闭(System.exit(int);
JVM由于资源问题退出(OOM);
应用程序接受到进程正常结束信号:SIGTERM或SIGINT信号。
优雅停机是默认开启的,停机等待时间为10秒。可以通过配置dubbo.service.shutdown.wait来修改等待时间。
基于ShutdownHook方式的优雅停机无法确保所有关闭流程一定执行完成,所以 Dubbo 推出了多段关闭的方式来保证服务完全无损。在关闭应用前,首先通过 QOS(在线运维命令) 的offline指令下线所有服务,然后等待一定时间确保已经到达请求全部处理完毕,由于服务已经在注册中心下线,当前应用不会有新的请求。这时再执行真正的关闭(SIGTERM 或SIGINT)流程,就能保证服务无损。
Dubbo优雅关闭的源码:
DubboShutdownHook.register方法
注册关闭钩子: /**
* 注册关闭钩子,在服务关闭时触发执行
*/
public void register() {
if (!registered.get() && registered.compareAndSet(false, true)) {
Runtime.getRuntime().addShutdownHook(getDubboShutdownHook());
}
}
DubboShutdownHook.doDestroy方法
销毁所有相关资源: /**
* 关闭注销所有资源, 包括注册器和协议处理器。
*/
public void doDestroy() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
// 销毁所有注册器,包括Zookeeper、etcd、Consul等等。
AbstractRegistryFactory.destroyAll();
// 销毁所有协议处理器,包括Dubbo、Hessian、Http、Jsong等。
destroyProtocols();
}