基于源码分析 HikariCP 常见参数的具体含义

打印 上一主题 下一主题

主题 1736|帖子 1736|积分 5208

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
HikariCP 是现在风头最劲的 JDBC 毗连池,号称性能最佳,SpringBoot 2.0 也将 HikariCP 作为默认的数据库毗连池。
要想用好 HikariCP,理解常见参数的具体含义至关重要。但是对于某些参数,只管官方文档给出了详细解释,很多开发、DBA 读完后还是会感到狐疑。
因此,本文将从源码角度对 HikariCP 中的一些常见参数进行分析,盼望能帮助大家更加清晰地理解这些参数的具体含义。
本文将分析的参数包括:

  • maximumPoolSize
  • minimumIdle
  • connectionTimeout
  • idleTimeout 及空闲毗连的清理逻辑。
  • maxLifetime
  • keepaliveTime
  • connectionTestQuery 及毗连有效性检测的实现逻辑。
  • leakDetectionThreshold
  • 什么时候会检测毗连的有效性?
maximumPoolSize

毗连池可以创建的最大毗连数,包括空闲和活动毗连。默认值为 10。
  1. if (maxPoolSize < 1) {
  2.    maxPoolSize = DEFAULT_POOL_SIZE;
  3. }
复制代码
毗连池中的空闲毗连是指当前没有被使用、处于空闲状态的毗连。空闲毗连可以随时被借用(即从毗连池中获取)来进行数据库操作。
注意,空闲毗连在 MySQL 中的状态是Sleep,但不是所有Sleep状态的毗连都是空闲毗连。
空闲毗连的清理逻辑

