자바생
article thumbnail
Published 2022. 10. 27. 01:43
HikariCP란 무엇일까? Spring
728x90

글을 쓰게 된 이유

 

 

 

항상 Spring Boot를 실행하면 HikariPool start, shutdown과 같은 로그를 보신 적이 있으실겁니다.

 

 

매번 볼 때마다 뭐지? 하며 궁금해했지만, 영한님 DB 강의를 듣다가 hikari가 커넥션 풀 오픈소스 라는 것을 알게 되었고,

 

 

더 깊게 공부해보고자 글을 작성하게 됐습니다.

 

 

 

 

 

Connection pool이 무엇인가요?

 

 

WAS에선 DB 연결을 위해 Connection을 해야합니다.

 

 

하지만 Connection을 하기 위해서는 TCP/IP 통신으로 매 연결마다 많은 리소스를 필요로 하기 때문에 미리 Connection을 가진 객체들을 Connection pool에 보관했다가 연결을 필요로 하면 pool에 있는 Connection 객체를 사용합니다.

 

 

 

그래서 따로 Connection을 하지 않고 Connection pool에 있는 Connection 객체를 사용하기 때문에 애플리케이션 서버의 퍼포먼스를 향상시킬 수 있습니다.

 

 

 

Test

 

 

 

@Test
void hikariTest() throws Exception {
    HikariDataSource dataSource = new HikariDataSource();

    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/receipt?characterEncoding=UTF-8&serverTimezone=UCT");
    dataSource.setUsername("root");
    dataSource.setPassword("1234");
    dataSource.setPoolName("java-saeng");

    Connection con1 = dataSource.getConnection();
    Connection con2 = dataSource.getConnection();
    System.out.println("con1 = " + con1 + " " + con1.getClass());
    System.out.println("con2 = " + con2 + " " + con2.getClass());

    Thread.sleep(1000);
}

 

 

 

DataSource가 무엇인가요?

 

 

DataSource 인터페이스는 connection을 얻기 위한 방법을 추상화한 것을 의미합니다.

 

 

DB connection pool 오픈 소스는 여러 가지가 있지만

connection pool에서 connection을 얻는 방법이 오픈 소스마다 다르기 때문에 변화에 따라 애플리케이션 코드도 변화를 줍니다.

 

 

이를 해결하기 위해 java에서는 connection을 얻기 위한 방법을 한 단계 더 추상화하여 DataSource 인터페이스를 만들었고, 개발자는 DataSource만 알면 됩니다.

 

 

 

HikariDataSource dataSource = new HikariDataSource()

 

 

 

HikariDataSource 객체를 생성하고, DB 연결을 위한 값들을 넣어줍니다.

 

 

setPoolName()은 connection pool 이름을 설정하는 옵션입니다.

 

 

따로 설정하지 않는 값들은 default 값이 들어가고, HikariPool의 default connection pool 개수는 10개입니다.

 

 

 

 

dataSource.getConnection()

 

 

HikariDataSource.java

 

 

 

@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; //private volatile HikariPool 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();
}

 

 

위 코드는 HikariDataSource의 getConnection 메서드입니다.

 

 

주석 부분부터 보면 HikariPool는 double-checked-locking 방법을 통해 객체를 얻습니다.

 

 

double-checked-locking 방법은 간단하게 말씀드리자면

 

 

싱글톤 객체를 생성하는 방법 중에 getInstance 메서드가 synchronized 되기 때문에 멀티 스레딩이지만 단일 스레딩처럼 액세스되어

성능이 저하되는 것을 해결하기 위해서 사용하는 방법입니다.

 

그래서 캐시의 일관성을 위해 volatile 변수를 사용합니다.

 

 

 

(double-checked-locking, volatile 은 따로 정리해두었습니다)

 

 

 

validate()

 

validate 메서드는 HikariConfig.java에서 HikariPool 에 대해 config 초기화 및 유효성 검사를 하게 됩니다.

 

 

그래서 Hikari property 값들을 로그에 남깁니다.

 

