HikariCP浅析(一)

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

  • fastPathPoolfinal 修饰,构造时决定。如果使用无参构造为 null ,使用有参构造和 pool 一样
  • poolvolatile 修饰,无参构造不会设置 pool,在 getConnection时构造 pool,有参构造和fastPathPool一样

连接池初始化1

初始化连接池

初始化代码如下:

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);
   }

该方法用于初始化整个连接池,给连接池内所有的属性做初始化的工作

  1. 利用 config 初始化各种连接池属性,并且产生一个用于生产物理连接的数据源 DriverDataSource
  2. 初始化存放连接对象的核心类 connectionBag
  3. 初始化一个延时任务线程池类型的对象 houseKeepingExecutorService,用于后续执行一些延时/定时类任务(比如连接泄漏检查延时任务,除此之外 maxLifeTime 后主动回收关闭连接也是交由该对象来执行的)
  4. 预热连接池,HikariCP 会在该流程的 checkFailFast 里初始化好一个连接对象放进池子内,当然触发该流程得保证 initializationTimeout > 0 时(默认值1),这个配置属性表示留给预热操作的时间(默认值1在预热失败时不会发生重试)。与 Druid 通过 initialSize 控制预热连接对象数不一样的是,HikariCP 仅预热进池一个连接对象。
  5. 初始化一个线程池对象 addConnectionExecutor, 用于扩充连接对象
  6. 初始化一个线程池对象 closeConnectionExecutor,用于关闭连接对象

连接池初始化1

到这里整个初始化就完成了,核心几个类以及对应的属性全部设置完毕,后续就走到了pool.getConnection()HikariCP获取连接、连接内部的管理后续再做分析

监控相关

最后补充下,HikariCP中监控这一块的内容,在我们使用的使用过程中,要对整个数据库连接池的状态、压力等情况做到实时了解,并且在出现问题,能够及时通知开发人员

image-20211022152438772

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秒 > maxLifetimemaxLifetime > 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 的一些重要配置,默认配置结合自己应用服务的实际情况,慢慢调优,保证连接池不成为整个服务中的瓶颈