本文是基于对druid参数的理解上,更进一步对各种异常情况下进行压测。结合产线实际情况,给出一个建议值。建议先阅读: druid数据源源码解读及参数解释
目标
数据库一般都是应用的最底层服务,而数据源正是对该底层服务的一个初步封装,因此数据源的正确配置至关重要。目前,我们对数据源的要求如下:
- 慢查询:隔离影响,避免占用过多资源
- qps突增:请求在合理的超时时间拿到连接并执行
- 网络抖动:短时抖动无感知,持续抖动快速失败
- 数据库不可用:快速失败
压测方式
为了避免应用中其他组件影响分析,只针对druid数据源进行压测,花了点时间写了一个Jmeter插件来进行压测。
这里非常不建议使用手写多线程/countDownLunch等方式模拟请求。一是这样统计结果非常困难;另外,也很难控制qps。
自定义Jmeter插件
采用了最简单的方式:初始化时读取Jmeter参数,并创建DruidDataSource
。
后续所有请求直接通过该数据源获取连接,执行预设sql,归还连接,一气呵成。
Jmeter插件开发这里不过多涉及,搜索关键字AbstractJavaSamplerClient
即可。
异常情况
慢查询
最常见的一种情况,我们的第一反应是通过配置connectionProperties
中的socketTimeout
,readTimeout
控制sql执行超时时间。
这些本身不是数据源定义的配置项,数据源只是简单的把connectionProperties配置传递给jdbc Driver。 所以oracle/mysql对执行超时的配置项不同,本文以mysql为例。
com.mysql.cj.protocol.a.NativeSocketConnection#connect
this.mysqlSocket = (Socket)this.socketFactory.connect(this.host, this.port, propSet, loginTimeout);
int socketTimeout = (Integer)propSet.getIntegerProperty(PropertyKey.socketTimeout).getValue();
if (socketTimeout != 0) {
try {
this.mysqlSocket.setSoTimeout(socketTimeout);
} catch (Exception var9) {
}
}
分析
根据上一篇的知识,我们针对慢查询可以做出以下分析:
-
配置sql执行超时时间,防止一个线程长时间占用数据库连接。
-
当数据库连接被慢查询占满时,其他线程不应等待过长时间。
用例一
配置:
key | value |
---|---|
socketTimeout | 1000 |
maxWait | 1500 |
maxActive | 8 |
failFast | false |
maxWaitThreadCount | -1 |
请求线程:100个线程,1秒内启动
限制单线程最大样本量:600/min
慢sql: select sleep(2)
可以观察到,打印出的报错信息如下:
The last packet successfully received from the server was 1,003 milliseconds ago. The last packet sent successfully to the server was 1,003 milliseconds ago.
...
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
...
2021-03-13 19:23:12,618 WARN c.a.d.p.DruidDataSource: get connection timeout retry : 1
2021-03-13 19:23:12,617 ERROR c.a.d.p.DruidDataSource: discard connection
压测结果如下:
根据压测统计可以看出,请求的响应时间主要有以下几个节点:
-
1000ms Line ->极少
因为目前数据源最大连接数配置只有8,在请求进来后连接池马上被占满。 所以,只有少数请求可以被`socketTimeout`影响:sql得到资源去执行,但是因为socket超时,并没有得到返回结果。
-
1500ms Line->84.5%
大多数请求都是1.5s:因为启动后连接池被瞬间占满,等待连接消耗1.5s后超时。这里是maxWait起作用
-
2500ms Line->98.1%
少部分请求在等待一段时间后得到了连接,但由于得到资源后去执行sql,最终因为socket超时结束。
-
3000ms Line->99.7%
-
3500ms Line->99.9
这个两个时间线通过日志来看,基本可以判断是由于丢弃连接,并触发notFullTimeoutRetryCount引发的。
用例一结论
通过上面的压测统计,可以很容易的分析出以下结论:
socketTimeout
确实起到了作用,使本来需要占用连接2s的sql在1.5秒就发生了超时异常。- 案例中绝大部分样本都是1.5秒返回,他们并没有去执行sql,而是在等待连接资源。
用例二
为了解决案例一中大量请求将时间耗费在等待连接的问题,本例中将减小maxWait
配置。
更新数据源配置如下:
key | value |
---|---|
socketTimeout | 1000 |
maxWait | 500 |
maxActive | 8 |
failFast | false |
maxWaitThreadCount | -1 |
其他采用与案例一相同的样本压测:
与案例一中压测结果相比的话,图形的形状大致相同,只是耗时有较大幅度的减小。
-
500ms Line -> 89.1%
效果明显,有接近90%的请求因为没有获得连接,在500ms内就因为获取不到连接超时返回。
...
-
1500 Line -> 99.9%
这里的效果已经和我们预期相同,最长时间为maxWait+socketTimeout。
用例二结论
当数据源所有连接被占用时,maxWait
为第一道防线。但是,只依赖maxWait
来防御还是有所粗糙,毕竟在慢sql充斥的情况下,有不少线程还是“白等”。
那么,有没有一种情况可以不等呢?请看案例三。
用例三
现在,我们希望如果发现当前等待获取连接的线程数过多的话,就直接失败,而不再去等待maxWait
。
更新数据源配置如下:
key | value |
---|---|
socketTimeout | 1000 |
maxWait | 500 |
maxActive | 8 |
failFast | false |
maxWaitThreadCount | 50 |
其他采用与案例一相同的样本压测:
-
4ms Line -> 57.6%
有50%+的请求由于进入时已有太多的线程在等待,而直接快速失败,没有白白浪费时间。
-
...
其他耗时产生的原因与上例相同,这里不再重复。
用例三结论
正常情况下,数据源中不会出现过多的等待线程。所以,通过maxWaitThreadCount
来控制快速失败也是一个不错的办法。
小结
一般来说,socketTimeout
建议配置为系统中执行最慢的sql耗时。并且,如果在数据源层面已经允许了慢sql,那么,在业务层一定要配置限流。
而maxWait
则需要考虑多种因素:网络收敛时间,业务线程超时时间等,所以maxWait
并不能一味的减小。
但是,在一些情况下,真的没必要再等maxWait
,这时就可以使用maxWaitThreadCount
来控制快速失败条件。
当然,maxWaitThreadCount
的配置要根据业务线程池大小,数据库连接池大小:maxActive
;最小空闲连接数minIdle
等来计算配置。
网络抖动(延迟)
我们使用阿里开源的混沌工程:chaosblade来模拟网络的各种异常情况。
## 示例
## 增加网络延迟:2s
./blade create network delay --time 2000 --interface eth0 --local-port 3306
## 恢复网络延迟
./blade destroy network delay --interface eth0
分析
网络的影响范围就比较广了:创建连接,实际执行都会受到网络延迟,丢包的影响。这里,我们主要测试短暂的网络不稳定会对数据源带来怎样的影响,以及,如何减小影响。
用例四
key | value |
---|---|
socketTimeout | 1500 |
connectTimeout | 100 |
maxWait | 500 |
maxActive | 8 |
failFast | false |
maxWaitThreadCount | 50 |
请求线程:100个线程,1秒内启动
限制单线程最大样本量:600/min
测试sql: select sleep(0.001)
网络延迟:浮动,1s内延迟。
./blade create network delay --time 0 --offset 1000 --interface eth0 --local-port 3306
在设置connectTimeout=100
,并设置网络延迟后,我们得到了一种新的异常:
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[?:1.8.0_181]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[?:1.8.0_181]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[?:1.8.0_181]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[?:1.8.0_181]
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61) ~[druid_tuning-1.0-SNAPSHOT-jar-with-dependencies.jar:?]
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[druid_tuning-1.0-SNAPSHOT-jar-with-dependencies.jar:?]
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151) ~[druid_tuning-1.0-SNAPSHOT-jar-with-dependencies.jar:?]
at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167) ~[druid_tuning-1.0-SNAPSHOT-jar-with-dependencies.jar:?]
at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:91) ~[druid_tuning-1.0-SNAPSHOT-jar-with-dependencies.jar:?]
at com.mysql.cj.NativeSession.connect(NativeSession.java:144) ~[druid_tuning-1.0-SNAPSHOT-jar-with-dependencies.jar:?]
at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:956) ~[druid_tuning-1.0-SNAPSHOT-jar-with-dependencies.jar:?]
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:826) ~[druid_tuning-1.0-SNAPSHOT-jar-with-dependencies.jar:?]
... 6 more
Caused by: java.net.SocketTimeoutException: connect timed out
at java.net.PlainSocketImpl.socketConnect(Native Method) ~[?:1.8.0_181]
由于网络延迟的关系,大部分创建数据库连接的请求都无法成功。
并且,maxWait的重试机制在本例的情况下影响最明显:存在将近1/3的线程等了两次maxWait。
另外,有65.2%的请求由于设置了maxWaitThreadCount
而快速失败。
用例四结论
socketTimeout
指的是通过tcp发送数据,等待响应的超时时间。而connectTimeout
指的是建立连接的超时时间。
当网络状况不佳,影响建立连接时,maxWait
的实际等待时间会受到notFullTimeoutRetryCount
的影响,造成实际等待时间翻倍。
案例五
五.一
key | value |
---|---|
socketTimeout | 1500 |
connectTimeout | 100 |
maxWait | 500 |
maxActive | 8 |
failFast | true |
maxWaitThreadCount | 50 |
其他与案例四相同,不同的是我们开启了failFast
,依旧存在1s内的网络延迟。
再来一遍:
虽然快速失败的效果很明显,执行耗时大大减少。
但是,在用例四种,还有3.2%的样本成功,到这里,怎么就全部失败了呢?
原因很简单:因为我们的initialSize
,minIdle
配置均为0(默认配置)。
由于池中没有空闲连接,当请求进入后,只能新建连接,而在druid数据源发现创建连接失败后,马上开启快速失败,导致后续请求只是发出了要求创建连接的信号,并没有再等待连接。
五.二
为了验证,我们调整initialSize
,minIdle
为8,其他相同在压测一次:
key | value |
---|---|
socketTimeout | 1500 |
connectTimeout | 100 |
maxWait | 500 |
maxActive | 8 |
failFast | true |
initialSize | 8 |
minIdle | 8 |
maxWaitThreadCount | 50 |
由于池中有较多的空闲连接,还是有一些请求可以得到机会去执行的,所以会有少量请求执行成功。
但是,结合日志和压测结果来看的话,failFast
并没有起到太多作用,本次测试仅有4笔请求响应了快速失败。
因为在池中有空闲连接的情况,很少有机会去创建新连接,并且,快速失败在创建连接失败后才会翻转变量,这时候,很多请求已经在等maxWait
了,只有后续新进去的请求才会响应。
所以,并不能第一时间触发快速失败,他们只能发现连接池满,等待。。。
用例五结论
当开启failFast
的情况下,connectTimeout
不能配置的过小,否则快速失败将过于敏感。
快速失败翻转后,仅对后续新进入的请求起作用,已经进入到等待的线程不会受到影响。
用例六
我们再测试一下瞬间的网络延迟有什么样的效果:
key | value |
---|---|
socketTimeout | 1500 |
connectTimeout | 100 |
maxWait | 500 |
maxActive | 8 |
failFast | false |
initialSize | 0 |
minIdle | 0 |
maxWaitThreadCount | 50 |
六.一
以下是没有任何干扰的情况下:
通过日志可以看出,本次失败的全部是因为maxWaitThreadCount
触发的快速失败。
六.二
在压测过程中,我们给他加一个500ms的延迟,持续500ms。
由于中间被施加了500ms延迟,会有大量的请求线程堆积,有近一半的请求是由于maxWaitThreadCount
触发快速失败。剩下是等待maxWait
超时失败。
用例六结论
当网络出现短时波动后,数据源等待线程会快速增长,并触发maxWaitThreadCount
快速失败。
实际上,如果我将maxWaitThreadCount
设置为-1
(关闭maxWaitThreadCount快速失败):
执行的成功率会大大提高,耗时也只是略有增长。(毕竟延迟持续时间只有0.5s)
小结
网络延迟的表现情况看上去和慢sql类似,都是执行耗时变长。不同的是:网络延迟会影响创建连接,所以:connectTimeout
应配置为网络最大延迟时间。
同时failFast
是由创建连接触发的,当failFast触发后,可以无视maxWait
超时时间,直接失败。
而maxWaitThreadCount
也是一种快速失败的方式,可以参考业务线程池配置,不宜配置过小,否则容易误伤(短时延迟的情况下)。
网络抖动(丢包)
分析
网络丢包在应用层看起来,和延迟类似,不过比相对固定延迟拥有更多的不确定性。
## 丢包20%
./blade create network loss --percent 20 --interface eth0 --local-port 3306
用例七
七.一
key | value |
---|---|
socketTimeout | 1500 |
connectTimeout | 1000 |
maxWait | 500 |
maxActive | 8 |
failFast | false |
initialSize | 0 |
minIdle | 0 |
maxWaitThreadCount | 50 |
请求线程:100个线程,1秒内启动
限制单线程最大样本量:600/min
测试sql: select sleep(0.001)
网络丢包:20%
同样的,由于maxWaitThreadCount
快速失败的关系,有超过一半的请求都执行了快速失败。
七.二
同样的,我们再把maxWaitThreadCount
关闭看一下:
通过日志可以看出,本次的失败原因主要是由于等待maxWait
超时引起的。
小结
虽然在协议上,tcp延迟和丢包不同。但是,如果表现在应用层面,都是相同的,看到的异常都是超时。
数据库宕机
数据库宕机的话,最好的情况就是快速失败了。
分析
数据库宕机后,新建连接一定会超时(connectTimeout),而现有连接的话,会走socketTimeout超时。
并且,等待线程也会快速堆积,我们这里主要看恢复速度。
用例八
key | value |
---|---|
socketTimeout | 1500 |
connectTimeout | 1000 |
maxWait | 500 |
maxActive | 8 |
failFast | false |
initialSize | 0 |
minIdle | 0 |
maxWaitThreadCount | 50 |
请求线程:100个线程,1秒内启动
限制单线程最大样本量:600/min
测试sql: select sleep(0.001)
在压测期间,停掉数据库,并重新启动。
小结
数据库宕机后,应用表现也和网络延迟类似。读超时,连接超时。
总结
本文针对druid数据源中几个关键参数对数据库的实际影响做了测试,异常情况下压测结果基本和我们预期相符。
那么汇总一下建议配置:
配置项 | 参考配置 |
---|---|
socketTimeout | 应用中最慢sql执行时间 |
connectTimeout | 网络最长延迟 |
maxWait | 参考业务超时时间,网络稳定性 |
maxWaitThreadCount | 参考业务线程数,业务超时时间 |
failFast | 根据实际情况,如果正常情况下网络很稳定,可以考虑开启 |
另外几个连接数配置这里就不太好给建议了,要根据自己应用的业务线程数,超时时间等等综合考虑。