性能文章>面试官:Dubbo服务重启下线,消费者继续调用,都异常了怎么办>

面试官:Dubbo服务重启下线,消费者继续调用,都异常了怎么办原创

436067

**本文首发于公众号【看点代码再上班】,建议关注公众号,及时阅读最新文章。**

一定要看的原文:https://mp.weixin.qq.com/s?__biz=MzIw……

 

大家好,我是tin,这是我的第20篇原创文章

 

我们有没有想过一个问题,在分布式系统中,服务节点重启的时候,消费者流量继续调用该节点,那么这部分调用全部异常,对于线上用户来说,系统故障?

 

针对这种情况,我们可以这么想:Dubbo服务重启下线的时候,消费者还会不会继续调用此服务节点?

 

答案当然是不会

 

今天就结合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关闭的钩子。

 

比如我们写一个关闭【看点代码再上班】书库的钩子,如下:

package com.tin.example.shutdown.hook;

/**
 * title: ShutdownHookTest
 * <p>
 * description:
 *
 * @author tin @公众号【看点代码再上班】 on 2022/4/5 下午12:27
 */
public class JVMShutdownHookTest {
    public static void main(String[] args) throws Exception {
        Runtime.getRuntime().addShutdownHook(new Thread(JVMShutdownHookTest::doSomething));

        while (true) {
            System.out.println("i am running...");
            Thread.sleep(500);
        }
    }

    /**
     * 停机前处理事项
     */
    private static void doSomething() {
        System.out.println("关闭【看点代码再上班】书库。");
    }
}

执行命令:

ericli@EricdeAir IntelliJIdea2020.1 % jps
5458 Jps
5394 KotlinCompileDaemon
2986 QuorumPeerMain
5451 Launcher
5452 JVMShutdownHookTest
3023 ZooKeeperMain
ericli@EricdeAir IntelliJIdea2020.1 % kill 5452

本地测试控制台结果输出如下:

 

从测试输出可以看出,当我执行kill pid(实际也是kill -15命令)命名时,我们的Java程序会先执行ShutDownHook钩子逻辑,最后才退出进程。

 

三、Spring容器的优雅下线

Spring的优雅下线也使用到了JVM的ShutdownHook。

 

首先,在application context被load时会注册一个ShutdownHook。这个ShutdownHook会在进程退出前执行销毁bean、容器的销毁等操作。

 

同时,Spring除了销毁bean等操作之外,还会发出一个ContextClosedEvent事件,很多基于Spring容器的三方框架都可以监听这个事件实现更多的优雅停机操作。

 

比如,我们可以监听Spring的事件:

@Override
    public void onApplicationContextEvent(ApplicationContextEvent event) {
        if (event instanceof ContextClosedEvent) {
            //自定一些容器关闭前业务要进行的操作
            onContextClosedEvent((ContextClosedEvent) event);
        }
    }

 

四、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,一个在努力让自己变得更优秀的普通工程师。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎加我提出,我一定细心推敲并加以修改。

 

看到这里请安排个“三连”(分享、点赞、在看)再走吧,坚持创作不容易,你的正反馈是我坚持输出的最强大动力,谢谢!

 

点赞收藏
分类:标签:
看点代码再上班
请先登录,查看6条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

浅析AbstractQueuedSynchronizer

浅析AbstractQueuedSynchronizer

7
6