大家好,我是IT孟德,You can call me Aman(阿瞒,阿弥陀佛的ē,Not阿门的ā),一个喜欢所有对象(热爱技术)的男人。我正在创作架构专栏,秉承ITer开源精神分享给志同道合(爱江山爱技术更爱美人)的朋友。专栏更新不求速度但求质量(曹大诗人传世作品必属精品,请脑补一下《短歌行》:对酒当歌,红颜几何?譬如媳妇,吾不嫌多...青青罗裙,一见动心,但为佳人,挂念至今...),用朴实无华、通俗易懂的图文将十六载开发和架构实战经验娓娓道来,让读者茅塞顿开、相见恨晚...如有吹牛,不吝赐教。关注wx公众号:IT孟德,一起修炼吧!
专栏文章推荐:
1、为什么用数据库连接池?
Mysql作为主流开源关系型数据库之一,广泛使用于各种类型的企业级应用。我们先来请求一个简单java接口,通过WireShark或TcpDump抓包分析执行一条sql的流程。
public static void main(String[] args) {
try{
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false",
"root", "root");
Statement statement = conn.createStatement();
String sql = "select * from user where user_id = '2300000017002913610'";
ResultSet rs = statement.executeQuery(sql);
statement.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
图1:未使用数据库连接池业务sql请求抓包
上图可以看出MySQL是基于TCP协议进行传输的。经过3次握手建立连接,然后进行账号和权限校验,认证通过继续参数初始化和执行sql,最终返回结果、4次挥手关闭TCP连接。执行一条简单查询sql从建立连接到断开连接包含5个环节,客户端和服务端你来我往量子纠缠了35次,实际流程还包含mysql内部缓存查询、sql解析&优化、引擎层IO以及可能发生的TCP重试等,往往更加复杂。
图2:业务sql请求流程图
我们再看看使用数据库连接池后执行同样一条sql的数据传输过程。
图3:使用连接池后业务sql请求抓包
因为直接复用了已经初始化的数据库连接,应用请求无需临时创建TCP连接和mysql认证,sql执行完成后连接会放回连接池,也无需挥手断开连接。总而言之,使用了数据库连接池,应用请求专注于sql执行,避免了频繁创建和释放数据库连接的资源开销,从而提升系统QPS和稳定性。
2、用哪个数据库连接池?
日常开发中,dbcp、c3p0、durid、hikariCP都是比较常见的数据库连接池解决方案。
对比维度 | dbcp | c3p0 | druid | hikariCP |
简介 | Apache开源,依赖 Jakarta commons-pool ,单线程,Tomcat内置 | 老古董。单线程,Hibernate默认连接池 | 阿里和Apache各自维护,提供强大的监控和扩展功能,防sql注入,内置连接泄露诊断,多线程、异步 | 代码体积小,Springboot2.x默认连接池,多线程、异步 |
Github star | 324 | 1.3k | 27.5k | 19.1k |
PSCache | 是 | 是 | 是 | 否 |
LRU | 是 | 否 | 是 | 否 |
监控 | Jmx | jmx/log | jmx/log/http | jmx/metrics |
Filter扩展 | 否 | 否 | 是 | 否 |
Sql拦截/解析 | 否 | 否 | 是 | 否 |
ExceptionSorter | 否 | 否 | 是 | 否 |
配置加密 | 否 | 否 | 是 | 否 |
对比可见,HikariCP大道至简,专注于连接池基本功能,Druid功能最全面,还支持sql级监控、filter扩展、防sql注入、配置加密等。接下来我们重点比较下当前GitHub星标较多、使用活跃的druid和hikariCP性能表现。
图4:使用HikariCP接口10并发100次请求性能表现
图5:使用Druid接口10并发100次请求性能表现
线程数 | Druid | HikariCP |
5并发 | RPS:7.1;Avg:614.5ms | RPS:9.1;Avg:465.3ms |
10并发 | RPS:8.4;Avg:949.2ms | RPS:12.2;Avg:627.7ms |
20并发 | RPS:7.2;Avg:2196.7ms | RPS:9.9;Avg:1229.8ms |
30并发 | RPS:6.3;Avg:3529.5ms | RPS:10.7;Avg:1913.7ms |
测试数据基于个人PC环境下对同一个http接口分别使用druid和hikariCP时进行多轮施压产生,压测工具为SuperBenchmarker,druid和hikariCP最小空闲连接数和最大连接数都为5和20,基础参数配置一致。
经过比较,同等条件下Hikaricp性能始终略胜一筹,这得益于它通过精简字节码提升JVM处理效率、使用FastStatementList、ConcurrentBag提高并发读写效率、使用threadlocal缓存连接及大量使用CAS的机制最大限度的避免锁竞争,将性能发挥到了极致。
综上,在实际应用中可根据项目侧重点和需求选择Druid或HikariCP,dbcp和c3p0都是单线程且疏于维护不予推荐。
3、怎么用好数据库连接池?
3.1、参数配置
诸如Druid和HikariCP等数据库连接池,除了数据源url、username、password和driverClassName是必须配置外,其他参数都是可选的。大多数程序员使用时,喜欢凭感觉设置或复用过往项目代码依葫芦画瓢,几乎不去追究是否合理。对于一些并发不高的小项目确实没多大影响,但在中大型项目中,非常有必要去推敲每一个参数的合理性,充分利用有限资源发挥连接池的优势,从而避免高并发场景下发生故障。为了用好数据库连接池,本章节结合源码分析和多年项目实战经验深度解读数据库连接池中一些举足轻重的关键配置,仅供参考。
3.1.1、连接数量
数据库连接池,顾名思义就是存放数据库连接的“池子”,所以首当其冲就是要设置“池子”的容量,即连接数的区间范围。Druid相关的参数有initialSize(连接池初始化时创建的连接数,默认0)、maxActive(连接池中最大连接数,默认8)、minIdle(连接池中最小空闲连接数,默认0)、maxIdle(连接池中最大空闲连接数,已废弃,默认8),HikariCP中只有minimumIdle(连接池最小空闲连接数,默认等于maximumPoolSize)和maximumPoolSize(连接池中最大连接数,默认10)。
最小空闲连接数和最大连接数设置多大才合理呢?首先要符合常理,比如Druid最maxActive为负数甚至比initialSize、minIdle还小会直接抛异常;而HikariCP中maximumPoolSize、minimumIdle为负数以及maximumPoolSize小于1也会抛异常,但minimumIdle比maximumPoolSize大时会自动修正为maximumPoolSize值。
确定最小连接数相对比较简单。Druid和HikariCP都支持多线程,所以理论上最小连接数或初始连接数与应用服务器CPU核数保持一致最佳。HikariCP为了性能的考虑,官方建议不要设置最小空闲连接数minimumIdle(默认等于maximumPoolSize),使用一个固定的池子来操作数据库。当然这个只是建议,具体要结合最大连接数的值来评估。
那么最大连接数是不是越大越好?当然不是,mysql数据库的最大连接数max_connections值会限制连接池最大连接数量,在mysql中max_connections默认为100,上限为16384。所以要先确定数据库max_connections值,一般通过评估系统并发,然后压测得到max_used_connections,理想状态下max_used_connections / max_connections≈ 80%。理论上最大连接数最大值介于max_used_connections和max_connections之间最合适,但还需考虑与数据库建立连接的应用服务部署节点数量和分库数量。假设我们的系统是一个3节点的单体服务,使用单个mysql实例横向拆分为10个相同结构的业务库,运营一段时间后max_used_connections为800,max_connections设置为1000,数据库连接池最大值评估为900,那么连接池最大连接数(maxActive、maximumPoolSize)为900/3/10=30。它是所有节点和分库连接的总和,一旦总数超过max_connections,就会出现“too many connections”报错。至此就大功告成了么,其实不然,我们同样要考虑应用服务的硬件性能,尤其是CPU。如上述算出每个连接池最大连接数为30,假如应用服务器只是2核CPU,连接线程远超CPU核数,因涉及频繁上下文切换,RPS和系统响应时间可能会更低。所以我们还需要参考上一章节使用SuperBenchmarker进行压测,在允许的最大值范围内试出一组最佳性能表现的参数(包括HikariCP建议设置相同的最小空闲连接和最大连接场景测试),当然还可以直接增加CPU核数或应用服务节点继续分摊。
在实际的业务中,我们的系统一般都是微服务架构,多节点分布式部署,数据分库包含横向和纵向,计算最大值需要一点耐心。还需要注意的是max_used_connections是数据库启动以来最大并发连接数,会随着业务增长而增长,所以需结合实际业务动态调整数据库和连接池的最大连接数。
3.1.2、连接生命周期
在HikariCP中,idleTimeout表示连接允许闲置的最大时间,且只有maximumPoolSize比 minimumIdle大时才生效。idleTimeout默认值为600000(10分钟),允许的最小值为10000毫秒(10秒)。idleTimeout值被设置为0或者生效时值比maxLifetime还大会被自动修改为0,意味着不会单独进行空闲连接超时检测,连接的存活时间由maxLifetime控制。maxLifetime声明了连接池中每一个连接最大的存活时间,所有非正在使用的连接超过该时间都会强制移除。假设idleTimeout设置为5分钟,maxLifetime设置为10分钟,当某个连接全生命周期时间不足10分钟且闲置超过5分钟时,会被空闲轮巡检测机制释放(轮询有时间差,一个空闲连接最大可空闲idleTimeout + 30s会剔除,平均idleTimeout + 15s);当连接全生命周期已超过10分钟但闲置时长仅1分钟时也可能被无情消灭。为什么说可能呢?为了防止连接池中连接大规模灭绝, HikariCP对每个连接应用了负衰减机制,逐渐减少空闲连接的数量(官方原话:On a connection-by-connection basis, minor negative attenuation is applied to avoid mass-extinction in the pool.)。
综上,HikariCP通过idleTimeout和maxLifetime定义连接的生命周期,idleTimeout受maxLifetime制约且生效条件判断较复杂,所以在实际应用中我们重点配置maxLifetime即可。maxLifetime默认是 1800000 (30分钟),最小值是30000(30秒),也支持配置为0(如果idleTimeout同时为0,则连接具有无限生命周期),那么究竟设置多长呢?官方建议该值比任何数据库或基础设施的连接时间限制短几秒(it should be several seconds shorter than any database or infrastructure imposed connection time limit.),原因是数据库还有自身的连接超时机制,如Mysql为了防止空闲连接浪费资源,超过wait_timeout设置时间后就会主动断开该连接,默认为28800s(8小时)。Mysql中可以执行“SHOW GLOBAL VARIABLES LIKE 'wait_timeout'”查看,wait_timeout过长会导致大量的SLEEP进程无法及时释放,过短可能发生“MySQL server has gone away”报错、死锁等待、事务中断等问题。一把我们的应用程序没有特别夸张的长事务和长连接的话,设置为5到30分钟就够用。假设mysql的wait_timeout为30分钟,那么hikariCP的maxLifetime可以在30分钟的基础上减去一点时间差即可,做到池子里创建的连接资源比mysql连接先失效,如果大于30分钟会因为拿到的连接对象实际上已被mysql主动断开从而发生“Communications link failure”异常。
图6:Communications link failure异常示例
在Druid中,通过minEvictableIdleTimeMillis和maxEvictableIdleTimeMillis定义连接的生命周期。minEvictableIdleTimeMillis为最小空闲时间,默认30分钟;maxEvictableIdleTimeMillis为最大空闲时间,默认7小时。Druid在init时创建了销毁连接的DestroyConnectionThread线程,该线程每隔timeBetweenEvictionRunsMillis执行一次销毁任务DestroyTask(如果timeBetweenEvictionRunsMillis小于0,则执行频率为1秒)。在DestroyTask的shrink方法中可以看到连接空闲时间大于minEvictableIdleTimeMillis且池中空闲连接大于最小空闲连接数minIdle则将该连接加入驱逐数组中,空闲时间大于maxEvictableIdleTimeMillis则不管minIdle直接加入驱逐数组。我们可以将其当做HikariCP的idleTimeout和maxLifetime,需要将这2个参数调整为小于数据库wait_timeout,且minEvictableIdleTimeMillis必须小于maxEvictableIdleTimeMillis。
图7:minEvictableIdleTimeMillis和maxEvictableIdleTimeMillis
3.1.3、连接探活
为了防止连接对象的生命周期小于数据库空闲连接存活时间的情况以及可能存在各种网络异常导致的连接失效,连接池自身是否有兜底的方案?
在高版本的HikariCP(V4.0.1以上)中,连接池增加了双保险探活机制,一方面通过定时任务KeepaliveTask对池里的空闲连接进行遍历判断是否存活,另一方面在获取连接时还会检测连接的有效性。
图8:KeepaliveTask
开启KeepaliveTask需要配置keepaliveTime,该属性控制 HikariCP 探活定时任务执行的时间频率,默认为0,最小值为30s,最大值必须小于maxLifetime。检测过程中连接会存在个位数毫秒级从池中移出,所以不宜过于频繁,一般设置为60000, 即1分钟检测一次。
图9:getConnection
keepaliveTime参数默认是不开启的,且执行存在一定时间差,为防止漏网之鱼,HikariCP在分配连接对象也会判断连接是否存活。如源码所示,hikari在borrow到poolEntry之后,执行图9红框中代码来判断连接的有效性。isMarkedEvicted是判断连接是否为标记删除状态,HikariCP的超时连接不是实时剔除的,在KeepaliveTask中就可以看到对无效连接进行softEvictConnection软删除,最后统一在获取连接时对拿到的标记删除状态的连接进行驱逐。poolEntry.lastAccessed是该连接最后使用的时间,now是当前时间,elapsedMillis方法显然是计算连接闲置时间,aliveBypassWindowMs为固定值500。为了减少不必要的心跳检测,只有连接闲置时间大于500ms时才会继续执行isConnectionAlive方法,KeepaliveTask中同样也是执行该方法,所以我们有必要探究一下isConnectionAlive方法。
图10:isConnectionAlive
在isConnectionAlive方法中,一开始根据isUseJdbc4Validation来决定是否使用JDBC4的Connection.isValid()。isUseJdbc4Validation来源于“this.isUseJdbc4Validation = config.getConnectionTestQuery() == null”,即connectionTestQuery参数如果为Null,isUseJdbc4Validation则为true,程序便会进入JDBC4的心跳检测,否则执行connectionTestQuery定义的sql语句(mysql一般为SELECT 1)判断连接的有效性。那么到底使用哪种心跳检测方式呢?官方强烈建议如果数据库驱动支持JDBC4, 就不要设置connectionTestQuery属性。因为相比于执行select查询方式探活,isValid通过ping 命令进行连接检测性能会更高,当前大多数数据库驱动都是支持JDBC4的。
图11:mysql驱动JDBC4支持
综上,虽然高版本的HikariCP连接池不配置keepaliveTime连接探活,应用程序获取到无效的连接不会报错,但重新建立连接增加了额外的性能开销,没有充分发挥连接池的优势,所以有必要参照上述分析设置合理的maxLifetime和keepaliveTime,而connectionTestQuery无需配置。
在Druid中,连接探活相关配置比较多,但跟HikariCP都是相通的。下面先列举出来,然后一一分析该如何设置。
# 是否开启空闲连接的检测
testWhileIdle: true
# 获取连接时检测连接是否有效
testOnBorrow: false
# 归还连接时检测连接是否有效
testOnReturn: false
# 检查空闲连接的频率(毫秒)
timeBetweenEvictionRunsMillis: 60000
# 检测连接是否有效的SQL语句
validationQuery: SELECT 1
testWhileIdle、testOnBorrow和testOnReturn都是配置是否进行连接有效性检测,区别只是触发时机不同。
图12:testOnBrorow和testWhileIdle
testOnBorrow和testWhileIdle都是连接池分配连接时getConnectionDirect检测有效性,当testOnBorrow开启时,testWhileIdle不会执行。二者的区别在于testOnBorrow无条件进行检测,testWhileIdle只有在连接闲置时长超过timeBetweenEvictionRunsMillis(该参数在驱逐无效连接的destroyTask中也有用到,默认60s)或小于0(可能存在时间回拨)时进行检测。对比看来,testWhileIdle执行概率低,性能开销小,所以一般推荐关闭testOnBorrow并开启testWhileIdle。而testOnReturn是在回收连接时进行检测,没有必要。
接着我们来看看Druid的探活方法testConnectionInternal()。Druid在初始化时会根据数据库驱动设置validConnectionChecker检查器。
图13:validConnectionChecker
这些检查器提供了默认validationQuery,当没有配置validationQuery可以执行对应的sql进行探活。创建Mysql检查器时传了usePingMethod参数,新老版本Druid的usePingMethod默认值不一样,如果不想翻源码可以打印数据源的isUsePingMethod()查看,当值设置为True且validationQuery为Null时,Mysql会通过ping方式探活。v1.2.6以下版本开启usePingMethod存在bug,需要关闭,而高版本中基于性能考虑建议开启。
Druid在v1.0.28版本增加keepAlive配置(默认False),在v1.1.16版本又增加了keepAliveBetweenTimeMillis,二者配合执行探活操作。但直到v1.2.7版本,开启keepAlive仍然存在一些Issues。
图14:Druid版本Issues示例
keepAlive和keepAliveBetweenTimeMillis也作用于destroyTask中。keepAliveBetweenTimeMillis默认为120s,所以在druid-1.1.16及以后的版本,连接池中的连接闲置时间需同时大于或等于keepAliveBetweenTimeMillis和timeBetweenEvictionRunsMillis 才会继续无效连接驱逐和探活操作。当开启keepAlive且连接闲置时间不小于keepAliveBetweenTimeMillis时,该连接就会被加入到探活数组中,后续的探活方式与testOnBorrow/testWhileIdle无异。所以高并发的业务系统若使用高版本druid(v1.2.7之后)建议开启keepAlive功能,通过定时的异步机制代替testWhileIdle获取连接时同步探活。
图15:keepAliveBetweenTimeMillis
3.2、连接池监控
当前主流的连接池组件都支持监控功能,通过监控数据库连接池的使用情况,可以实时了解连接池的使用情况和性能状况,及时发现连接池中存在的问题,如连接泄漏、连接超时等,进而动态调整相关参数,优化系统资源利用,避免系统故障。
3.2.1、Druid监控
Druid天生就具备强大的监控特性,除了提供了连接数、活跃线程数等常规连接池监控指标,还能监控和统计sql执行、并发、慢查、执行时间区间分布等信息。这对于诊断和优化数据库访问性能非常有帮助,可以发现潜在的性能问题并进行针对性的优化。Druid连接池的监控信息主要是通过StatFilter采集的,StatFilter对CPU和内存的消耗都极小,对系统的影响可以忽略不计。在springboot项目中,我们可以通过引入starter依赖或自定义依赖的方式整合Druid。
3.2.1.1、引入starter依赖方式
- pom中引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.21</version>
</dependency>
- 增加yml或properties配置
1)开启Filter
Druid内置了非常多的过滤器,StatFilter用于统计监控信息,WallFilter防御SQL注入,Log4j2Filter打印sql语句,ConfigFilter加解密数据库密码,还有EncodingConvertFilter、CommonsLogFilter、jLogFilter、MySQL8DateTimeSqlTypeFilter等等。其中StatFilter是必须的,其他的根据需要配置,用逗号分隔,参考如下:
spring.datasource.druid.filters=stat,wall
2)配置Filter
开启上述过滤器后,如默认配置不满足需求,可以自定义配置,参考如下:
# 配置StatFilter
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.db-type=mysql
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=1000
# 配置WallFilter
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=mysql
spring.datasource.druid.filter.wall.config.delete-allow=false
spring.datasource.druid.filter.wall.config.drop-table-allow=false
3)配置WebStatFilter
WebStatFilter是web监控过滤器,用于统计web应用请求中所有的数据库信息,如sql语句,sql执行的时间、请求次数、请求的url 地址、以及seesion监控、数据库表的访问次数等。
#启用WebStatFilter
spring.datasource.druid.web-stat-filter.enabled=true
#添加过滤规则
spring.datasource.druid.web-stat-filter.url-pattern=/*
#排除不必要的url
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
#开启session统计功能
spring.datasource.druid.web-stat-filter.session-stat-enable=true
#session的最大个数,默认100
spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
4)配置StatViewServlet
Druid提供了内置web页面用于查看相关监控指标,通过配置StatViewServlet来设置管理后台的属性,如登录账号、密码、黑白名单等。
#启用内置的监控页面
spring.datasource.druid.stat-view-servlet.enabled=true
#内置监控页面的地址
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
#关闭 Reset All 功能
spring.datasource.druid.stat-view-servlet.reset-enable=false
#设置登录用户名
spring.datasource.druid.stat-view-servlet.login-username=admin
#设置登录密码
spring.datasource.druid.stat-view-servlet.login-password=123456
#白名单,默认允许所有访问
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
3.2.1.2、引入自定义依赖方式
- pom中引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
- 编写配置类、后台管理Servlet、Web监控Filter
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Arrays;
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
//后台管理Servlet
@Bean
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
StatViewServlet statViewServlet = new StatViewServlet();
servletRegistrationBean.setServlet(statViewServlet);
servletRegistrationBean.addUrlMappings("/druid/*");
//IP白名单
servletRegistrationBean.addInitParameter("allow","127.0.0.1");
servletRegistrationBean.addInitParameter("loginUsername", "admin");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
return servletRegistrationBean;
}
//web监控Filter
@Bean
public FilterRegistrationBean webStatFilter() {
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> filterRegistrationBean =
new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setEnabled(true);
//设置统计的url
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
//设置排除的url
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
通过上述任意方式配置好Druid监控,启动项目后我们就能访问管理后台查看数据源、sql监控、sql防火墙等。
图16:Druid 监控web界面
此外可以通过DruidStatManagerFacade的getDataSourceStatDataList方法获取监控数据。开发者还可以按需使用DruidStatManagerFacade提供的其他方法,实现定制监控客户端。另外通过启用JMX端口且在StatViewServlet中设置了jmxUrl属性,则会根据指定的url获取jmx服务返回的监控数据。借助jmx-Exporter还可以把jmx暴露的监控指标数据转换为Prometheus可识别的格式。启用JMX配置jvm启动参数参考:
-Djava.net.preferIPv4Stack=true
-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.management.jmxremote.port=1099
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
图17:Druid jmx
3.2.2、Hikaricp监控
HikariCP内置了micrometer监控指标库和prometheus、dropwizard实现。SpringBoot应用可以直接引入Actuator依赖,然后通过JMX或者HTTP endpoints来获取监控指标。同时引入micrometer-registry-prometheus,SpringBoot会自动配置一个PrometheusMeterRegistry和CollectorRegistry来收集和输出格式化的metrics数据供prometheus服务抓取。参考步骤如下:
1)pom中引入依赖
<!--Actuator提供了生产级别的功能,比如健康检查,审计,指标收集,HTTP 跟踪等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--Micrometer将actuator metrics转换为prometheus可识别格式 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
2)Actuator配置
#按需暴露endpoints,默认为health,*为全部
management.endpoints.web.exposure.include=prometheus
#指定端口,默认为server.port,安全起见可自定义,可整合 Spring Security进行安全校验
management.server.port=8090
#访问路径,默认为/actuator,安全起见可以自定义
management.endpoints.web.base-path=/monitor
完成上述步骤,启动服务,即可通过http访问Actuator查看暴露指标,默认情况包括常规的系统运行指标、jvm指标、tomacat指标以及jdbc和Hikaricp连接池等指标。 最后在prometheus 中配置一个job进行采集按需使用,可重点关注hikaricp_connections_pending(获取连接排队线程数)。
图18:Hikaricp prometheus指标
4、优化实践
4.1、案例现状
某业务平台关系型数据库选型为阿里云RDS mysql5.7版本,默认wait_timeout为86400s(24h),支持最大连接数max_connections为10532,所有分库都在该mysql实例上,具体情况如下:
-
业务a库
-
业务b库
-
业务c库
-
业务d库
-
业务e_0-31库(按业务覆盖省份水平拆分32个库)
服务端组件分布在4C16G的阿里云ECS上双节点部署,数据库操作在不同的dubbo服务中闭环,连接池选型为Druid,具体情况如下:
组件 | 节点数 | 数据源 | initial Size | max Active | minIdle | minEvictable IdleTimeMillis | timeBetween EvictionRunsMillis | validation QueryTimeout:s |
---|---|---|---|---|---|---|---|---|
a-service | 2 | 1 | 50 | 1500 | 20 | 1800000 | 1800000 | 30 |
b-service | 2 | 34 | 50 | 1500 | 20 | 1800000 | 1800000 | 30 |
c-service | 2 | 33 | 20 | 1500 | 20 | 1800000 | 1800000 | 30 |
d-service | 2 | 33 | 20 | 5000 | 20 | 1800000 | 1800000 | 30 |
e-service | 2 | 1 | 20 | 50 | 20 | 1800000 | 1800000 | 30 |
4.2、优化思路
上述可见druid配置完全与业务平台的现状和软硬件环境不相符,赋值随心所欲。所有服务启动后就会产生2*50+2*34*50+2*33*20+2*33*20+2*20=6240个初始连接,最大连接数赋值更夸张,远超数据库本身限制。经查数据库自上次重启以来Max Used Connections恰好为6.24K,基本断定该阶段用到的最大瞬时连接不足6240,存在资源浪费。
图19:MySQL Connections
基于当前的服务器配置,假设真实最大使用连接达到6240,也会存在频繁的线程切换问题,这种情况可以增加服务节点和数据库实例。当前应用服务器和数据库都不存在瓶颈,暂且按数据库支持最大连接的80%来限制所有数据源最大连接总和,另外mysql自身的连接超时时间wait_timeout调整为1805s(30min+5sec)。对应的连接池参数优化如下(同时附hikaricp供对比):
Druid-1.2.21 | HikariCP-4.0.3 | |
初始连接 | initialSize:5 | / |
最小空闲连接 | minIdle:5 | minimumIdle:5 |
最大连接 | maxActive:40 | maximumPoolSize:40 |
连接最小空闲时间(单位ms) | minEvictableIdleTimeMillis:600000 | / |
连接最大闲置时间(单位ms) | maxEvictableIdleTimeMillis:1800000 | idleTimeout:600000 |
连接最大存活时间(单位ms) | / | maxLifetime:1800000 |
|
|
|
|
|
|
是否开启异步探活 | keepAlive:true | / |
探活频率(单位ms) | keepAliveBetweenTimeMillis:60000 | keepaliveTime:60000 |
启用mysql ping方式探活 | usePingMethod:true | 不配置connectionTestQuery |
专栏文章推荐: