What is HikariCP

光 HikariCP・A solid, high-performance, JDBC connection pool at last.

HikariCP 는 고성능 JDBC Connection Pool 이다. 2012년경 Brett Wooldridge 가 개발한 매우 가볍고(약 130Kb) 번개처럼 빠른 zero-overhead JDBC 연결 풀링 프레임워크이다.

"Simplicity is prerequisite for reliability." - Edsger Dijkstra

Down the Rabbit hole

HikariCP 는 어떤 방식으로 성능 최적화를 이끌어낼까?

getConnection()

HikariCP 는 getConnection 의 수가 다른 JDBC 에 비해서 적다.

FastList

전체 성능을 향상 시키기 위한 다양한 미세 최적화들이 존재한다. FastList 는 그 중 하나이다.

  • 한 가지 중요하지 않은(성능 면에서) 최적화는 열린 인스턴스를 추적 하는 데 사용되는 ArrayList<Statement> 인스턴스 의 사용을 제거하는 것이었다. 닫히면 이 컬렉션에서 제거되어야 하고 닫힐 때 컬렉션을 반복하고 열려 있는 모든 인스턴스를 닫고 마지막으로 컬렉션을 지워야 한다.
  • 자바의 ArrayList 의 remove() 는 head to tail 로 스캔하지만, JDBC 프로그래밍의 일반적인 패턴은 사용 직후 문을 닫거나 열기의 역순으로 수행하는 것이 효과적이다.
  • 따라서 범위 검사를 제거하고 꼬리에서 머리까지 제거 스캔을 수행 하는 사용자 정의 클래스(FastList)로 대체되었다.

ConcurrentBag

  • HikariCP 에는 ConcurrentBag 라는 사용자 지정 잠금 없는 컬렉션이 포함되어 있다. 아이디어는 C# .NET ConcurrentBag 클래스에서 차용했지만 내부 구현은 상당히 다르다.
  • ConcurrentBag 는 다음을 제공한다.
    • A lock-free design
    • ThreadLocal caching
    • Queue-stealing
    • Direct hand-off optimizations
    • …resulting in a high degree of concurrency, extremely low latency, and minimized occurrences of false-sharing

Invocation: invokevirtual vs invokestatic

  • invokevirtual
    • Connection, Statement 및 ResultSet 인스턴스에 대한 프록시를 생성하기 위해 HikariCP 는 초기에 ConnectionProxy 정적 필드(PROXY_FACTORY)에 유지되는 싱글톤 팩토리를 사용했었다.
    • public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
          return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
      }
      
    • Using the original singleton factory, the generated bytecode looked like this:
    • public final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
      flags: ACC_PRIVATE, ACC_FINAL
      Code:
        stack=5, locals=3, args_size=3
           0: getstatic     #59                 // Field PROXY_FACTORY:Lcom/zaxxer/hikari/proxy/ProxyFactory;
           3: aload_0
           4: aload_0
           5: getfield      #3                  // Field delegate:Ljava/sql/Connection;
           8: aload_1
           9: aload_2
          10: invokeinterface #74,  3           // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
          15: invokevirtual #69                 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
          18: return
      
  • invokestatic
    • We eliminated the singleton factory (which was generated by Javassist) and replaced it with a final class having static methods (whose bodies are generated by Javassist). The Java code became:
    • public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
        return ProxyFactory.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
      }
      
    • Where getProxyPreparedStatement() is a static method defined in the ProxyFactory class. The resulting bytecode is:
    • private final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
      flags: ACC_PRIVATE, ACC_FINAL
      Code:
        stack=4, locals=3, args_size=3
           0: aload_0
           1: aload_0
           2: getfield      #3                  // Field delegate:Ljava/sql/Connection;
           5: aload_1
           6: aload_2
           7: invokeinterface #72,  3           // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
          12: invokestatic  #67                 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
          15: areturn
      
  • What is differences
    • getStatic() 호출이 필요 없어짐
    • The invokevirtual call is replaced with a invokestatic call that is more easily optimized by the JVM.
    • stack size 가 5개에서 4개로 줄어듦. 전체적으로 이 변경으로 인해 스택에서 정적 필드 액세스, 푸시 및 팝이 제거되었으며 호출 사이트가 변경되지 않도록 보장 되므로 JIT 에서 호출을 더 쉽게 최적화할 수 있다.

About Pool sizing

With Spring Boot

Spring Boot 는 기존에 tomcat-jdbc 를 기본 Datasource 로 제공했었는데 2.0부터 HikariCP가 기본으로 변경되었다.

Connection Pool

커넥션 풀을 사용하는 목적은 다음과 같다.

  • 처리량 증가
    • DB 와 네트워크 연결하는 시간을 단축하여, 응답 시간을 단축하고 이로 인한 처리량 증가
  • 일관된 DB 성능 유지
    • DB 에 대한 커넥션 개수를 일정 수준으로 제한하여 DB 포화를 방지하고 이로 인한 일관된 DB 성능 유지

커넥션 풀을 올바르게 설정하지 않으면 오히려 성능 문제를 유발할 수 있다.

