整个公司没几个人会配置JAVA数据源
前言
说来可笑,公司的私有云平台为部署的应用指定了云框架,一方面想要部署在云上的应用符合云开发范式,但是另一方面在很多关键技术选型上又没有做严格的限制,特别是Java数据源的选型,有用HikariCP的,有用Druid的,还有使用TomcatJdbc的,不光是数据源的选型五花八门,每个团队的每个小组对数据源的配置都有自己的想法,那配置简直是 狗看了都摇头,甚至出现了将一种数据源的配置配给另外一种数据源的情况。
所以果不其然,在开年到现在的不到四个月里,十几起生产事件和数据源的配置有关,最终大领导坐不住了,骂了一句,整个公司没几个人会配置Java数据源吗。
说回这个数据源,也就是数据库连接池,我们现在常用的有HikariCP,号称速度最快,还有Druid,号称功能最丰富,以及TomcatJdbc,号称均衡之王。其实因为数据源导致生产事件,一方面研发 肯定是菜这个没得跑,另一方面就是数据源有些配置结合其底层机制是真的抽象,特别是Druid,源码写得最抽象,机制最繁杂,部分版本存在恶性BUG,毫无疑问的,因为Druid数据源导致的生产事件也是最多的。
既然抛出了问题,那么本文就想要尝试解决问题,不然大领导就要 尝试把框架组的我们给解决了。
本文不会分析源码,而是给出上述三种数据源的最简实践配置,并且以图文形式来讲解这个配置为什么这么配,以及这个配置会在底层怎么影响数据源的工作。
探究源码不易,如果本文能够帮助到你,希望点赞,收藏加关注,谢谢屏幕前帅气的你。
思维导图如下。
正文
一. 认识数据源
本二硕一的可以跳过这一节。
相信大家都用过线程池,是一种将线程缓存起来的池化技术,可以避免线程频繁创建和销毁带来的性能损耗。同样的,数据源也是一种池化技术,将数据库连接缓存起来,可以避免数据库连接频繁建立和回收带来的性能损耗。
那现在跟随我的步伐,来看一下数据源是怎么融入我们的实际业务开发的。
我们要和数据库做交互,首先需要数据库连接,也就是Connection,在远古时期,我们是通过JDBC的相关API来获取Connection的,伪码如下。
// 直接通过数据库驱动获取连接
Connection connection = DriverManager.getConnection(url, username, password);
有了数据源之后,我们获取Connection就不再原始了,可以先把数据源创建出来,然后再向数据源借一个Connection,伪码如下。
// 创建数据源
DataSource dataSource = new DataSource();
// 通过数据源获取连接
Connection connection = dataSource.getConnection();
为什么说是借呢,因为有借还有还嘛,当业务线程从数据源借走一个连接并使用完毕后,业务线程需要负责将连接归还到数据源,这样数据源才能继续将这个连接借给下一个业务线程。
我们要使用数据源时,不可能每次都是new出来再使用,所以在Springboot框架下,数据源通常都是作为bean存放在IOC容器中,并且可以有多个数据源的bean,这也是我们常说的多数据源和动态数据源,此时业务线程要获取连接时,就是下面这样的。
通常我们操作数据库,会使用例如MyBatis,MyBatis-Plus或者JdbcTemplate这样的ORM框架,所以此时数据源不再是被业务线程注入,而是将数据源配置给到ORM框架,由ORM框架向数据源借连接,也由ORM归还连接给数据源,就像下面这样。
因为有事务的存在,所以真正的情况其实是下面这样的。
那么数据源在真实的使用中,其实就是提前创建出来然后负责给业务线程,或者负责给各种ORM框架提供Connection,当然使用完的Connection,也需要归还给到数据源。
现在假设因为研发 太菜,数据源一通乱配,导致数据源出现很严重的性能瓶颈,那么一个接口阻塞几十秒不是什么难事,随便一个生产事件也不是什么难事,所以我们接下来就好好研究一下怎么配。
二. 数据源配置分类说明
数据源的配置,按照我的理解,可以分为下面这么几大类。
- 连接数量配置。也就是配置数据源里面的连接数量,以及和连接数量变化相关的一些时间参数;
- 连接校验配置。数据源会管理连接,并且负责将可用的连接给到业务线程,那么数据源需要一种机制来保证连接可用以及发现不可用的连接,那么连接校验配置就是决定怎么去保证连接可用和发现不可用连接;
- 连接保活配置。数据库连接底层是一个TCP连接,只要创建出来就会占用到数据库服务端一个会话,那么如果数据库连接长时间不用,那么可能中间的网络转发设备,或者数据库服务端都会有相应的机制来杀掉这个TCP连接,所以需要保活,也就是让连接就算空闲很久也具有活性;
- 连接获取配置。主要就是控制数据源没有可用连接时,业务线程的最大等待时间。
后续探究数据源配置时,会以上面四个大类来展开,每个大类下会分别探究HikariCP,Druid和TomcatJdbc该怎么配以及为什么这么配。
三. 连接数量配置
连接数量的配置,可以决定数据源初始状态有多少连接,空闲状态有多少连接,最大有多少连接,以及连接数量如何变化。
在讨论这部分配置前,我们需要一个前提条件,就是数据库服务端支持最大连接数为600,我们应用双集群部署,每集群下5个Pod实例,每实例配置单数据源。
1. HikariCP
HikariCP数据源的连接数量配置,罗列如下。
- maximumPoolSize=30。表示数据源的数据库连接的最大数量,包括可用连接和已用连接,如果不配,默认是10,这里建议配置为30;
- minimumIdle=30。控制数据源的最小空闲连接数,也就是当前数据源如果连接数小于等于该配置,那么就算一个连接空闲时间达到了清理的条件,这个连接也不会被清理。不同于另外两款数据源,对于HikariCP这个数据源来说,我建议把minimumIdle和maximumPoolSize配置为相同的值,这样可以保持数据源的连接数相对稳定,以达到更机制的速度;
- idleTimeout=120000。控制非核心连接空闲达到多久会被销毁。这里说的非核心连接就是数量大于minimumIdle小于等于maximumPoolSize的这一部分连接,后台会有线程每隔30s就判断一次非核心连接是否空闲达到idleTimeout,如果达到就销毁这个连接。该配置默认是600s,最小可以配置为10s,如果配置小于10s则会被重置为默认的600s。这个配置仅在minimumIdle < maximumPoolSize时生效,所以在我的建议中,可以不配。
对于HikariCP数据源的连接数量配置,值得建议的就一点,maximumPoolSize要和minimumIdle配一样。
2. Druid
Druid就有点麻烦了,我们下面一起来看看。
- initialSize=15。该配置项控制数据源初始化时就创建出来的连接数,默认是0;
- maxActive=30。该配置项决定数据源的最大连接数,即可用连接和已用连接之和不能大于这个值,默认是8;
- minIdle=15。该配置项决定数据源的核心连接数,所谓核心连接,就是只要空闲时间满足小于等于maxEvictableIdleTimeMillis的连接,这个连接就不会被销毁,默认是0。关于这个参数,需要结合minEvictableIdleTimeMillis和maxEvictableIdleTimeMillis一起理解;
- minEvictableIdleTimeMillis=60000。连接会被销毁的最小空闲时间。非核心连接只要空闲时间大于等于该配置,那么这个非核心连接就会被销毁。默认是30m,如果不想常态让数据源维持太多连接,可以考虑配置一个较小的值;
- maxEvictableIdleTimeMillis=25200000。连接会被销毁的最大空闲时间。无论是核心连接还是非核心连接,只要空闲时间大于该配置,那么这个连接就会被销毁,默认是7h,建议配置为略小于数据库服务端连接空闲超时时间,比如MySQL服务端连接的空闲超时时间是8h,那么这里可以配置为7h;
- phyTimeoutMillis=25200000。连接的物理存活时间,默认是 -1表示一直存活,可以配置为和maxEvictableIdleTimeMillis一样的值,在配置了maxEvictableIdleTimeMillis时,phyTimeoutMillis也可以不配置;
为什么说Druid有点麻烦呢,因为在连接数量这一块儿,Druid的配置是最多,判断逻辑也是最多的,下面用一个流程图来示意下。
Druid数据源里面的连接,会被后台线程每间隔timeBetweenEvictionRunsMillis就扫描一次,主要就是判断这个连接需不需要被回收,需不需要被保活(关于保活会在后面章节说明),所以Druid数据源的连接数量配置,请一定要仔细看一下上面的说明和图示。
3. TomcatJdbc
TomcatJdbc数据源的连接数量配置,罗列如下。
- initialSize=15。该配置决定数据源初始化时就创建的连接数量,默认是10;
- minIdle=15。该配置项决定数据源能空闲的连接数的最小值,即空闲连接数小于这个值时无论连接空闲多久都不会被销毁,建议将该值配置为和initialSize一样的大小,默认等于initialSize;
- maxActive=30。该配置项决定数据源总连接数的最大值,默认是100,请一定不要使用默认值,默认的最大连接数太大了,在发生流量突增或者弹性扩容时,容易把服务端连接占满;
- maxIdle=30。这个配置通常是不生效的,但是这里还是解释一下这个配置,因为有人不是很理解这个配置的含义。maxIdle表示空闲连接的最大数量,也就是连接归还到数据源时如果空闲连接数已经大于等于maxIdle,此时这个连接不会归还到数据源而是直接释放,默认等于maxActive;
TomcatJdbc的连接数量配置相较于Druid就简单许多,只需要控制好maxActive的取值,避免单数据源占用过多连接。
四. 连接校验配置
业务线程从数据源借出一个连接时,如果连接不可用,那么就会造成业务线程里面的业务逻辑报错,这肯定是需要避免的,所以三种数据源都提供了连接校验配置,以保证连接借出和归还时的有效性。
1. HikariCP
HikariCP在将连接借出之前,默认一定会做一次连接校验,所以使用HikariCP数据源时,借出连接时的校验是无法避免的。
- connectionTestQuery=SELECT 1。所谓连接校验,其实就是数据源拿着即将借出去的连接与数据库服务端做一次交互,如果配了connectionTestQuery,那么就是执行一次配置的SQL语句,如果connectionTestQuery没有配,那么此时就是Ping一次数据库服务端。HikariCP数据源的connectionTestQuery默认是null,所以请一定要配上connectionTestQuery;
- validationTimeout=5000。无论是通过Ping还是通过执行SQL来校验连接,肯定是需要一个超时时间的,validationTimeout就是决定这个超时时间,默认是5000ms,最小可以设置为250ms,小于最小值会被设置为默认值。
请一定不要将validationTimeout设置得很大,否则会极大的降低业务线程获取数据库连接的效率。
2. Druid
Druid在连接校验方面,提供了如下参数进行逻辑控制。
- testOnBorrow=false。该配置项控制每次获取连接时是否校验连接有效性,如果没有打开testWhileIdle,那么该配置项需要配置为true,但在打开了testWhileIdle时,建议testOnBorrow配置为false。默认是false;
- testOnReturn=false。该配置项控制每次归还连接时是否校验连接有效性,这里的连接校验会发生在业务线程中,会影响业务性能,建议配置为false。默认是false;
- testWhileIdle=true。该配置项控制每次获取连接时是否对空闲连接进行有效性校验,这里的空闲标准是 连接空闲时间 > timeBetweenEvictionRunsMillis,建议开启该配置,相较于testOnBorrow,testWhileIdle能够以更低的性能损耗保证连接的有效性。默认是true;
- validationQueryTimeout=5。注意单位是秒。该配置项决定校验连接时的最大等待时间,超过这个时间则校验连接失败,建议配置一个较小的值以防止校验连接等待很久。默认是 -1,表示在Statement层面不超时,此时校验连接的超时时间就取决于连接串中的socketTimeout;
- validationQuery=SELECT 1。表示校验连接时执行的查询语句, 如果是MySQL则默认查询语句为SELECT 1,如果是Oracle则默认查询语句为SELECT 'x' FROM DUAL,如果是PgSQL则默认查询语句为SELECT 'x',建议根据具体的数据库进行显式的配置;
Druid针对连接校验提供了丰富的选择,如果求稳,建议打开testOnBorrow,如果求快,建议打开testWhileIdle,无论求啥,建议都不要打开testOnReturn。
同时这里多说一句,请一定要显式的配置validationQueryTimeout,因为如果你不配,那么其取值就为默认的 -1,那么执行SQL时的Statement就没有超时时间,此时超时时间就取决于连接串里面的socketTimeout,但是假如socketTimeout也没配呢,此时超时时间一般是900s也就是15min左右,万一连接不可用,然后我们又啥都没配,那画面不敢想,所以请一定把validationQueryTimeout配好。
3. TomcatJdbc
TomcatJdbc的连接校验配置如下。
- testOnBorrow=true。该配置项决定从数据源获取到连接后是否校验其有效性,建议配置为true即需要校验连接的有效性,在校验不通过时TomcatJdbc会直接重连一次数据库,这里的重连,其实就是先销毁底层数据库连接然后再新建一个底层数据库连接。该配置项默认为false但在Springboot中默认是true;
- validationQuery=SELECT 1。该配置项决定校验连接时使用的SQL语句,默认是null但在Springboot中默认是 /* ping */ SELECT 1;
- validationQueryTimeout=5。注意单位是秒。该配置项决定校验连接时的最大等待时间,超过这个时间则校验连接失败,建议配置一个较小的值以防止校验连接等待很久。默认是 -1表示不超时,此时校验连接的超时时间就取决于连接串中的socketTimeout。
TomcatJdbc校验连接失败时会重连一次,重连后会再次校验连接,如果还失败则抛出异常,这种机制其实还算优雅,所以TomcatJdbc建议将testOnBorrow配置为true.
TomcatJdbc和Druid一样,也有validationQueryTimeout配置,并且注意事项都是一样的,这里就不再赘述了。
五. 连接保活配置
连接保活超级,超级,超级重要。
在这之前,再重新强调一下什么是连接保活。所谓连接保活,就是在连接空闲时,每隔一定时间使用这个连接和数据库做一次交互,这样能起到两个作用,其一是让中间网络设备知道当前连接对应的TCP连接有在被使用以避免中间的网络设备把TCP连接给无感知的断开掉,其二是维持当前连接在数据库服务端这边的会话活性,以避免数据库服务端这边将连接对应的会话给杀掉。
合理配置数据源的保活策略,能够避免连接失活带来的额外性能开销,极大的提升数据源的连接借出效率,所以连接保活配置,真的很重要。
1. HikariCP
如果使用的HikariCP的版本是4.x,那么连接保活配置如下。
- keepaliveTime=120000。该配置项是HikariCP数据源连接保活的最重要配置项。配置了该参数后,每个连接在创建出来时会被添加一个周期定时任务,每隔keepaliveTime的时间就会对连接做一次保活,如果定时任务触发时连接正在被使用,则当次保活取消。默认是0表示不保活,建议一定要配置一个小于T的值(T表示连接空闲多久时会被无感知的断开),如果配置为一个小于30s的值,则保活功能也会关闭;
- maxLifetime=180000。该配置项控制连接的物理存活时间,严格意义来讲,这不是一个保活配置,但是却能起到和保活差不多的效果。如果配置了maxLifetime则每个连接在创建出来时会被添加一个定时任务,大约在maxLifetime时间时会触发定时任务,触发定时任务时如果连接没有被使用则直接销毁连接,如果连接有被使用则标记连接为软销毁,被标记为软销毁的连接在下一次被获取时就会被物理销毁。默认是1800s,最小可设置为30s,小于最小值时会被重置为默认值,建议配置为一个略小于T的值(T表示连接空闲多久时会被无感知的断开)。
如果HikariCP的版本是3.x,那么是没有keepaliveTime这个配置项的,此时只能通过配置maxLifetime来起到和保活一样的效果。
2. Druid
Druid数据源从1.0.28版本开始,就提供了连接保活配置,说明如下。
- keepAlive=true。是连接保活的总开关,默认是false,一定要打开这个开关;
- keepAliveBetweenTimeMillis=120000。连接的空闲时间大于等于该配置项时,连接就会做一次保活,默认是120s;
- timeBetweenEvictionRunsMillis=60000。控制销毁线程执行任务的频率,销毁线程执行的任务就是进行空闲连接销毁和保活(可以参考第三节第2小节的流程图),该配置项需要满足这样一个规则,即keepAliveBetweenTimeMillis + timeBetweenEvictionRunsMillis < T(T表示连接空闲多久时会被无感知的断开),默认是60000ms。
Druid要开启保活,最重要的就是将keepAlive配置为true,然后想要保活能百分百的起到效果,就需要保证keepAliveBetweenTimeMillis + timeBetweenEvictionRunsMillis < T。
3. TomcatJdbc
TomcatJdbc数据源的连接保活配置如下。
- testWhileIdle=true。配置为true表示开启连接保活,每隔timeBetweenEvictionRunsMillis的时间触发一次连接保活,所有空闲且校验间隔达到了validationInterval的连接会进行保活。默认是false;
- timeBetweenEvictionRunsMillis=60000。表示验证并清理空闲连接的定时任务的触发间隔。这个验证并清理空闲连接的定时任务主要作用就是进行连接保活,所以这个配置可以理解为触发连接保活的间隔时间。默认是5000ms,该配置需要满足这样一个规则,即timeBetweenEvictionRunsMillis + validationInterval < T(T表示连接空闲多久时会被无感知的断开);
- validationInterval=120000。表示连接相邻两次校验的间隔时间。也就是只有当前时间距离连接上一次被校验的时间大于validationInterval时,这个连接才允许被校验,默认是3000ms;
- maxAge=180000。表示连接的最大存活时间,默认是0表示一直存活,建议配置为一个小于T的尽可能大的值。
TomcatJdbc数据源的保活开关配置是testWhileIdle,可千万不要和Druid的那个testWhileIdle搞混了。建议显式的配置timeBetweenEvictionRunsMillis和validationInterval为上面推荐的值,总觉得TomcatJdbc的默认配置都有点激进。
六. 连接获取配置
三种数据源的连接获取配置都不多,除了Druid有两个相关配置外,其余两种数据源只有一个相关配置,但是连接获取配置是最有坑的,你以为你是这样配的,但其实事情往往不是你想的那样。
1. HikariCP
只有一个相关配置,如下所示。
- connectionTimeout=5000。等待获取连接的最大超时时间,默认是30000ms,最小可配置为250ms,小于最小值则会被设置为默认值。建议配置一个较小的值。
当我们配置了connectionTimeout时,我们本意就是获取连接时最多等connectionTimeout这么多时间,超时则抛出SQLException异常,但是实际情况是有出入的,下面用一个简单的流程图来进行示意。
connectionTimeout这个值包含从数据源获取连接和校验连接的时间。如果一个连接获取出来但是校验失败,此时整个过程耗时没有达到connectionTimeout,那么会继续从数据源再获取连接,然后再校验,直到获取到一个通过校验的连接或者整个过程耗时达到了connectionTimeout。如果达到了connectionTimeout也没获取到连接,此时会抛出SQLException。
这里的坑点就在于获取到连接后是先校验然后再计算剩余等待时间,这就导致万一校验连接耗时很久,那么业务线程等待连接超时的时间是会大于connectionTimeout的值的。
但是规避这个问题也很简单,第一就是配好连接保活,第二就是将校验连接的超时时间validationTimeout配得小一点。
2. Druid
Druid数据源的连接获取配置有两个,如下所示。
- maxWait=5000。表示获取连接时的单次等待从连接数组获取连接的最大时间。默认是 -1,表示单次等待从连接数组获取连接是一直不超时的,建议配置为一个较小的值;
- notFullTimeoutRetryCount=-1。该配置项默认为0,建议配置为 -1。
Druid的maxWait配置是最具迷惑性的,你可能以为maxWait控制的是业务线程等待获取连接的最大超时时间,但是实际maxWait仅控制单次等待从连接数组获取连接的最大时间,当达到这个时间并超时之后,Druid会进行下面这样的操作。
- 如果此时数据源的连接没满(可用连接数 + 正在使用连接数 < 最大连接数),那么会最多做notFullTimeoutRetryCount + 1次的重试获取连接,每次重试都是最大会等待maxWait的时间;
- 如果此时数据源连接满了(可用连接数 + 正在使用连接数 >= 最大连接数),或者notFullTimeoutRetryCount + 1次重试已经做完了,此时抛出超时异常。
所以当数据源连接不够用时,业务线程获取连接至少等待maxWait的时间,至多等待notFullTimeoutRetryCount + 2倍的maxWait的时间。
上面讨论的是从连接数组等待但最后没有获取到连接的情况,但是如果获取到了连接,并且我们配置了testOnBorrow或者testWhileIdle等校验连接的配置,然后还校验失败了,此时Druid会先丢弃校验失败的连接,然后又去以maxWait的超时时间去连接数组里面等待,然后重复这整个过程。仔细想一想,maxWait仅决定单次从连接数组等待获取连接的超时时间,但是整个获取连接的耗时,包括多次从连接数组等待连接的耗时,以及每次获取到连接后的校验耗时,导致最终的获取连接的耗时与maxWait基本是没什么关系的。
下面以一个简单流程图来加深理解。
要规避Druid获取连接时存在的问题,第一就是配好连接保活,第二就是将notFullTimeoutRetryCount配置为 -1,第三就是校验连接超时时间validationQueryTimeout配得小一点。
3. TomcatJdbc
TomcatJdbc的连接获取配置,解释如下。
- maxWait=5000。该配置项决定从数据源获取连接时最多等多久, 等待超过这个时间则会抛出超时异常,默认是30000ms,建议配置一个较小的值。
如果数据源一直是空的,那么业务线程等待获取连接的最大超时时间就由maxWait来决定,但是如果获取到了连接,并且配置了testOnBorrow,此时连接还会进行一次连接校验,如果校验失败,则会重连并再次进行连接校验,所以业务线程真正的等待获取连接最大超时时间应该是会大于maxWait的。
TomcatJdbc的获取连接的流程图示意如下。
个人感觉三种数据源里面,TomcatJdbc的获取连接的流程是最优雅的,这个重连机制就很灵性,但是也不可避免的会有业务线程真正获取连接的最大超时时间会大于maxWait的情况,要避免也很简单,配置好连接保活,避免校验连接阻塞很久,这样业务线程等待获取连接就不会阻塞很久。
七. 版本说明
1. HikariCP
HikariCP的版本建议是4.0.3,因为从这个版本开始,提供了keepaliveTime参数来开启连接保活。
2. Druid
Druid的版本至少需要是1.0.28,因为从这个版本开始,提供了keepAlive保活机制。
但是在Druid的1.2.17及以下版本,存在连接泄露的BUG,具体可以参加如下issue。
如果能升级的话,尽量升级到1.2.18及之后的版本。
3. TomcatJdbc
TomcatJdbc跟着Tomcat版本走即可,例如9.0.82为一个稳定版本。
总结
常用数据源有三种,分别是HikariCP,Druid和TomcatJdbc,但是无论是哪种数据源,其配置均可以分类为连接数量配置,连接校验配置,连接保活配置和连接获取配置,只有理解这四类配置,并配置合适的值,才能避免因为数据源的问题导致的接口响应时间增大和成功率下降等问题。
探究源码不易,如果觉得本文对你有帮助,烦请点赞,收藏加关注,谢谢帅气漂亮的你。
转载自:https://juejin.cn/post/7372084329079865354