더보기
23:09:05.450 [main] DEBUG com.zaxxer.hikari.HikariConfig - java-saeng - configuration:
23:09:05.461 [main] DEBUG com.zaxxer.hikari.HikariConfig - allowPoolSuspension................................false
23:09:05.461 [main] DEBUG com.zaxxer.hikari.HikariConfig - autoCommit................................true
23:09:05.461 [main] DEBUG com.zaxxer.hikari.HikariConfig - catalog................................none
23:09:05.462 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionInitSql................................none
23:09:05.462 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionTestQuery................................none
23:09:05.462 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionTimeout................................30000
23:09:05.462 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSource................................none
23:09:05.462 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceClassName................................none
23:09:05.462 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceJNDI................................none
23:09:05.465 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceProperties................................{password=<masked>}
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - driverClassName................................none
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - exceptionOverrideClassName................................none
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - healthCheckProperties................................{}
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - healthCheckRegistry................................none
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - idleTimeout................................600000
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - initializationFailTimeout................................1
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - isolateInternalQueries................................false
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - jdbcUrl................................jdbc:mysql://localhost:3306/receipt?characterEncoding=UTF-8&serverTimezone=UCT
23:09:05.466 [main] DEBUG com.zaxxer.hikari.HikariConfig - keepaliveTime................................0
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - leakDetectionThreshold................................0
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - maxLifetime................................1800000
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - maximumPoolSize................................10
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - metricRegistry................................none
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - metricsTrackerFactory................................none
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - minimumIdle................................10
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - password................................<masked>
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - poolName................................"java-saeng"
23:09:05.467 [main] DEBUG com.zaxxer.hikari.HikariConfig - readOnly................................false
23:09:05.468 [main] DEBUG com.zaxxer.hikari.HikariConfig - registerMbeans................................false
23:09:05.468 [main] DEBUG com.zaxxer.hikari.HikariConfig - scheduledExecutor................................none
23:09:05.468 [main] DEBUG com.zaxxer.hikari.HikariConfig - schema................................none
23:09:05.469 [main] DEBUG com.zaxxer.hikari.HikariConfig - threadFactory................................internal
23:09:05.469 [main] DEBUG com.zaxxer.hikari.HikariConfig - transactionIsolation................................default
23:09:05.469 [main] DEBUG com.zaxxer.hikari.HikariConfig - username................................"root"
23:09:05.469 [main] DEBUG com.zaxxer.hikari.HikariConfig - validationTimeout................................5000

 

 

pool = result = new HikariPool(this)

 

 

처음 Connection pool을 생성할 때는 HikariPool 객체를 생성해야합니다.

 

하지만 다음 getConnection부터는 HikariPool 객체를 생성하지 않습니다.

 

위에서 double-checked-locking을 통해 HikariPool 객체는 싱글톤으로 관리되기 떄문입니다.

 

그래서 다음 getConnection 부터는 if 문에 들어가지 않고 바로 result.getConnection() 을 return 합니다.

 

 

 

HikariPool은 HikariConfig를 파라미터로 받는 생성자가 있습니다.

 

 

 

PoolBase.java

 

private void initializeDataSource() {
  final String jdbcUrl = config.getJdbcUrl();
  final String username = config.getUsername();
  final String password = config.getPassword();
  final String dsClassName = config.getDataSourceClassName();
  final String driverClassName = config.getDriverClassName();
  final String dataSourceJNDI = config.getDataSourceJNDI();
  final Properties dataSourceProperties = config.getDataSourceProperties();

  DataSource ds = config.getDataSource();
  if (dsClassName != null && ds == null) {
     ds = createInstance(dsClassName, DataSource.class);
     PropertyElf.setTargetFromProperties(ds, dataSourceProperties);
  }
  else if (jdbcUrl != null && ds == null) {
     ds = new DriverDataSource(jdbcUrl, driverClassName, dataSourceProperties, username, password);
  }
  else if (dataSourceJNDI != null && ds == null) {
     try {
        InitialContext ic = new InitialContext();
        ds = (DataSource) ic.lookup(dataSourceJNDI);
     } catch (NamingException e) {
        throw new PoolInitializationException(e);
     }
  }

  if (ds != null) {
     setLoginTimeout(ds);
     createNetworkTimeoutExecutor(ds, dsClassName, jdbcUrl);
  }

  this.dataSource = ds;
}

 

 

HikariPool은 PoolBase를 상속하기 때문에 먼저 PoolBase 생성자를 호출하게 됩니다.

 

 

PoolBase에서는 전달받은 config를 통해 기본적인 driverClassName, dataSourceProperties 등 각 property를 설정합니다.

 

 

HikariPool.java

 

 

hikariPool 클래스의 생성자입니다. connection을 어떻게 생성하고 관리하는지 알아보기 위해서 생성자를 천천히 살펴보겠습니다.

 

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

  handleMBeans(this, true);

  ThreadFactory threadFactory = config.getThreadFactory();

  final int maxPoolSize = config.getMaximumPoolSize();
  LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(maxPoolSize);
  this.addConnectionQueueReadOnlyView = unmodifiableCollection(addConnectionQueue);
	//createThreadPoolExecutor
  this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy());
  this.closeConnectionExecutor = createThreadPoolExecutor(maxPoolSize, poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());

  this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);

  //houseKeeper
  this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

  if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1) {
     addConnectionExecutor.setMaximumPoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));
     addConnectionExecutor.setCorePoolSize(Math.min(16, Runtime.getRuntime().availableProcessors()));

     final long startTime = currentTime();
     while (elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle()) {
        quietlySleep(MILLISECONDS.toMillis(100));
     }

     addConnectionExecutor.setCorePoolSize(1);
     addConnectionExecutor.setMaximumPoolSize(1);
  }
}

 

 

checkFailFast 메서드

 