空闲毗连由 HouseKeeper 定期清理。
HouseKeeper 是 HikariCP 中的一个定时使命,负责清理空闲毗连、调解毗连池大小等。
HouseKeeper 会在启动后 100 毫秒执行第一次使命,然后每隔 housekeepingPeriodMs 毫秒执行一次。
housekeepingPeriodMs 的值由com.zaxxer.hikari.housekeeping.periodMs决定,默认是 30000 毫秒(30秒)。
  1. public Connection getConnection() throws SQLException
  2. {
  3.    return getConnection(connectionTimeout);
  4. }

  5. public Connection getConnection(final long hardTimeout) throws SQLException
  6.    {
  7.       suspendResumeLock.acquire();
  8.       finalvar startTime = currentTime();

  9.       try {
  10.          var timeout = hardTimeout;
  11.          do {
  12.             var poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
  13.          ...

  14.    }
复制代码
下面我们看看 HouseKeeper 使命具体的实现逻辑。
  1. if (minIdle < 0 || minIdle > maxPoolSize) {
  2.    minIdle = maxPoolSize;
  3. }
复制代码
可以看到,空闲毗连能回收的前提是 idleTimeout 大于 0,且 minIdle 小于 maxPoolSize。
如果按照官方建议不显式设置 minIdle 的话,则 minIdle 会取 maxPoolSize 的值,此时空闲毗连将不会被回收。
无论是否回收空闲毗连,最后都会调用 fillPool 来填充毗连池,以确保池中有足够的毗连。
空闲毗连的连续时长是通过elapsedMillis(entry.lastAccessed, now)计算的,此中 entry.lastAccessed 记录了毗连最后一次被访问的时间。该时间戳会在以下两种场景下设置:

  • 创建物理毗连时:当一个新的毗连被创建并参加毗连池时,lastAccessed 会被设置为当前时间,表现毗连的创建时间。
  • 毗连归还给毗连池时:当毗连被归还给毗连池时,lastAccessed 会更新为归还时的时间。
因此,空闲毗连的连续时长实际上等于当前系统时间减去毗连最后一次归还给毗连池的时间。
maxLifetime

毗连池中毗连的最大生命周期,单位为毫秒。默认值为 1800000(30分钟),最小答应值是 30000(30秒)。
  1. private synchronized void fillPool(final boolean isAfterAdd)
  2. {  
  3.    // 获取当前空闲连接数
  4.    finalvar idle = getIdleConnections();
  5.    // 检查是否需要创建新连接,创建新连接的条件是总连接数小于 maximumPoolSize 且空闲连接数小于 minimumIdle。
  6.    finalvar shouldAdd = getTotalConnections() < config.getMaximumPoolSize() && idle < config.getMinimumIdle();

  7.    if (shouldAdd) {
  8.       // 计算需要创建的连接数
  9.       finalvar countToAdd = config.getMinimumIdle() - idle;
  10.       for (int i = 0; i < countToAdd; i++)
  11.          addConnectionExecutor.submit(isAfterAdd ? postFillPoolEntryCreator : poolEntryCreator);
  12.    }
  13.    elseif (isAfterAdd) {
  14.       logger.debug("{} - Fill pool skipped, pool has sufficient level or currently being filled.", poolName);
  15.    }
  16. }
复制代码
如果 maxLifetime 设置为 0,则表现不限定毗连的最大生命周期。
如果 maxLifetime 不等于 0 且小于 30 秒,则会输出警告日志,提示 maxLifetime 设置过短,并将 maxLifetime 设置为默认的最大生命周期 MAX_LIFETIME(即 30 分钟)。
在创建一个新的物理毗连时,会为其设置一个到期执行的使命MaxLifetimeTask,该使命将在毗连的生命周期到期时执行。毗连的生命周期时间等于 maxLifetime 减去一个随机偏移量。
  1. public void setConnectionTimeout(long connectionTimeoutMs)
  2. {
  3.    if (connectionTimeoutMs == 0) {
  4.       this.connectionTimeout = Integer.MAX_VALUE;
  5.    }
  6.    else if (connectionTimeoutMs < SOFT_TIMEOUT_FLOOR) {
  7.       throw new IllegalArgumentException("connectionTimeout cannot be less than " + SOFT_TIMEOUT_FLOOR + "ms");
  8.    }
  9.    else {
  10.       this.connectionTimeout = connectionTimeoutMs;
  11.    }
  12. }
复制代码
当毗连的生命周期(lifetime)到期时,MaxLifetimeTask 会被触发,它会调用 softEvictConnection() 方法尝试驱逐该毗连。如果驱逐乐成,则会调用 addBagItem() 方法判断是否向毗连池中添加新的毗连。
  1. // 如果 idleTimeout 与 maxLifetime 的值过于接近,且 maxLifetime 大于 0,连接池将禁用 idleTimeout,避免设置的超时时间影响连接生命周期。
  2. if (idleTimeout + SECONDS.toMillis(1) > maxLifetime && maxLifetime > 0 && minIdle < maxPoolSize) {
  3.    LOGGER.warn("{} - idleTimeout is close to or more than maxLifetime, disabling it.", poolName);
  4.    idleTimeout = 0;
  5. } // 如果 idleTimeout 小于 10 秒,且 minIdle 小于最大连接数 maxPoolSize,连接池会将 idleTimeout 设置为默认值 IDLE_TIMEOUT(10分钟),避免空闲连接存活时间过短影响池的正常使用。
  6. else if (idleTimeout != 0 && idleTimeout < SECONDS.toMillis(10) && minIdle < maxPoolSize) {
  7.    LOGGER.warn("{} - idleTimeout is less than 10000ms, setting to default {}ms.", poolName, IDLE_TIMEOUT);
  8.    idleTimeout = IDLE_TIMEOUT;
  9. } // 如果连接池已配置为固定大小(即 minIdle == maxPoolSize),并且 idleTimeout 被显式设置,连接池会发出警告,说明该设置无效。
  10. else  if (idleTimeout != IDLE_TIMEOUT && idleTimeout != 0 && minIdle == maxPoolSize) {
  11.    LOGGER.warn("{} - idleTimeout has been set but has no effect because the pool is operating as a fixed size pool.", poolName);
  12. }
复制代码
下面我们看看softEvictConnection()的实现逻辑。
  1. private final long housekeepingPeriodMs = Long.getLong("com.zaxxer.hikari.housekeeping.periodMs", SECONDS.toMillis(30));
  2.    
  3. this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
复制代码
毗连首先会被标记为驱逐状态。
如果调用者是毗连的拥有者,或者毗连的状态可以从 STATE_NOT_IN_USE(未使用)变化为 STATE_RESERVED(已预留),则会调用 closeConnection 销毁该毗连。
需要注意的是,对于正在使用的毗连,仅会将其标记为驱逐状态,而不会销毁,即使其生命周期已经到期。只有当毗连被归还到毗连池时,才会真正执行销毁操作。
下面是毗连归还到毗连池时的实现细节。
  1. private finalclass HouseKeeper implements Runnable
  2.    {
  3.       ...
  4.       public void run()
  5.       {
  6.          try {
  7.             ...
  8.             if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
  9.                logPoolState("Before cleanup ");
  10.                // 获取连接池所有未使用的连接(STATE_NOT_IN_USE)
  11.                finalvar notInUse = connectionBag.values(STATE_NOT_IN_USE);
  12.                // 计算需要清理的连接数 maxToRemove,即当前未使用连接数减去最小空闲连接数。
  13.                var maxToRemove = notInUse.size() - config.getMinimumIdle();
  14.                for (PoolEntry entry : notInUse) {
  15.                   // 如果连接的空闲时间超过 idleTimeout,则关闭该连接。
  16.                   if (maxToRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
  17.                      closeConnection(entry, "(connection has passed idleTimeout)");
  18.                      maxToRemove--;
  19.                   }
  20.                }
  21.                logPoolState("After cleanup  ");
  22.             }
  23.             else
  24.                logPoolState("Pool ");
  25.             // 调用 fillPool(true) 以确保连接池维持最小空闲连接数。
  26.             fillPool(true); // Try to maintain minimum connections
  27.          }
  28.          catch (Exception e) {
  29.             logger.error("Unexpected exception in housekeeping task", e);
  30.          }
  31.       }
  32.    }
复制代码
如果毗连被标记为驱逐状态,则会销毁该毗连。如果毗连未被标记为驱逐,则会执行正常的毗连归还操作。
keepaliveTime

对空闲毗连进行定期心跳检测的时间隔断,单位为毫秒。默认值为 120000(2分钟),最小答应值是 30000(30秒)。
  1. if (maxLifetime != 0 && maxLifetime < SECONDS.toMillis(30)) {
  2.    LOGGER.warn("{} - maxLifetime is less than 30000ms, setting to default {}ms.", poolName, MAX_LIFETIME);
  3.    maxLifetime = MAX_LIFETIME;
  4. }
复制代码
如果 keepaliveTime 不等于 0 且小于 30 秒,则输出警告日志,提示 keepaliveTime 设置过短,并禁用心跳检测(将 keepaliveTime  设置为 0)。
定期检测的目的主要有两个:

  • 检测毗连是否失效。
  • 防止毗连因长时间空闲而被数据库或其他中间层关闭。
在创建新的物理毗连时,会为其设置一个定期执行的使命KeepaliveTask,该使命会在 heartbeatTime 后首次执行,并随后以相同的时间隔断(heartbeatTime)重复执行。heartbeatTime 等于 keepaliveTime 减去一个随机偏移量(variance)。
variance 是最大为 keepaliveTime 的 10% 的随机偏移量。引入该随机偏移量的目的是为了避免所有毗连在同一时刻发送心跳,从而减轻系统资源竞争和负载。
  1. private PoolEntry createPoolEntry()
  2.    {
  3.       try {
  4.          finalvar poolEntry = newPoolEntry(getTotalConnections() == 0);

  5.          finalvar maxLifetime = config.getMaxLifetime();
  6.          if (maxLifetime > 0) {
  7.             // 如果 maxLifetime 大于 10000 毫秒,则生成一个最大为 maxLifetime 的 25% 的随机偏移量
  8.             finalvar variance = maxLifetime > 10_000L ? ThreadLocalRandom.current().nextLong( maxLifetime / lifeTimeVarianceFactor ) : 0L;
  9.             finalvar lifetime = maxLifetime - variance;
  10.             poolEntry.setFutureEol(houseKeepingExecutorService.schedule(new MaxLifetimeTask(poolEntry), lifetime, MILLISECONDS));
  11.          }
  12.          ...
  13.          return poolEntry;
  14.       }
  15.       ...
  16.       returnnull;
  17.    }
复制代码
以下是 KeepaliveTask 的具体实现。
  1. private final class MaxLifetimeTask implements Runnable
  2. {
  3.    ...
  4.    public void run()
  5.    {
  6.       if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
  7.          addBagItem(connectionBag.getWaitingThreadCount());
  8.       }
  9.    }
  10. }
