面试官:Dubbo服务重启下线,消费者继续调用,都异常了怎么办原创
**本文首发于公众号【看点代码再上班】,建议关注公众号,及时阅读最新文章。**
一定要看的原文:
大家好,我是tin,这是我的第20篇原创文章
我们有没有想过一个问题,在分布式系统中,服务节点重启的时候,消费者流量继续调用该节点,那么这部分调用全部异常,对于线上用户来说,系统故障?
针对这种情况,我们可以这么想:Dubbo服务重启下线的时候,消费者还会不会继续调用此服务节点?
答案当然是不会。
今天就结合dubbo源码一起来捋一捋,先上一个目录:
-
-
二、JVM的优雅下线
-
三、Spring容器的优雅下线
-
四、Dubbo的优雅停机
-
五、结语
一、操作系统的优雅停机
优雅停机是指在停止应用时,执行一系列"操作"保证应用正常关闭。这些操作包括拒绝新请求和新连接、关闭服务注册、等待已有请求执行完成、关闭线程池、关闭连接、释放资源等。
优雅停机可以避免非正常关闭程序可能造成的任务被抛弃、数据丢失、应用异常等问题。优雅停机本质上是进程即将关闭前执行的一些额外流程。
操作系统本身也有对优雅停机的思考。
我们停止一个进程是,会使用kill -9和kill -15命令。但是,我们一般不建议使用kill -9。
这是因为kill -9命令很强硬,它会向操作系统内核发出SIGKILL信号,该信号直接停止进程,不能被阻塞或者忽略。
kill -9显然不那么优雅,如果在发出SIGKILL信号的时候,进程还有未处理完成的任务,这时候任务就会被丢弃或者终止导致数据的不可预知错误。
使用kill -15就不一样了,这时候发给系统内核的是一个SIGTERM信号。
当进程接收到SIGTERM信号时,具体要如何处理是由进程自己决定,进程就可以拒绝外部新的任务并完成已有任务后再停止。
在Docker中,docker stop相当于kill -15,他会向容器内的进程发送SIGTERM信号,在10S之后(可通过参数指定)再发送SIGKILL信号。
而docker kill就像kill -9,直接发送SIGKILL信号。
二、JVM的优雅下线
我们Java应用运行时就是一个独立的进程,它的关闭也即是JVM的关闭。
JVM的关闭也分为正常关闭、强制关闭、异常关闭。
JVM的正常关闭是Java程序优雅停机的关键,正常关闭的过程中,JVM可以做一系列预善后工作,比如线程池、连接池任务的完成和资源的释放等。
JVM还预留了钩子机制,供我们开发自行处理一些特定操作。
这种钩子机制就是JDK中提供的shutdown hook,它有一个方法Java.Runtime.addShutdownHook(Thread hook),可以注册一个JVM关闭的钩子。
比如我们写一个关闭【看点代码再上班】书库的钩子,如下:
执行命令:
本地测试控制台结果输出如下:
从测试输出可以看出,当我执行kill pid(实际也是kill -15命令)命名时,我们的Java程序会先执行ShutDownHook钩子逻辑,最后才退出进程。
三、Spring容器的优雅下线
Spring的优雅下线也使用到了JVM的ShutdownHook。
首先,在application context被load时会注册一个ShutdownHook。这个ShutdownHook会在进程退出前执行销毁bean、容器的销毁等操作。
同时,Spring除了销毁bean等操作之外,还会发出一个ContextClosedEvent事件,很多基于Spring容器的三方框架都可以监听这个事件实现更多的优雅停机操作。
比如,我们可以监听Spring的事件:
四、Dubbo的优雅停机
Dubbo的优雅停机从起初的2.5版本到当前的2.7,经历了多次优化。
最终版本也同样采用到了Spring的ApplicationContextEventListener接口,监听Spring容器的close事件。
在org.apache.dubbo.config.spring.extension.SpringExtensionFactory类中,就有关于ShutDownHook注册的相关代码:
特别有意思的是,这里有一个issue,看②处代码,它是一个关闭注册的动作,①处则是一个注册的动作。
既要注册又要注销,这是为什么呢?
Dubbo的开发者们已经发现,Spring容器下的Dubbo进程会触发两次关机钩子。
一次在org.apache.dubbo.config.bootstrap.DubboBootstrap中:
另一次是在org.apache.dubbo.config.spring.context.DubboBootstrapApplicationListener中通过监听spring的关闭事件来关闭dubbo服务:
这会导致什么后果?
答:会导致高并**况下,一些任务线程获取不到资源而抛异常。
这个issue中也讲得很明白了,如下:
❝dubbo没有关闭完,数据库连接池已经关闭,导致应用并发高的时候很多没跑完的dubbo线程拿不到数据库连接抛出异常。
If there is an exception, please attach the exception trace:
msg:DUBBO服务异常 remoteHost:xxx.xx.xxx.xxx service:com.xxx.service.xxxService method:xxx message:nested exception is org.apache.ibatis.exceptions.PersistenceException:
Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLException: HikariDataSource HikariDataSource (HikariPool-1) has been closed.
❞
这也是Dubbo优化多次后做出的处理结果,以下文章描述得非常详细,值得一看:
https://www.cnkirito.moe/dubbo-gracefully-shutdown
我们再来看看Dubbo ShutdownHook都做了哪些事情。
org.apache.dubbo.config.DubboShutdownHook#doDestroy
1. 注册中心数据销毁:关闭注册中心中本节点对应的提供者地址以及订阅数据。
2. 协议流程数据销毁:销毁所有协议,包括所有已经暴露和引用的服务,释放协议所占用的所有资源,比如连接和端口。
Dubbo销毁注册中心的同时还需要注销Spring容器,这部分工作由Spring的ShutdownHook完成。
org.springframework.context.support.AbstractApplicationContext#doClose
发布close事件,就有利于我们应用层自己做一些特殊处理逻辑,比如Dubbo关闭注册中心。
在关闭Spring容器之前,Dubbo已经关闭注册中心上对应的服务注册,不再接收新请求进来,同时还会把本地未完成的任务做完,最后才销毁bean以及关闭进程。
结语
我是tin,一个在努力让自己变得更优秀的普通工程师。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎加我提出,我一定细心推敲并加以修改。
看到这里请安排个“三连”(分享、点赞、在看)再走吧,坚持创作不容易,你的正反馈是我坚持输出的最强大动力,谢谢!