private void checkFailFast() {
  final long initializationTimeout = config.getInitializationFailTimeout();
  if (initializationTimeout < 0) {
     return;
  }

  final long startTime = currentTime();
  do {
     //PoolEntry 객체 생성
     final PoolEntry poolEntry = createPoolEntry();
     if (poolEntry != null) {
        if (config.getMinimumIdle() > 0) {
           //PoolEntry를 보관하는 connectionBag에 넣기
           connectionBag.add(poolEntry);
           logger.debug("{} - Added connection {}", poolName, poolEntry.connection);
        }
        else {
           quietlyCloseConnection(poolEntry.close(), "(initialization check complete and minimumIdle is zero)");
        }

        return;
     }

     if (getLastConnectionFailure() instanceof ConnectionSetupException) {
        throwPoolInitializationException(getLastConnectionFailure().getCause());
     }

     quietlySleep(SECONDS.toMillis(1));
  } while (elapsedMillis(startTime) < initializationTimeout);

  if (initializationTimeout > 0) {
     throwPoolInitializationException(getLastConnectionFailure());
  }
}

 

checkFailFast 메서드는 이름답게 DB와 연결을 되는지 먼저 시도해봅니다.

 

그래서 initializationTimeout 라는 실패 횟수를 나타내는 변수가 < 0 이면 연결이 잘 됐기 때문에 checkFailFast를 하지 않습니다.

 

기본 값은 1이기 때문에 처음에 커넥션을 생성할 때는 기본적으로 하나의 커넥션을 생성합니다.

 

 

// HikariPool.java

private PoolEntry createPoolEntry()
{
  try {
     final PoolEntry poolEntry = newPoolEntry();

     final long maxLifetime = config.getMaxLifetime();
     if (maxLifetime > 0) {
        // variance up to 2.5% of the maxlifetime
        final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0;
        final long lifetime = maxLifetime - variance;
        poolEntry.setFutureEol(houseKeepingExecutorService.schedule(new MaxLifetimeTask(poolEntry), lifetime, MILLISECONDS));
     }

     final long keepaliveTime = config.getKeepaliveTime();
     if (keepaliveTime > 0) {
        // variance up to 10% of the heartbeat time
        final long variance = ThreadLocalRandom.current().nextLong(keepaliveTime / 10);
        final long heartbeatTime = keepaliveTime - variance;
        poolEntry.setKeepalive(houseKeepingExecutorService.scheduleWithFixedDelay(new KeepaliveTask(poolEntry), heartbeatTime, heartbeatTime, MILLISECONDS));
     }

     return poolEntry;
  }
  catch (ConnectionSetupException e) {
     if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
        logger.error("{} - Error thrown while acquiring connection from data source", poolName, e.getCause());
        lastConnectionFailure.set(e);
     }
  }
  catch (Exception e) {
     if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
        logger.debug("{} - Cannot acquire connection from data source", poolName, e);
     }
  }

  return null;
}


// PoolBase.java

PoolEntry newPoolEntry() throws Exception {
  return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit);
}

 

 

 

createPoolEntry → newPoolEntry → new Connection 생성자를 통해 Connection을 생성합니다.

그래서 Connection을 생성하여 PoolEntry 객체를 만들고 이를 반환하여 PoolEntry를 저장하는 connectionBag에 add합니다.

 

 

여기서 PoolEntry란 Connection 인스턴스를 추적하는데 사용된다고 합니다.

 

 

 

 

new Connection 메서드(PoolBase.java)

 

PoolEntry를 생성하기 위해서는 connection을 생성해야하는데, PoolBase에서는 new Connection 메서드를 통해서 생성합니다.

 

 

우리가 알고 있는 주입된 dataSource에서 getConnection을 통해 생성되는 것을 알 수 있습니다.

 

 

 

HikariPool의 inner class

 

HouseKeeper

 

 

Hikari는 HouseKeeper를 통해 connection을 관리합니다. 마지막에 fillPool 메서드를 보면

 

 

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

 

 

 

addConnectionExecutor를 통해서 poolEntryCreator를 통해 connection을 생성합니다.

 

 

 

 

PoolEntryCreator

 

 

 

HikariPool 내부 클래스에 있는 PoolEntryCreator는 PoolEntry를 만들고 풀에 추가하는 역할을 합니다.

 

 

 

 

 

앞서 checkFailFast 메서드에서 본 createPoolEntry 를 통해 PoolEntry를 생성하고 있습니다.

 

 

 

while 조건식을 보면 shouldCreateAnotherConnection() 을 볼 수 있는데, 해당 조건은 다른 idle connection이 있거나 새로운 연결을 기다리는 스레드가 있는 경우에만 연결을 만듭니다.

 

 

 

즉, PoolEntryCreator에서 생성되는 connection은 이미 풀이 있는 상태에서 생성하는 것을 알 수 있습니다.

 

 

 

정리

 

 

 

spring boot에서 기본적으로 사용하는 커넥션 풀 오픈소스인 Hikari를 살펴보았습니다.

 

 

HikariCP는 double-checked-locking을 통해 Hikari 객체를 싱글톤으로 관리하고 있었습니다.

 

 

connection 관리는 inner class인 HouseKeeper를 통해 connection을 조절하고, PoolEntryCreator를 통해 connection을 생성했습니다.

 

 

 

그리고 연결 최적화를 위해 다른 라이브러리를 사용하는 것이 아닌 checkFailFast를 통해 connection을 1개만 만들어봐서 성공 여부를 확인합니다.

 

 

 

 

REFERENCES

스프링 공식문서

영한님 DB 강의

728x90
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그