Hkari连接的管理
一个告警
某天发现一个流量不大的系统启动时启动时居然产生大量获取数据库连接慢的告警(>50ms),分析系统代码发现系统启动会有将一堆任务提交到线程池中执行,每个任务都会查询数据库,任务总数不到100个,核心线程30,最大200,相当于100个并发查询SQL。系统系统使用的连接池为Hikari,初始连接配置为15,按理100个并发查询并不算很大的流量,为什么会告警呢?于是有了本文
连接的创建
HikariPool实例化的时候将会开启一个定时任务,默认是每间隔3秒进行一个"管家的巡查服务"
public HikariPool(final HikariConfig config)
{
super(config);
...
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
....
}
}
HouseKeeper
其中巡游服务的一个任务就是保证连接池拥有最少的连接的数(也就是我们设置的setMinimumIdle
)
/**
* The house keeping task to retire and maintain minimum idle connections.
*/
private final class HouseKeeper implements Runnable
{
private volatile long previous = plusMillis(currentTime(), -housekeepingPeriodMs);
@Override
public void run()
{
...
fillPool(); // Try to maintain minimum connections
}
catch (Exception e) {
logger.error("Unexpected exception in housekeeping task", e);
}
}
}
fillPool
这里计算了需要添加到连接池中的连接数,其考虑了以下几点:
config.getMaximumPoolSize() - getTotalConnections()
:计算最大连接数与现有连接总数之间的差值。这确保了新添加的连接不会超过最大限制。config.getMinimumIdle() - getIdleConnections()
:计算最小空闲连接数与当前空闲连接数之间的差值。这确保了池中有足够的空闲连接,以便在需要时立即使用。addConnectionQueueReadOnlyView.size()
:这是正在创建但尚未完成的连接数。这部分从上面的差值中减去,避免重复计算。
/**
* Fill pool up from current idle connections (as they are perceived at the point of execution) to minimumIdle connections.
*/
private synchronized void fillPool()
{
final int connectionsToAdd =
Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections())
- addConnectionQueueReadOnlyView.size();
if (connectionsToAdd <= 0) logger.debug("{} - Fill pool skipped, pool is at sufficient level.", poolName);
for (int i = 0; i < connectionsToAdd; i++) {
addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator);
}
}
poolEntryCreator
poolEntryCreator
是连接的创建者,它的实现了Callable,内部会在连接池状态正常&需要创建新的连接时不断完成创建作业
另外这里可以看到的一点时,如果创建连接失败,这里记录日志并休眠一段时间,然后增加重试间隔时间,直到最大重试间隔时间或连接超时时间。
/**
* Creating and adding poolEntries (connections) to the pool.
*/
private final class PoolEntryCreator implements Callable<Boolean>
{
...
@Override
public Boolean call()
{
long sleepBackoff = 250L;
while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
final PoolEntry poolEntry = createPoolEntry();
if (poolEntry != null) {
connectionBag.add(poolEntry);
...
return Boolean.TRUE;
}
...
quietlySleep(sleepBackoff);
sleepBackoff = Math.min(SECONDS.toMillis(10), Math.min(connectionTimeout, (long) (sleepBackoff * 1.5)));
}
...
// Pool is suspended or shutdown or at max size
return Boolean.FALSE;
}
}
shouldCreateAnotherConnection
从这里我们可以创建新的连接有两个条件:存在等待获得连接的线程|空闲线程小于最小空闲线程
private synchronized boolean shouldCreateAnotherConnection() {
return getTotalConnections() < config.getMaximumPoolSize() &&
(connectionBag.getWaitingThreadCount() > 0 || getIdleConnections() < config.getMinimumIdle());
}
回收开头
到这里我们其实很容易解释开头的问题了,由于是项目刚启动,连接池内其实没有任何的连接,此时执行SQL,只能等待连接连接池先创建,这就好比去吃刚开门的餐馆,只能等老板准备好才能烧火做饭。