Configuration Hikaricp Connection Pool

maximumPoolSize

  • 최대 커넥션 개수
    • 커넥션 풀이 제공할 수 있는 최대 커넥션 개수
    • ACTIVE + IDLE
  • 계산에 필요한 항목
    • 한 커넥션 당 쿼리 실행 시간, 최대(목표) TPS
  • 단순 계산 식
    • 최대 TPS = 1개 커넥션의 초당 처리 요청 개수 * 동시 커넥션 개수
    • 동시 커넥션 개수 = 최대 TPS / 1개 커넥션의 초당 요청 처리 개수
    • 동시 커넥션 개수 = 최대 TPS / (1초 / 쿼리 실행 시간)
  • 예시
    • 하나의 웹 요청이 한 개의 커넥션을 사용하고, 한 웹 요청이 실행하는 쿼리 총 실행 시간은 0.1 초
    • 목표는 100 TPS
    • 동시에 필요한 커넥션 개수를 단순 계산
      • 동시 커넥션 개수 = 목표 TPS / (1초 / 쿼리 실행 시간)
        • 100 / (1초 / 0.1초) = 10
      • 즉, 커넥션 풀 최대 개수가 10이고 평균 0.1초 소요되면 100TPS 처리 가능
  • 최대 커넥션 개수 고려 사항
    • 평균 이상으로 실행 시간이 튀는 개수나 비율 검토
    • 0.1초가 평균인데 1초 걸리는 쿼리가 순간적으로 발생하면?
    • 커넥션 풀 최대 개수가 10개일 때 TPS 는 100 -> 55로 떨어짐
      • 1초/1초 * 5개 요청: 5 TPS
      • 1초/0.1초 * 5 요청: 50 TPS
    • 느린 쿼리를 염두하고 최대 개수를 높여야 함
      • 느린 쿼리의 발생 개수와 빈도를 고려, 부하 테스트 같은 것을 하면 조금 더 명확한 값을 얻을 수 있다.

connectionTimeout

  • 커넥션 풀에서 커넥션을 구하기 위해 대기하는 시간
    • 커넥션 풀의 모든 커넥션이 사용중일 때 대기 발생
  • 고려 사항
    • 기본 값은 30초: 너무 큼
      • 순간적인 트래픽 증가 시 스레드 풀 기반 WAS 는 모든 스레드가 대기할 수 있음
      • 따라서, 사용자는 응답 없는 상태 지속
    • 기본 값 대신 0.5 ~ 3초 이내로 설정
      • 응답 없는 것보다는 빨리 에러 화면이라도 응답주는게 나음

maxLifetime

  • 커넥션의 최대 유지 시간
    • 커넥션을 생성한 이후 이 시간이 지나면 커넥션을 닫고 풀에서 제거
    • 제거한 뒤 커넥션을 새로 생성
  • 기본 규칙
    • 네트워크나 DB 의 관련 설정 값보다 작은 값 사용
      • 그래야 네트워크가 끊기 면서 발생하는 에러를 발생시키지 않을 것이다.
      • 관련 설정 예: 네트워크 장비의 최대 TCP 커넥션 유지 시간
  • 이 값이 관련 설정보다 크면
    • 이미 유효하지 않은 커넥션이 풀에 남게 됨
    • 풀에서 유효하지 않은 커넥션을 구하는 과정에서 커넥션을 새로 생성
    • 트래픽이 몰리는 시점일 경우 성능 저하 유발

keepaliveTime

  • 커넥션이 살아 있는지 확인하는 주기
    • 유휴 커넥션에 대해 커넥션 확인
    • 유효하지 않은 커넥션은 풀에서 제거
    • 제거한 뒤 커넥션을 새로 생성
  • 기본 규칙
    • 네트워크나 DB 의 관련 설정 값보다 작은 값 사용
      • 관련 설정 예: DB 의 미활동 커넥션 대기 시간

minimumIdle

  • 커넥션 풀에서 유지할 최소 유효 커넥션 개수
    • 설정하지 않으면 maximumPoolSize 와 동일
  • 기본 규칙
    • Hikari Docs: 설정하지 않는 것을 추천
      • 즉, maximumPoolSize 와 동일 크기를 추천(= 고정 크기 풀 추천)
      • 이 값이 작으면 급격한 트래픽 증가 시 성능 저하 일으킬 가능성
    • 설정할 경우 다음 고려
      • 트래픽이 서서히 증가 -> 최소 TPS 기준
      • 트래픽이 특정 시점에 급격히 증가 -> 설정하지 말 것
  • 트래픽 적은 시간대 DB 자원 사용을 줄이기 위함

idleTimeout

  • 최대 유휴 시간: 사용되지 않고 풀에 머무를 수 있는 시간
    • 풀에서 이 시간동안 머무른 커넥션은 종료하고 풀에서 제거
    • minimumIdle < maximumPoolSize 인 경우에 적용
    • 이 시간이 지났다고 바로 빠지진 않음 (Hikari Docs: 15초+)
  • 기본 규칙
    • 트래픽이 빠지는 시간 간격