复制代码
connectionTestQuery

用于设置毗连检测语句,默认为 none。
对于支持 JDBC4 的驱动程序,建议不要设置该参数,因为 JDBC4 提供了Connection.isValid()方法来进行毗连有效性检查。
JDBC4 是 Java Database Connectivity (JDBC) 的第 4 版,首次在 Java 6(即 Java 1.6)中引入。因此,只要程序使用的是 Java 1.6 及更高版本,就可以使用isValid()方法。
毗连有效性检测的实现逻辑

如果 connectionTestQuery 为 none,则会将 isUseJdbc4Validation 设置为 true。
  1. private boolean softEvictConnection(final PoolEntry poolEntry, final String reason, final boolean owner)
  2. {
  3.    // 将连接标记为驱逐状态
  4.    poolEntry.markEvicted();
  5.    if (owner || connectionBag.reserve(poolEntry)) {
  6.       closeConnection(poolEntry, reason);
  7.       returntrue;
  8.    }

  9.    returnfalse;
  10. }

  11. void markEvicted()
  12. {
  13.    this.evict = true;
  14. }

  15. public boolean reserve(final T bagEntry)
  16. {
  17.    return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
  18. }
复制代码
isUseJdbc4Validation 会用在两个地方:

  • 判断驱动是否支持connection.isValid方法。
  • 检测毗连是否失效。
