Backend/Spring+Boot

HikariCP 커넥션 풀

findmypiece 2021. 12. 14. 15:31
728x90

Spring Boot 2.0을 기점으로 Default DBCP가 Tomcat DBCP -> HikariCP로 바뀌었다. 이는 우리가 흔히 알고 있는 DB커넥션 풀인데  둘이 다른 점이 있어 정리 해놓는다.

 

일단 Tomcat DBCP 와 달리 HikariCP 는 유휴 커넥션을 유지시키지 않는다. Tomcat DBCP 에서는 test-while-idle 옵션을 통해 유휴커넥션의 세션이 종료되지 않도록 주기적으로 validationQuery 를 날리는 방식을 사용했는데 HikariCP 에서는 maxLifetime이 지나면  해당 커넥션을 폐기하고 다시 생성하는 방식을 사용한다.

 

이러한 로직은 HikariCP 라이브러리의 HikariPool 내부 클래스인 KeepaliveTask 에서 아래와 같이 확인된다. softEvictConnection 이 커넥션을 폐기하는 메소드이고 addBagItem 이 풀에 다시 커넥션을 생성하는 메소드이다.

private final class KeepaliveTask implements Runnable
{
   private final PoolEntry poolEntry;

   KeepaliveTask(final PoolEntry poolEntry)
   {
      this.poolEntry = poolEntry;
   }

   public void run()
   {
      if (connectionBag.reserve(poolEntry)) {
         if (!isConnectionAlive(poolEntry.connection)) {
            softEvictConnection(poolEntry, DEAD_CONNECTION_MESSAGE, true);
            addBagItem(connectionBag.getWaitingThreadCount());
         }
         else {
            connectionBag.unreserve(poolEntry);
            logger.debug("{} - keepalive: connection {} is alive", poolName, poolEntry.connection);
         }
      }
   }
}

 

하지만 이렇더라도 풀에 있는 커넥션을 아무런 확인 없이 사용할 순 없기 때문에 커넥션에 대한 유효성 검증이 필요하다. HikariCP 에서는 사용하는 JDBC 버전에 체크로직이 상이하다.

 

JDBC4 이전 버전에서는 connectionTestQuery 를 지정해서 사용하지만 이상 버전에서는  JDBC에서 자체적으로 제공되는 검증로직이 사용되기 때문에 connectionTestQuery 가 필요없다. 이는 HikariPool.isConnectionAlive() 에서 아래와 같이 확인된다.

boolean isConnectionAlive(final Connection connection)
{
   try {
      try {
         setNetworkTimeout(connection, validationTimeout);

         final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;

         if (isUseJdbc4Validation) {
            return connection.isValid(validationSeconds);
         }

         try (Statement statement = connection.createStatement()) {
            if (isNetworkTimeoutSupported != TRUE) {
               setQueryTimeout(statement, validationSeconds);
            }

            statement.execute(config.getConnectionTestQuery());
         }
      }
      finally {
         setNetworkTimeout(connection, networkTimeout);

         if (isIsolateInternalQueries && !isAutoCommit) {
            connection.rollback();
         }
      }

      return true;
   }
   catch (Exception e) {
      lastConnectionFailure.set(e);
      logger.warn("{} - Failed to validate connection {} ({}). Possibly consider using a shorter maxLifetime value.",
                  poolName, connection, e.getMessage());
      return false;
   }
}

connection.isValid() 가 바로 그것인데 내부 로직을 살펴보면 먼저 해당 커넥션이 종료되었는지 체크한다. 커넥션이 종료되는 경우는 hikariCP의 maxLifetime이 초과한 경우이다. 만약 커넥션이 닫혀 있지 않다면 실제 유효한 커넥션인지 DB서버에 ping을 날려서 확인한다. 

 

이렇게 체크한 결과 커넥션이 유효하지 않다면 풀에서 다른 커넥션을 가져와서 사용한다. 다만 DB서버 자체가 죽어있는 상태라면 풀에 커넥션이 없거나 모두 유효하지 않을 것이기 때문에 이 경우에는 Exception을 발생시킨다.

 

이에 HA 구성을 위해서는 jdbcUrl에 포함되는 DB서버 도메인은 vip로 구성하고 스위치에서 health check 를 통해 적절한 서버로 요청을 전달해주도록 해주거나 이 처리를 어플리케이션에서 별도로 구현해 주어야 한다.

 

개인적으로 전자가 더 맞는거 같은데 어플리케이션에서 방법이 없는 건 아니다. mysql 에서 제공하는 jdbc:mysql:loadbalance, jdbc:mysql:loadbalance 스키마를 사용하면 특정 서버가 죽어있을 경우 다른 서버의 커넥션을 사용한다.

 

HikariCP 옵션설정은 아래 블로그에 정리가 정말 잘되어 있다.

https://effectivesquid.tistory.com/entry/HikariCP-세팅시-옵션-설명

 

주의해야 할 점은 maximum-pool-size 은 절대 1로 설정하면 안된다. 카프카 컨슈머 같이 싱글스레드 방식에서 태스크당 1개만 필요하더라도 아래 공식에 맞게 줘야 한다.

pool size = Tn x (Cm - 1) + 1
Tn : Thread의 최대 수
Cm: 하나의 Task에서 필요한 Connection 갯수
https://jaehun2841.github.io/2020/01/27/2020-01-27-hikaricp-maximum-pool-size-tuning/#하나의-Task에서-필요한-Connection은-2개인데-왜-Dead-Lock이-걸리죠

 

 

https://pkgonan.github.io/2018/04/HikariCP-test-while-idle
https://pasudo123.tistory.com/468
https://www.jianshu.com/p/e3ad5de8501a
https://gywn.net/2012/07/mysql-replication-driver-error-report/
https://effectivesquid.tistory.com/entry/HikariCP-세팅시-옵션-설명
728x90