type
status
date
slug
summary
tags
category
icon
password
AI summary

优雅停机之Dubbo

前言

本篇文章主要分析dubbo-2.6.5的停机流程,看看国内的微服务框架天花板是如何优雅停机的。
原本打算一篇文章讲完Dubbo、RocketMQ以及spring-cloud-gateway的优雅停机,奈何Dubbo的优雅停机实现路径较为坎坷,并且多个版本来回勾连。为了能把我研究的过程尽数分享给各位看官,篇幅可能较长。
从源码看 ,dubbo-2.6.5注册了单独的应用级ShutdownHook——DubboShutdownHook。如果应用运行在Spring容器中且注册了SpringShutdownHook,那么在停机过程中两个ShutdownHook会并行执行。但是从我们长期使用的经验来看,停机时并没有明显的异常信息。是真的没问题还是我们运气好?今天我们通过源码加日志来验证一下

DubboShutdownHook

DubboShutdownHook 代码如下,整体结构比较简单
我不打算追踪源码来分析流程,阅读代码的枯燥过程交给我就行了。这里我直接给出总结:
  1. 处理registtry(注册中心)相关停机逻辑
    1. 移除注册中心相关的节点(这里是3个consumer节点和1个provider节点)
      1. 摘除自身provider信息
      2. 摘除自身consumer信息(作为consumer启动时会在对应provider节点下的consumers节点里注册)
    2. 停止监听
      1. 作为provider,应用会监听自身提供的provider对应的providers节点
      2. 作为consumer,应用会监听依赖provider对应的providers、configurators、routers节点
    3. 断开和注册中心的连接
  1. 处理protocol(协议)相关停机逻辑(主要是client和server之间的断连)
    1. 作为server,给所有client发送read_only数据包,告知client我们要停机了,并且等待所有client主动和自己断开连接,最后关闭server,不再监听对应服务端口。等待时间有个最大值(默认10s),超过之后就直接关闭server。
    2. 作为client,要主动断开和依赖provider对应server之间的连接
下面我们再通过日志来熟悉和加深一下印象

停机日志分析

下面是一个上下游依赖比较简单的应用的停机日志。为了防止存在偶然性,我们人为的在DubboShutdownHook里增加一个断点,让SpringShutdownHook先走完,再执行DubboShutdownHook,观察是否会有异常。如果你觉得日志过于冗长,也可以跳过直接看总结。

名词解释

这里可能会涉及到4个名词,提前解释一下
  • server:dubbo服务provider所在实例,一般监听20880端口,tcp连接的server端
  • client:dubbo服务consumer所在实例,tcp连接的client端
  • provider:dubbo服务的provider,一个服务可以有多个provider
  • consumer:dubbo服务的consumer,一个服务可以有多个consumer
如果你逐行看懂了上面的日志,那么相信你对DubbShutdownHook的逻辑应该已经非常清晰了。

共享连接

默认情况下,consumer所在的实例和provider所在的实例是共享连接的,无论有多少个provider和consumer。所以在客户端准备关闭连接的时候,会存在一个计数器的概念,只有一个共享连接上的所有consumer→provider的关系都释放了,那才能关闭连接。

在途请求的双等待

在步骤1.1中,当我们从注册中心摘除自身provider信息的时候,有依赖此provider的consumer就会监听到provider下线的消息,从而主动关闭和provider之间的连接。不过这里存在一种特殊情况:就是此时consumer和对应provider之间还有已发起但未完成的请求。在这种场景下,consumer会等待请求完成后再断开连接(最大等待时间默认10s)。但是在这个等待的时间段里我们不希望consumer还能发起新的请求,所以才有了后面server给此类client发送read_only包:虽然连接没有关闭,但是client只能读数据而不能发起新请求了。当然这里也有可能是注册中心推送延迟或者网络原因导致的client未及时和server断开连接,这些场景这里就不展开细说了。
而server也会等待所有client都关闭连接后再关闭(最大等待时间默认10s)

dubbo-2.6.5版本优雅停机存在的问题

乍一看上面的日志,没有任何报错,并且已经是在我们人为的让SpringShutdownHook先执行的前提下。虽然没报错,但是在Spring容器关闭以后,服务还没从注册中心下线这段时间,如果有请求进来真的没有问题吗?
答案肯定是否定的,如果说请求里用到了已经被destory的Bean,并且对该Bean的方法调用里有做destory与否的判断逻辑,那么肯定是会报错的。比如
【dubbo优雅关机】关于spring管理的应用重复监听处理dubbo关闭。
Updated May 16, 2021
提到的Spring容器关闭导致了HikariDataSource的关闭,后续进来的请求需要查询数据库的地方就报错了。由此看来,SpringShutdownHookDubboShutdownHook并行执行肯定是存在问题的。
既然存在这样的可能性,那为什么我们一次都没碰到过?我又回溯了一下之前的日志,发现其实有碰到过,只是日志级别是WARN,并且数量不多,可能就没太在意。下面截取了我们在使用自研的基于Redis的分布式锁在停机过程中的异常日志:

数据库连接池关闭之谜

但是怎么从来没碰到过数据库相关的异常呢?我们99%的接口里面至少都会包含一次数据库交互啊,这个理论上应该是最容易出现问题的地方。
我尝试在SpringShutdownHook执行完毕后,调用一个会查询数据库的方法,发现竟然可以成功返回。并且在SpringShutdownHook执行过程中,数据库连接池和数据库连接也并没有被关闭(我们的框架会打印连接关闭的日志)。
我们自研的框架没有使用SpringBoot自带的DataSourceAutoConfiguration,而是自定义了一套自动配置并且强制指定数据库连接池为tomcat-jdbc
于是我先尝试还原成SpringBoot官方的DataSourceAutoConfiguration。我们使用的2.3.4版本默认第一优先级是HikariDataSource。此时重复前面的步骤,发现在Spring容器关闭的时候有打印出连接池关闭的日志,并且在后续查数据库的时候会报错,也就是和前面的issues里一样:
难道是tomcat-jdbc在停机的时候不会关闭连接池销毁连接吗?打开org.apache.tomcat.jdbc.pool.DataSource的源码,我们看到有定义close()方法。通过优雅停机——基础篇我们知道对于具有close()方法的单例Bean,Spring容器在停机过程中会调用其close()方法做destory。那么这里为什么没有调用呢?
原来我们之前在实现动态数据源的时候,注入容器的并不是org.apache.tomcat.jdbc.pool.DataSource类型的Bean,而是我们包装过的DynamicDataSource,而DynamicDataSource并没有close()方法。
💡
动态数据源就是通过配置中心的改动可以实时的调整运行时数据源的功能。是结合我们自研的配置中心回调做的。动态替换过程就是使用新参数创建一个新的数据源,并关闭老数据源。
DynamicDataSource 的方法都委托给了内部真正的DataSource
我们修改DynamicDataSource类实现AutoCloseable接口,并实现了close()方法。终于在Spring容器关闭的时候打印出了连接关闭的日志。
不过此时我们再次调用了查询数据库的接口,发现仍然没有报错。因为tomcat-jdbc连接池关闭的时候并没有设置一个已经关闭的标志位,而是直接把pool设置为null了,下一次请求进来则会重新初始化连接池,并创建初始连接,而不会报错:

dubbo优雅停机的发展之路

既然dubbo-2.6.5的停机流程存在问题,那么我们看看在后续版本有没有做优化。在这个过程中,我翻看了dubbo-2.6.5版本前后关于优雅停机的所有issues。并按照时间顺序,整理出了下面的优化路线。

issue#1665 pull#1763(May 17, 2018)

  1. 去掉了之前的Application级的DubboShutdownHook
  1. 新增DubboApplicationListener监听Spring容器关闭事件来做优雅停机
  1. 增加DubboWebApplicationInitializer来注册ContextLoaderListener:主要逻辑是为了注册DubboApplicationListener
作者期望做到无感注册DubboApplicationListener监听器来监听ContextClosedEvent事件,依托于SpringShutdownHook来完成优雅停机。另外,作者还调整了一下代码结构
1. Introduce a Bootstrap class to easily start/stop Dubbo when running without Spring. 2. Introduce DubboApplicationListener to listen to the Spring lifecycle event to start/stop Dubbo under Spring.
但是,这个优化存在不少问题
  1. 去掉了之前的应用级的DubboShutdownHook,会导致一些不依赖Spring运行的dubbo应用无法优雅停机
  1. 由于DubboWebApplicationInitializer注册了一个ContextLoaderListener,这种做法对于本身已经配置了ContextLoaderListener的用户是不兼容的。
  1. 更糟糕的是,SpringBoot应用部署到外部容器的启动流程虽然没有用到ContextLoaderListener,但是由于SpringBoot启动过程中会创建ROOT_WEB_APPLICATION_CONTEXT,所以也是不兼容的。
  1. 更扎心的是,对于SpringBoot内嵌容器启动的应用,虽然不会报错,但是SpringBoot内嵌容器启动是不会加载DubboWebApplicationInitializer类的。所以也不会注册DubboApplicationListener

pull#1820(May 31, 2018)

