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

本文主要是介绍[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 RocketMQ吞掉异常问题排查,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 背景说明
    • 蒙蔽双眼
    • 口说无凭
      • 修补引发的新问题
      • 解决配置问题
    • 本地监控佐证
    • 万法归元

背景说明

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

蒙蔽双眼

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

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 = 30spring.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

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


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 = 300spring.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.HikariDataSourcemysql-payment.username = root
mysql-payment.password = xxx
mysql-payment.url = jdbc:mysql://xxx:3306/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaimysql-cashier.username = xxx
mysql-cashier.password = xx
mysql-cashier.url = jdbc:mysql://xxx:3306/cashier?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaispring.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. 服务消息积压根本没有任何错误
    这期间一直怀疑是新升级代码过多创建线程,但是几经确认是规范的创建线程池,自信注释掉所有可能过多创建线程地方,发布后继续消息积压,几经尝试无果

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

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依赖】

  <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数值需要各自工业实践结果:

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.HikariDataSourcemysql-payment.username = root
mysql-payment.password = xxx
mysql-payment.url = jdbc:mysql://xxx:3306/payment?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaimysql-cashier.username = xxx
mysql-cashier.password = xx
mysql-cashier.url = jdbc:mysql://xxx:3306/cashier?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaispring.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 = truespring.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 = truespring.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去处理我们的业务异常,一定要手动捕获处理,否则很多未知的问题很难定位发现

持续分享,持续输出…

这篇关于[前车之鉴] SpringBoot原生使用Hikari数据连接池升级到动态多数据源的深坑解决方案 RocketMQ吞掉异常问题排查的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/588919

相关文章

Python获取中国节假日数据记录入JSON文件

《Python获取中国节假日数据记录入JSON文件》项目系统内置的日历应用为了提升用户体验,特别设置了在调休日期显示“休”的UI图标功能,那么问题是这些调休数据从哪里来呢?我尝试一种更为智能的方法:P... 目录节假日数据获取存入jsON文件节假日数据读取封装完整代码项目系统内置的日历应用为了提升用户体验,

vue使用docxtemplater导出word

《vue使用docxtemplater导出word》docxtemplater是一种邮件合并工具,以编程方式使用并处理条件、循环,并且可以扩展以插入任何内容,下面我们来看看如何使用docxtempl... 目录docxtemplatervue使用docxtemplater导出word安装常用语法 封装导出方

将Mybatis升级为Mybatis-Plus的详细过程

《将Mybatis升级为Mybatis-Plus的详细过程》本文详细介绍了在若依管理系统(v3.8.8)中将MyBatis升级为MyBatis-Plus的过程,旨在提升开发效率,通过本文,开发者可实现... 目录说明流程增加依赖修改配置文件注释掉MyBATisConfig里面的Bean代码生成使用IDEA生

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

Springboot @Autowired和@Resource的区别解析

《Springboot@Autowired和@Resource的区别解析》@Resource是JDK提供的注解,只是Spring在实现上提供了这个注解的功能支持,本文给大家介绍Springboot@... 目录【一】定义【1】@Autowired【2】@Resource【二】区别【1】包含的属性不同【2】@

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co