背景
我们线上一个项目在发版本时,经常会有部分实例无法正常启动的情况,特别是在没有进行灰度发布的场景下,基本上很难正常的启动成功,异常信息如下:
问题分析
看日志可以很容易看出来,启动异常的原因是hikari的配置绑定异常,原因是hikari的连接池已经seal了:The configuration of the pool is sealed once started.
字面意思:连接池配置一旦启动就被密封。那么问题来了,这个状态是怎么设置的?我的服务明明还没有启动;就算设置了,理论上配置初始化应该只会初始经一次,为什么会报错?
sealed状态设置
我们先看一下hikari的源码,找一下连接池的sealed
状态是怎么设置的:
在HikariConfig
中查看设置sealed
字段的方法,查看它的调用链路,发现只有两个地方调用了此方法来设置sealed
状态为true
,一个是构造函数,也就是初始化数据源时:
另一个是获取连接时:
可以看到,在HikariConfig
构造函数中,会先初始化连接池,然后设置sealed
状态。在getConnection
时,会判断连接池是不是已经被初始化了,如果没有,则会初始化连接池,并设置sealed
状态。
异常来源
我们现在知道sealed
状态的设置时机,那么The configuration of the pool is sealed once started. 这个异常又是什么时候被抛出的呢,同样是在HikariConfig
中搜索对应异常,可以看到这个异常都是通过checkIfSealed
方法抛出,很明显,这个方法就是检查sealed
状态的,查看它的调用链路:
可以看到,在设置HikariConfig
的任意属性时,都会先检查状态。这么看来,sealed
其实就是为了在连接池初始化完成之后,不允许再动态的去更新配置(可以使用其它的方式),那么理论上来说,Spring容器还在启动过程中, 为什么在设置属性之前,sealed状态就已经被设置的呢?
异常原因
我们回过去再去看一下启动日志,通过上面的源码,我们可以看到在初始化连接池的前后,都会打印INFO
日志,那么我们可以搜索对应的日志查看连接池的初始化情况:
从日志中可以看到,我们前面两个数据源都正常初始化完成了(主从数据库),但是后面马上又报错了(Spring绑定属性),好像看不出什么来,但是如果我们关注一下日志打印对应的线程就可以发现,初始化连接池和报错的线程并不是同一个!前面的初始化日志,都是在dubbo线程中打印的,而后面报错的日志,是在main线程中。
这时候就很明显了,上面说到,sealed
状态不仅在构造函数中设置,在getConnection
时也会去初始化连接池并更新状态。这明显就是spring容器还在初始化中,但是dubbo服务已经提前暴露了,导致有请求进来开始请求DB了。
异常流程
- dubbo服务成功暴露
- dubbo请求进入,存在DB请求,调用
getConnection
初始化连接时,sealed = true
- spring容器创建数据源,设置属性时发现
sealed = true
,抛出异常 - 服务启动失败
回到最开始的现象,特别是在没有进行灰度发布的场景下,基本上很难正常的启动成功
。在灰度发布时,是服务启动后,才会转发部分流量到灰度容器,所以dubbo是否提前暴露并没有影响,因为在启动过程中并没有请求进入。但是在非灰度发布场景下,由于dubbo服务一暴露,马上就有请求进入,所以导致启动异常。
解决
知道问题产生的原因,就很容易解决问题了,解决方式很简单,让dubbo的服务在spring容器启动后暴露即可,从dubbo的文档可以看到,2.6.5之后不会再出现这种弱智问题了,但是我们的服务比较旧,用的2.6.3的版本,所以需要配置delay=-1,或者设置一个较长的delay时间。