[前车之鉴] 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

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

部署Vue项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