SpringBoot 2.x官方已经宣布使用 HikariCP 作为 SpringBoot 默认的数据库连接池,作为新一代的连接池,HikariCP在性能表现上非常优秀,而且代码非常精简,并发设计也非常值得我们借鉴和学习
初始化过程
核心组件
今天我们首先来分析下 HikariCP 的初始化过程,开始在之前我们先上一张图
上图就是获取连接的大概的逻辑过程,其中涉及以下几个角色,这几个类包含了HikariCP大部分逻辑
HikariDataSource作为 springBoot2.x 启动时默认加载的数据源实现类,提供给上层服务直接使用,持有HikariPool对象HikariPool真正连接池管理类,提供了获取连接、丢弃连接、关闭连接、回收连接等能力,供上层使用,内部持有ConcurrentBag对象ConcurrentBag真正的存在连接的地方,内部持有一个CopyWriteArrayList对象sharedList,另外还通过threadList提供线程级别的连接缓存ProxyFactory生成包装类HikariProxyConnection既然是包装类,肯定是会有一些额外的操作包到了里面,这里还涉及到了Javassist技术,具体逻辑见JavassistProxyFactory
启动流程
先来看服务启动是加载连接池的逻辑
@Bean(name = "agreementDataSource")
@ConfigurationProperties(prefix = "mybatis")
public DataSource agreementDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "readSource")
@ConfigurationProperties(prefix = "mybatis.read")
public DataSource readSource() {
return DataSourceBuilder.create().build();
}
这里只是简单初始化了 Datasource 这个bean, Datasource 内部的 HikariPool 并没有初始化,初始化HikariPool 的逻辑后置到了第一次获取的连接的时候
首次获取连接
@Override
public Connection getConnection() throws SQLException
{
if (isClosed()) {
throw new SQLException("HikariDataSource " + this + " has been closed.");
}
if (fastPathPool != null) {
return fastPathPool.getConnection();
}
// See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
HikariPool result = pool;
if (result == null) {
synchronized (this) {
result = pool;
if (result == null) {
validate();
LOGGER.info("{} - Starting...", getPoolName());
try {
pool = result = new HikariPool(this);
this.seal();
}
catch (PoolInitializationException pie) {
if (pie.getCause() instanceof SQLException) {
throw (SQLException) pie.getCause();
}
else {
throw pie;
}
}
LOGGER.info("{} - Start completed.", getPoolName());
}
}
}
return result.getConnection();
}
第一次请求进来的时候,会判断当前连接池是否为空,如果为空,进行连接池的初始化,这里有两个 HikariPool 对象,volatile 修饰的 pool 导致每次读 pool 都要从主存加载,每次写也要写回主存,性能不如没 volatile 修饰的 fastPathPool ,我们在初始化的使用了无参构造,所以这里使用的是 pool
fastPathPool:final修饰,构造时决定。如果使用无参构造为 null ,使用有参构造和pool一样pool:volatile修饰,无参构造不会设置pool,在getConnection时构造pool,有参构造和fastPathPool一样
初始化连接池
初始化代码如下:
public HikariPool(final HikariConfig config){
super(config);
this.connectionBag = new ConcurrentBag<>(this);
this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
checkFailFast();
if (config.getMetricsTrackerFactory() != null) {
setMetricsTrackerFactory(config.getMetricsTrackerFactory());
}
else {
setMetricRegistry(config.getMetricRegistry());
}
setHealthCheckRegistry(config.getHealthCheckRegistry());
registerMBeans(this);
ThreadFactory threadFactory = config.getThreadFactory();
LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
this.addConnectionQueue = unmodifiableCollection(addConnectionQueue);
this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());
this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS);
}
该方法用于初始化整个连接池,给连接池内所有的属性做初始化的工作
- 利用
config初始化各种连接池属性,并且产生一个用于生产物理连接的数据源DriverDataSource - 初始化存放连接对象的核心类
connectionBag - 初始化一个延时任务线程池类型的对象
houseKeepingExecutorService,用于后续执行一些延时/定时类任务(比如连接泄漏检查延时任务,除此之外maxLifeTime后主动回收关闭连接也是交由该对象来执行的) - 预热连接池,
HikariCP会在该流程的checkFailFast里初始化好一个连接对象放进池子内,当然触发该流程得保证initializationTimeout > 0时(默认值1),这个配置属性表示留给预热操作的时间(默认值1在预热失败时不会发生重试)。与Druid通过initialSize控制预热连接对象数不一样的是,HikariCP仅预热进池一个连接对象。 - 初始化一个线程池对象
addConnectionExecutor, 用于扩充连接对象 - 初始化一个线程池对象
closeConnectionExecutor,用于关闭连接对象
到这里整个初始化就完成了,核心几个类以及对应的属性全部设置完毕,后续就走到了pool.getConnection(),HikariCP获取连接、连接内部的管理后续再做分析
监控相关
最后补充下,HikariCP中监控这一块的内容,在我们使用的使用过程中,要对整个数据库连接池的状态、压力等情况做到实时了解,并且在出现问题,能够及时通知开发人员
HikariCP内置实现了多种监控的实现,如果需要自定义的实现,也可以扩展对应的接口,并且在初始化监控配置时设置进去
public interface MetricsTrackerFactory {
/**
* Create an instance of an IMetricsTracker.
*
* @param poolName the name of the pool
* @param poolStats a PoolStats instance to use
* @return a IMetricsTracker implementation instance
*/
IMetricsTracker create(String poolName, PoolStats poolStats);
}
这里看下几个默认实现的指标
public static final String HIKARI_METRIC_NAME_PREFIX = "hikaricp";
private static final String METRIC_CATEGORY = "pool";
private static final String METRIC_NAME_WAIT = HIKARI_METRIC_NAME_PREFIX + ".connections.acquire";
private static final String METRIC_NAME_USAGE = HIKARI_METRIC_NAME_PREFIX + ".connections.usage";
private static final String METRIC_NAME_CONNECT = HIKARI_METRIC_NAME_PREFIX + ".connections.creation";
private static final String METRIC_NAME_TIMEOUT_RATE = HIKARI_METRIC_NAME_PREFIX + ".connections.timeout";
private static final String METRIC_NAME_TOTAL_CONNECTIONS = HIKARI_METRIC_NAME_PREFIX + ".connections";
private static final String METRIC_NAME_IDLE_CONNECTIONS = HIKARI_METRIC_NAME_PREFIX + ".connections.idle";
private static final String METRIC_NAME_ACTIVE_CONNECTIONS = HIKARI_METRIC_NAME_PREFIX + ".connections.active";
private static final String METRIC_NAME_PENDING_CONNECTIONS = HIKARI_METRIC_NAME_PREFIX + ".connections.pending";
private static final String METRIC_NAME_MAX_CONNECTIONS = HIKARI_METRIC_NAME_PREFIX + ".connections.max";
private static final String METRIC_NAME_MIN_CONNECTIONS = HIKARI_METRIC_NAME_PREFIX + ".connections.min";
hikaricp_connections_pending非常有必要监控- 正在等待连接的线程数量。排查性能问题时,这个指标是一个重要的参考指标,如果正在等待连接的线程在相当一段时间内数量较多,可以考虑扩大数据库连接池的 size
hikaricp_connections_acquire非常有必要监控- 连接获取时间。可以观察时间段内获取连接的耗时
hikaricp_connections_timeout非常有必要监控- 获取连接超时的线程数量。超时数量增加肯定是数据库出了问题(一次数据库操作耗时过久,不能及时归还连接) 或者连接池内的连接数量不够了
hikaricp_connections_active有必要监控- 当前活跃连接数量。根据活跃连接数的增加趋势可以适当调整对应的配置
配置解读
HikariDataSource在初始化pool属性时,会对配置进行校验,如果不合法的配置会在validate后重置为默认值,下面对重要的几个配置做个解读
重要配置
connectionTimeout:- 描述:等待来自池的连接的最大毫秒数
- 默认值:30000ms
validate重置: 如果小于250毫秒,则被重置回30秒- 推荐配置:在池连接不紧张的情况下,默认值即可,在交互频繁的场景可以适当调低,便于监控和调整池大小
idleTimeout:- 描述: 连接允许在池中闲置的最长时间
- 默认值:600000ms
validate重置: 如果idleTimeout+ 1秒 >maxLifetime且maxLifetime> 0,则会被重置为0(代表永远不会退出);如果idleTimeout!= 0且小于10秒,则会被重置为10秒- 推荐配置:此值仅在
minimumIdle定义为小于maximumPoolSize时适用,并且当前池中的连接数要大于minimumIdle时启用,这里用默认配置即可
maxLifetime:- 描述:池中连接最长生命周期
- 默认值: 1800000ms
validate重置: 如果不等于0且小于30秒则会被重置回30分钟- 推荐配置:这里最好设置为比数据库超时
wait_timeout小一点点,避免出现数据库层面已经超时,但是连接池中的连接还是存活的
minimumIdle:- 描述:池中维护的最小空闲连接数
- 默认值:10
validate重置:minIdle< 0或者minIdle>maxPoolSize, 则被重置为maxPoolSize- 推荐配置:看网上资料都是推荐设置和
maximumPoolSize保持一致,这样最大可能较少因为扩容和缩容导致的获取连接阻塞,不过实践中发现必要不大。这个可以结合线上连接池监控的表现,最好保持这个配置大于能保证在峰值流量的下的activie_connection值,像我们服务中高峰期单机活跃连接数也就是10左右,所以配置10~20即可
maximumPoolSize:- 描述:池中最大连接数,包括闲置和使用中的连接
- 默认值:10
validate重置: 如果maxPoolSize小于1,则会被重置。当minIdle<= 0 被重置为DEFAULT_POOL_SIZE则为10; 如果minIdle> 0则重置为minIdle的值- 推荐配置:为了保证极端流量的下连接池能扛住,可以设置这个值数倍于
minimumIdle,当然也要考虑数据库本身连接上限,单库多实例部署的情况下,所有实例连接数小于数据库的上限
总结
HikariCP的初始化,默认在获取第一次连接的时候完成初始化HikariCP的一些重要监控指标,做到对连接池线上使用情况心中有数HikariCP的一些重要配置,默认配置结合自己应用服务的实际情况,慢慢调优,保证连接池不成为整个服务中的瓶颈