[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 & RocketMQ吞掉异常问题排查

文章目录

背景说明

当前业务场景我们使用原生SpringBoot整合Hikari数据源连接池提供服务,但是近期业务迭代需要使用动态多数据源,很自然想到dynamic-source,结果一系列惨案离奇发生。。。

蒙蔽双眼

原生SpringBoot整合HikariCp数据源连接池配置【这个是没问题的配置】

java 复制代码
spring.datasource.hikari.allow-pool-suspension = true
spring.datasource.hikari.connection-timeout = 10000
spring.datasource.hikari.pool-name = HikariPool
spring.datasource.hikari.idle-timeout = 60000
spring.datasource.hikari.maximum-pool-size = 300
spring.datasource.hikari.max-lifetime = 120000
spring.datasource.hikari.minimum-idle = 30

spring.datasource.type = com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://a.com:4000/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username = xx
spring.datasource.password = sx

而升级后的动态多数据源配置如下:【有严重问题】

java 复制代码
spring.datasource.dynamic.primary = tidb-payment
spring.datasource.dynamic.strict = false
spring.datasource.dynamic.hikari.idle-timeout = 60000
spring.datasource.dynamic.hikari.max-lifetime = 120000
spring.datasource.dynamic.hikari.connection-timeout = 10000
spring.datasource.dynamic.hikari.minimum-idle = 30
spring.datasource.dynamic.hikari.maximum-pool-size = 300



spring.datasource.url = jdbc:mysql://a.com:4000/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username = xxx
spring.datasource.password = xxx
spring.datasource.type = com.zaxxer.hikari.HikariDataSource

mysql-payment.username = root
mysql-payment.password = xxx
mysql-payment.url = jdbc:mysql://xxx:3306/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai

mysql-cashier.username = xxx
mysql-cashier.password = xx
mysql-cashier.url = jdbc:mysql://xxx:3306/cashier?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai

spring.datasource.dynamic.primary = tidb-payment
spring.datasource.dynamic.datasource.tidb-payment.url = ${spring.datasource.url}
spring.datasource.dynamic.datasource.tidb-payment.username = ${spring.datasource.username}
spring.datasource.dynamic.datasource.tidb-payment.password = ${spring.datasource.password}
spring.datasource.dynamic.datasource.tidb-payment.type = ${spring.datasource.type}

spring.datasource.dynamic.datasource.mysql-payment.url = ${mysql-payment.url}
spring.datasource.dynamic.datasource.mysql-payment.username = ${mysql-payment.username}
spring.datasource.dynamic.datasource.mysql-payment.password = ${mysql-payment.password}
spring.datasource.dynamic.datasource.mysql-payment.type = ${spring.datasource.type}

spring.datasource.dynamic.datasource.mysql-cashier.url = ${mysql-cashier.url}
spring.datasource.dynamic.datasource.mysql-cashier.username = ${mysql-cashier.username}
spring.datasource.dynamic.datasource.mysql-cashier.password = ${mysql-cashier.password}
spring.datasource.dynamic.datasource.mysql-cashier.type = ${spring.datasource.type}

来,无论几年经验的道友看看此配置有什么问题?刚使用的童鞋很难发现,因为没有一定的并发量, 几乎很难发现其中 很致命的2个问题

  1. 全局配置是各自独享,不是共享
  2. 当前配置的最大活跃连接数和最小活跃连接数实际运行都是10,即配置是错误的

实话说,我也是遇到我人生第一个职业滑铁卢:

  1. 只要服务一发版,消息服务一直处于积压状态,而这个服务业务逻辑又很单一就是消费数据写TIDB,加上匮乏的测试人员,非生产环境根本看不出任何问题
  2. 只要一回滚就正常
  3. 服务消息积压根本没有任何错误
    这期间一直怀疑是新升级代码过多创建线程,但是几经确认是规范的创建线程池,自信注释掉所有可能过多创建线程地方,发布后继续消息积压,几经尝试无果

最搞笑的是,在期间做的修补策略还因为看不到异常,而引入一个新的问题:

java 复制代码
WARN com.baomidou.dynamic.datasource.DynamicRoutingDataSource [240] - dynamic-datasource initial loaded [0] datasource,Please add your primary datasource or check your configuration

当你第一次看到这个警告切记不要忽略,因为此时服务虽然只是启动告警,但是只要一尝试sql连接,直接异常:Caused by: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource

本来我不需要单独讲,因为自测是基本的素养,但是因为在当时上线修补过程中是缺少测试【过于自信】,所以任务服务发版没问题忽略,而异常还是我后来从rocketmq_client.log找到,还不是自身配置logback-spring.xml对应日志文件,所以一直没在意,关键RocketMQ还吃掉了异常,直接当回滚处理.


口说无凭

修补引发的新问题

首先对着回滚前最后一次修补代码分支先直接在本地压测,瞬间发现baomidou.dynamic.datasource.exception.CannotFindDataSourceException:ception

但问题来了,线上为什么没有这个异常,搜遍了日志无果,后来想到当前直接监听RocketMQ消费,统一在consumeMessage 方法处理,如下

坑啊,当时没发现是因为程序没有任何错误还傻傻以为是程序处理正常,只是线程积压了

话说回来,这个错误算比较低级了,因为引入了dynamic-datasource 数据源但是却没有配置好数据源,而默认引入依赖就会在业务的sql操作中使用改配置数据源连接池【当时回滚代码逻辑是不清晰的,只回滚配置注释代码是不够的,要么基于老分支直接重写逻辑本地验证后再试,要么所有新代码一起移除,包括mave依赖】

java 复制代码
  <dependency>
          <groupId>com.baomidou</groupId>
           <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
           <version>3.4.0</version>
 </dependency>

这里可以推理得到:既然这个错误是RocketMQ捕获了,那么自然打在了RocketMQ配置的日志文件中:rockemq_client.log 注意这个不配置就自动生成,关键还只保留8小时,最终本地验证是找到了,生产因为过了一天所以看不到

解决配置问题

现在我们回来看看配置两个问题是怎么回事,这个比较隐晦了,我加好了数据源后拷贝生产一份配置到本地,开始debug定位发现,配置最大活跃连接、最小活跃连接数首先是-1 然后在校验合法性时改成了默认值10

what?没生效本能想到这不可能,因为生产一直这么使用的,甚至怀疑生产一直是错误的,但是生产让SRE查询监控信息确认是正确的,瞬间再次怀疑自己,索性仔细比对生产老配置发现和源代码排查

才知道maximum-pool-size minimum-idle在升级使用dynamic-source是不对的,属性名发生了变更分别变成了max-pool-size 和 min-idle , 本以为原路拷贝即可谁知在dynamic-datasource源码中配置HikariCp做了替换,真的坑爹

这里就可以解释,线上是并发比较高的,所以很快把10个连接占满,甚至已经抛出了连接不可用的异常由于被RocketMQ捕获,所以很难发现,于是修正了属性值再次Debug正常设置成功。

修正了属性值还不够,接下来有第二个问题,请回到开头再次观察连接池配置是全局配置,最初也是没有好好看源码以为是三个数据源共享配置,直到我在调试过程中看到源码确实是独自设置,我才恍然

是否允许全局独享取决你的业务场景,如果你的数据库的所在数据源都是独立部署的那么 共享除了失去定制的灵活性没啥性能问题,但是如果你的本质是一个数据源多个数据库 这么配置会撑爆数据库连接,使用时需要谨慎!

有人要问了谁叫你不看文档,这里要diss 一下 dynamic-source官方文档说明这一块是真的黑心

所以经过上面分析最正确的配置模版如下,注意我只保证属性一定设置生效,但是value数值需要各自工业实践结果:

java 复制代码
spring.datasource.url = jdbc:mysql://a.com:4000/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username = xxx
spring.datasource.password = xxx
spring.datasource.type = com.zaxxer.hikari.HikariDataSource

mysql-payment.username = root
mysql-payment.password = xxx
mysql-payment.url = jdbc:mysql://xxx:3306/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai

mysql-cashier.username = xxx
mysql-cashier.password = xx
mysql-cashier.url = jdbc:mysql://xxx:3306/cashier?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai


spring.datasource.dynamic.primary = tidb-payment
spring.datasource.dynamic.datasource.tidb-payment.url = ${spring.datasource.url}
spring.datasource.dynamic.datasource.tidb-payment.username = ${spring.datasource.username}
spring.datasource.dynamic.datasource.tidb-payment.password = ${spring.datasource.password}
spring.datasource.dynamic.datasource.tidb-payment.type = ${spring.datasource.type}
spring.datasource.dynamic.datasource.tidb-payment.hikari.max-pool-size = 50
spring.datasource.dynamic.datasource.tidb-payment.hikari.min-idle = 4
spring.datasource.dynamic.datasource.tidb-payment.hikari.max-lifetime = 120000
spring.datasource.dynamic.datasource.tidb-payment.hikari.connection-timeout = 10000
spring.datasource.dynamic.datasource.tidb-payment.hikari.idle-timeout = 60000
spring.datasource.dynamic.datasource.tidb-payment.hikari.allow-pool-suspension = true

spring.datasource.dynamic.datasource.mysql-payment.url = ${mysql-payment.url}
spring.datasource.dynamic.datasource.mysql-payment.username = ${mysql-payment.username}
spring.datasource.dynamic.datasource.mysql-payment.password = ${mysql-payment.password}
spring.datasource.dynamic.datasource.mysql-payment.type = ${spring.datasource.type}
spring.datasource.dynamic.datasource.mysql-payment.hikari.max-pool-size = 25
spring.datasource.dynamic.datasource.mysql-payment.hikari.min-idle = 4
spring.datasource.dynamic.datasource.mysql-payment.hikari.max-lifetime = 120000
spring.datasource.dynamic.datasource.mysql-payment.hikari.connection-timeout = 10000
spring.datasource.dynamic.datasource.mysql-payment.hikari.idle-timeout = 60000
spring.datasource.dynamic.datasource.mysql-payment.hikari.allow-pool-suspension = true

spring.datasource.dynamic.datasource.mysql-cashier.url = ${mysql-cashier.url}
spring.datasource.dynamic.datasource.mysql-cashier.username = ${mysql-cashier.username}
spring.datasource.dynamic.datasource.mysql-cashier.password = ${mysql-cashier.password}
spring.datasource.dynamic.datasource.mysql-cashier.type = ${spring.datasource.type}
spring.datasource.dynamic.datasource.mysql-cashier.hikari.max-pool-size = 25
spring.datasource.dynamic.datasource.mysql-cashier.hikari.min-idle = 3
spring.datasource.dynamic.datasource.mysql-cashier.hikari.max-lifetime = 120000
spring.datasource.dynamic.datasource.mysql-cashier.hikari.connection-timeout = 10000
spring.datasource.dynamic.datasource.mysql-cashier.hikari.idle-timeout = 60000
spring.datasource.dynamic.datasource.mysql-cashier.hikari.allow-pool-suspension = true

本地监控佐证

至此问题排查和解决已经确定,但是这么debug修改我还是不太放心,比较之前自信修改的教训让我历历在目,有了解到SpringBoot自带监控肯定有关于数据源连接池的信息,如果能看到自己期望的结果,那么一定不会有问题了

所以这里参考网上如何打开本地健康检查【不推荐生产环境使用】:Springboot整合Prometheus本地监控多数据源 ,这一篇不仅给出了方案,还发现了SpringBoot监控多数据源的bug,即只监控到一个问题:配置之后把之前的流程走一遍确实走到了默认值10

不用不知道,我又陷入另一个自我怀疑阶段:在本地和测试环境启动参数、apollo配置、代码完全一致的情况,都使用错误的数据连接池配置后, 测试和本地展现两种不同的数据源监控结果:云服务器是-1,而本地一直都是10,详情分析请看这一篇:【沉淀之华】SpringBoot使用HikariCP数据源两次初始化过程 & 服务器与本地完全一致却不同数据源结果定位


万法归元

从上面坎坷的排查过程看,需要注意3点

  1. 平时迭代一定要尽可能做好自测,甚至是压测
  2. 不要定式思维,按技术文档或者源码配置【无奈官方文档都成了资本手下,只恨无开源精神】
  3. 不要让RocketMQ去处理我们的业务异常,一定要手动捕获处理,否则很多未知的问题很难定位发现

持续分享,持续输出...

相关推荐
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
AskHarries6 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion7 小时前
Springboot的创建方式
java·spring boot·后端
Yvemil78 小时前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
星河梦瑾9 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
计算机学长felix10 小时前
基于SpringBoot的“交流互动系统”的设计与实现(源码+数据库+文档+PPT)
spring boot·毕业设计
.生产的驴10 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲10 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
撒呼呼11 小时前
# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)
数据库·spring boot·spring·mvc·springboot
因我你好久不见11 小时前
springboot java ffmpeg 视频压缩、提取视频帧图片、获取视频分辨率
java·spring boot·ffmpeg