检测毗连是否失效是在isConnectionDead中实现的。
  1. void recycle(final PoolEntry poolEntry)
  2. {
  3.    metricsTracker.recordConnectionUsage(poolEntry);
  4.    // 如果连接被标记为驱逐状态,则销毁连接
  5.    if (poolEntry.isMarkedEvicted()) { 
  6.       closeConnection(poolEntry, EVICTED_CONNECTION_MESSAGE);
  7.    } else {
  8.       if (isRequestBoundariesEnabled) {
  9.          try {
  10.             poolEntry.connection.endRequest();
  11.          } catch (SQLException e) {
  12.             logger.warn("endRequest Failed for: {},({})", poolEntry.connection, e.getMessage());
  13.          }
  14.       }
  15.       // 如果连接未被标记为驱逐,将执行正常的连接归还操作
  16.       connectionBag.requite(poolEntry);
  17.    }
  18. }
复制代码
可以看到,如果 isUseJdbc4Validation 为 true,则会调用connection.isValid方法来检测毗连的有效性。否则,系统将使用配置的 connectionTestQuery 来执行 SQL 查询,以检查毗连是否有效。
leakDetectionThreshold

毗连从池中取出后,如果未归还超过一定时间,则会记录日志,提示可能的毗连走漏。默认值为 0,表现禁用走漏检测。
  1. if (keepaliveTime != 0 && keepaliveTime < SECONDS.toMillis(30)) {
  2.    LOGGER.warn("{} - keepaliveTime is less than 30000ms, disabling it.", poolName);
  3.    keepaliveTime = 0L;
  4. }
复制代码
如果 leakDetectionThreshold 小于 2 秒,或者 leakDetectionThreshold 大于毗连池的 maxLifetime,则会发出警告,并将其重置为 0,禁用走漏检测。
实现细节可参考:怎样定位 Druid & HikariCP 毗连池的毗连走漏问题?
什么时候会检测毗连的有效性?

除了通过 KeepaliveTask 定期检查毗连的有效性外,HikariCP 还会在借用毗连时进行有效性检测。
这个检测逻辑在 getConnection 方法中实现。具体来说,在从毗连池借用毗连后,会检查毗连的最后归还时间(poolEntry.lastAccessed)与当前时间的差值是否超过 aliveBypassWindowMs(默认 500 毫秒)。如果超过该时间阈值,则会调用 isConnectionDead(poolEntry.connection) 来检查毗连是否失效。
  1. private PoolEntry createPoolEntry()
  2.    {
  3.       try {
  4.          finalvar poolEntry = newPoolEntry(getTotalConnections() == 0);
  5.          ...
  6.          finallong keepaliveTime = config.getKeepaliveTime();
  7.          if (keepaliveTime > 0) {
  8.             // variance up to 10% of the heartbeat time
  9.             finalvar variance = ThreadLocalRandom.current().nextLong(keepaliveTime / 10);
  10.             finalvar heartbeatTime = keepaliveTime - variance;
  11.             poolEntry.setKeepalive(houseKeepingExecutorService.scheduleWithFixedDelay(new KeepaliveTask(poolEntry), heartbeatTime, heartbeatTime, MILLISECONDS));
  12.          }

  13.          return poolEntry;
  14.       }
  15.       ...
  16.       returnnull;
  17.    }
复制代码
aliveBypassWindowMs 由配置项com.zaxxer.hikari.aliveBypassWindowMs控制,默认值为 500 毫秒。
这一逻辑与其他毗连池中的 testOnBorrow 参数类似,只不过 testOnBorrow 是每次都检查,而 HikariCP 只有在毗连空闲超过 500 毫秒时才会检查。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

郭卫东

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表