人们很快发现了问题1。于是有了这个pr:
  1. 修复了pull#1763产生的问题1:重新注册DubboShutdownHook。并且注释了这仅仅只是为了兼容性。这是什么意思呢?看看第二点改动你就明白了
    1. notion image
  1. 如果是通过Spring容器启动,并且注册了SpringShutdownHook的场景,那么就会移除前面注册的DubboShutdownHook这就是所谓的兼容性:防止在Spring容器中,SpringShutdownHookDubboShutdownHook并行执行可能会带来的问题。(这里似乎已经在解决我们碰到的问题了)
    1. notion image
  1. 如果是通过SpringContainer启动,也走这一套(移除DubboShutdownHook
    1. notion image
不过遗憾的是,由于后三个问题都没有得到解决,所以这个版本在SpringBoot下依然没法正常工作。

issue#1998 pull#2126(Jul 26, 2018)

pull#2126主要就是为了解决pull#1763带来的问题2
作者去掉了DubboWebApplicationInitializer,增加了web-fragment.xml文件。web-fragment.xml依旧定义了一个ContextLoaderListener,不过我个人感觉并没有解决问题。
所以后续还有人提出了类似问题
spring boot with dubbo 2.6.3, cause ‘ Cannot initialize context because there is already a root application context present - check whether you have multiple ContextLoader* definitions in your web.xml’
Updated Mar 5, 2019
使用JavaConfig搭建框架时报错。 Throw Error:BeanDefinitionStoreException when build by JavaConfig
Updated Oct 8, 2018

pull#2725(Nov 5, 2018)

终于有人碰到了问题3问题4。这个pr的作者直接把前面3个pr的大部分改动还原了。这样,相关issues的问题直接迎刃而解了。
这个改动被合并到了2.6.6版本,但是不知道为什么在更新说明里没有写出来。
notion image
不过这个pr也没解决ShutdownHook并行执行的问题。所以后面也有小伙伴提出了相关issues
Summary of several issues of graceful shutdown
Updated Nov 5, 2018
Problems of graceful shutdown in 2.6.3 and some recommendations
Updated Nov 5, 2018

issue#2901(Dec 7, 2018)

终于要来解决两个ShutdownHook并行的问题了。不过此版本的优化还是没考虑全面:因为使用Spring容器,并不能保证一定注册了SpringShutdownHook。而这里并没有考虑未注册的场景,一股脑的在Spring场景下,把DubboShutdownHook移除了。所以有了后面#3008的优化。
这个改动2.6.x上不存在,只合并到了2.7.x

issue#3008(Dec 18, 2018)

该版本保证了只要在Spring环境下,那么会主动注册SpringShutdownHook,不依赖用户手动注册。这样就可以愉快的移除DubboShutdownHook了。
至此,已经解决我们说的并行ShutdownHook问题了。
不过当时有2.6.x2.7.x两个版本在并行迭代,关于并行ShutdownHook的问题在2.7.5版本又被改错了,直到2.7.12版本才被修复,具体可见
【dubbo优雅关机】关于spring管理的应用重复监听处理dubbo关闭。
Updated May 16, 2021
另外,关于ShutdownHook还有一个值得注意的地方,可能存在内存泄漏,具体可见
Fix DubboShutdownHook Memory Leak
Updated Dec 10, 2018

总结

Dubbo的优雅停机之路真的是不平坦,反反复复,并且很多版本的优化和改动都存在一些明显的问题,但是最终还是愉快的合并了。这要造成了不少我在回溯过程中的困扰。不过好在我觉得当下我应该是理清楚了。
Dubbo的优雅停机确实是有不少场景需要考虑的,比如非Spring、Spring without SpringShutdownHook、SpringBoot内嵌容器、SpringBoot外部容器、Spring应用外部容器等等。在设计之初可能很难考虑全面。这也有点前端机型适配的意思,优先解决90%的主流问题也不失为一种好的方案。
文章的篇幅以及花费的时间真的是有点超出预期了~当然,也希望你看完之后会有收获。

参考

  1. 深入Spring Boot (十五):web.xml去哪了
  1. web-fragment使用
  1. Allow the embedded web server to be shut down gracefully
    Updated Mar 18, 2022
  1. howto.traditional-deployment
  1. Dubbo 优雅停机
  1. 一文聊透 Dubbo 优雅停机
Docker端口映射的实现机制优雅停机(上)
Loading...
黑微狗
黑微狗
一只普通的干饭汪🍚
Latest posts
RocketMQ 4.6.0 Message Trace 功能异常排查
2025-4-8
browser-use 项目核心原理
2025-3-28
关于怎么搭建一个这样的blog
2025-3-28
关于怎么给blog搞一个自定义的域名
2025-3-28
Excel导入需求升级——支持内嵌图片导入
2025-3-28
mysql流式查询中的一个坑
2025-3-28