시냅스

HikariCP 101 : 코드로 알아보는 HikariCP 본문

Java, Spring

HikariCP 101 : 코드로 알아보는 HikariCP

ted k 2023. 4. 1. 18:12

Connection Pool

https://liltdevs.tistory.com/136

 

Java 커넥션 풀과 데이터 소스

커넥션 풀, Connection Pool 데이터베이스 커넥션을 획득할 때마다 tcp/ip 커넥션 (3 way handshake)을 연결하는 네트워크 동작이 발생되어 커넥션을 생성하기 위한 리소스를 매번 사용해야 한다. 커넥션을

liltdevs.tistory.com

Connection pool 은 데이터베이스와의 연결을 효율적으로 관리하기 위한 기술입니다.

미리 생성된 일정 개수의 데이터베이스 커넥션을 재사용하여

데이터베이스에 대한 연결 및 해제에 소요되는 시간과 리소스를 절약하여 전체 성능을 향상시킬 수 있습니다.

 

Hikari CP

HikariCP 는 SpringBoot 2.0 이후부터 default connection pool 로 채택되었습니다.

가볍고 높은 성능을 제공하는 커넥션 풀 라이브러리 입니다.

HikariCP가 어떻게 다른 커넥션 풀보다 빠른 성능을 지닐 수 있는지 아래에서 알아보겠습니다.

 

Hikari CP 가 빠른 이유

  • HikariCP는 매우 경량화되어 있습니다.
    • 코드가 간결하고 단순하며, 작은 라이브러리로 구성되어 있습니다.
    • 따라서 클래스 로딩 및 인스턴스화 시간이 매우 짧아 커넥션 풀의 초기화, 확장에 이점을 가집니다.
    • 자체적인 자료구조를 사용하기도 합니다. (FastList, ConcurrentBag…)
    • 깃허브
  • HikariCP는 자체 스레드 풀을 사용하여 커넥션 확장 및 유지 관리를 병렬로 처리할 수 있습니다.
    • 동시성을 높이고 처리량을 높일 수 있습니다.
  • HikariCP는 커넥션 확장 및 유지 관리에 대한 뛰어난 알고리즘을 사용합니다.
    • 커넥션 풀 내의 커넥션 수를 동적으로 조절하고, 유휴 커넥션을 검사하여 유휴 시간이 지난 커넥션을 종료합니다.
      • connection 이 부족할 때에는 PoolEntryCreator 를 이용하고,
      • connection 이 너무 많을 때에는 HouseKeeper 를 이용합니다.
    • HouseKeeping
    // ...
    
    // idelTimeout 이 정해져잇고, 최소값이 최대 값보다 적은 경우 pool 을 동적으로 조정한다.
    // scheduleWithFixedDelay 를 통해 스케쥴러로 등록되어 있다.
    if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
    	logPoolState("Before cleanup ");
    	final var notInUse = connectionBag.values(STATE_NOT_IN_USE);
    	var maxToRemove = notInUse.size() - config.getMinimumIdle();
    	// 사용하지 않는 connection close
    	for (PoolEntry entry : notInUse) {
    	if (maxToRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
    		 closeConnection(entry, "(connection has passed idleTimeout)");
    		 maxToRemove--;
    	}
    }
    
    // ...
  • HikariCP는 적극적인 커넥션 리사이클링을 사용합니다.
    • 커넥션을 반환할 때 커넥션의 모든 상태를 초기화하고 재사용 가능한 상태로 만듭니다.
    • 커넥션 생성 및 제거에 따른 오버헤드를 크게 줄일 수 있습니다.
    // HikariPool에 connection을 반환하며 recycle 하게 된다.
    @Override
    void recycle(final PoolEntry poolEntry)
    {
    	metricsTracker.recordConnectionUsage(poolEntry);
    	
    	connectionBag.requite(poolEntry);
    }
    
    // ConcurrentBag.java
    public void requite(final T bagEntry)
    {
      // 사용하지 않는 connection 으로 setting
      bagEntry.setState(STATE_NOT_IN_USE);
    
      for (var i = 0; waiters.get() > 0; i++) {
    	 // 만약 connection 을 요구하는 thread 가 있다면 우선적으로 connection 을 반환
         if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
            return;
         }
    		// ...
      }
    
      // 커넥션 정보를 thread local 에 저장한다.
      final var threadLocalList = threadList.get();
      if (threadLocalList.size() < 50) {
         threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
      }
    }
    // 차후 connection 을 빌려올 때 thread local 에 있는 connection 을 먼저 시도한다
    // thread local 에 connection 이 있다는 것은, 가용한 connection 이 있다는 것으로 판단한다.
    public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
    {
      // Try the thread-local list first
      final var list = threadList.get();
      for (int i = list.size() - 1; i >= 0; i--) {
         final var entry = list.remove(i);
         @SuppressWarnings("unchecked")
         final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
         if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
      }
      // handoffQueue 를 체크하고, 이후에는 timeout 시간까지 대기한다. 
    }

 

 

도식화로 보는 Hikari CP Connection borrow/close

 

Connection borrow

내부적으로 ConcurrentBag 이라는 구조체를 이용해 connection 관리합니다.

HikariPool.getConnection() → ConcurrentBag.borrow() 를 사용해 가능한 connection 리턴합니다.

  1. connection.getConnection
  2. 이전에 사용한 Connection 이 idle 이면 해당 connection 반환
  3. 이전에 사용한 내역이 없었고, 사용 가능한 connection 이 있다면 해당 connection 반환
  4. 없다면 handoffQueue 에서 대기
  5. 30초 동안 대기 후 connection 수립 안되면 timeout에러

 

https://techblog.woowahan.com/2664/

 

 

Connection close

connection 이 close 되거나 exception 으로 rollback 될 때 실행되는 flow 입니다.

  1. connection.close → ConcurrentBag.requite()
  2. 해당 커넥션을 idle connection 으로 상태 변경 (poolEntry.setState(STATE_NOT_IN_USE))
  3. 만약 큐에서 커넥션을 대기하는 스레드가 있으면 우선 빌려주고
    1. 커넥션을 빌려주고 커넥션 정보를 threadLocal 에 추가함
  4. 없다면 해당 커넥션 정보를 threadlocal 에 추가함

https://techblog.woowahan.com/2664/

 

 

참고 : Root Transaction

  • Root Transaction 은 Hikari CP 커넥션 풀링 시스템 내부에서 발생하는 트랜잭션입니다.
  • 커넥션 획득, 쿼리 실행, 트랜잭션 처리 등의 모든 작업이 내부적으로 Root Transaction 안에서 수행됩니다.
    • Transactional 을 위해 proxy 가 실행되는 시점에 root transaction 또한 동시에 실행됩니다.
    • 마찬가지로, proxy 가 commit 하거나 rollback 하는 시점에 connection 을 반환하고 동시에 종료됩니다.
    • 때문에 트랜잭션 불일치나 다른 문제가 발생하지 않도록 보장합니다.
  • 또한 root transaction 내부에서 connection 획득에 실패하거나 문제가 생긴다면 hikariCP 가 우선적으로 처리합니다.
Comments