HikariCP
database
- What is HikariCP
- Down the Rabbit hole
- About Pool sizing
- With Spring Boot
- Connection Pool
- Configuration Hikaricp Connection Pool
- Links
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 처리 가능
- 동시 커넥션 개수 = 목표 TPS / (1초 / 쿼리 실행 시간)
- 최대 커넥션 개수 고려 사항
- 평균 이상으로 실행 시간이 튀는 개수나 비율 검토
- 0.1초가 평균인데 1초 걸리는 쿼리가 순간적으로 발생하면?
- 커넥션 풀 최대 개수가 10개일 때 TPS 는 100 -> 55로 떨어짐
- 1초/1초 * 5개 요청: 5 TPS
- 1초/0.1초 * 5 요청: 50 TPS
- 느린 쿼리를 염두하고 최대 개수를 높여야 함
- 느린 쿼리의 발생 개수와 빈도를 고려, 부하 테스트 같은 것을 하면 조금 더 명확한 값을 얻을 수 있다.
connectionTimeout
- 커넥션 풀에서 커넥션을 구하기 위해 대기하는 시간
- 커넥션 풀의 모든 커넥션이 사용중일 때 대기 발생
- 고려 사항
- 기본 값은 30초: 너무 큼
- 순간적인 트래픽 증가 시 스레드 풀 기반 WAS 는 모든 스레드가 대기할 수 있음
- 따라서, 사용자는 응답 없는 상태 지속
- 기본 값 대신 0.5 ~ 3초 이내로 설정
- 응답 없는 것보다는 빨리 에러 화면이라도 응답주는게 나음
- 기본 값은 30초: 너무 큼
maxLifetime
- 커넥션의 최대 유지 시간
- 커넥션을 생성한 이후 이 시간이 지나면 커넥션을 닫고 풀에서 제거
- 제거한 뒤 커넥션을 새로 생성
- 기본 규칙
- 네트워크나 DB 의 관련 설정 값보다 작은 값 사용
- 그래야 네트워크가 끊기 면서 발생하는 에러를 발생시키지 않을 것이다.
- 관련 설정 예: 네트워크 장비의 최대 TCP 커넥션 유지 시간
- 네트워크나 DB 의 관련 설정 값보다 작은 값 사용
- 이 값이 관련 설정보다 크면
- 이미 유효하지 않은 커넥션이 풀에 남게 됨
- 풀에서 유효하지 않은 커넥션을 구하는 과정에서 커넥션을 새로 생성
- 트래픽이 몰리는 시점일 경우 성능 저하 유발
keepaliveTime
- 커넥션이 살아 있는지 확인하는 주기
- 유휴 커넥션에 대해 커넥션 확인
- 유효하지 않은 커넥션은 풀에서 제거
- 제거한 뒤 커넥션을 새로 생성
- 기본 규칙
- 네트워크나 DB 의 관련 설정 값보다 작은 값 사용
- 관련 설정 예: DB 의 미활동 커넥션 대기 시간
- 네트워크나 DB 의 관련 설정 값보다 작은 값 사용
minimumIdle
- 커넥션 풀에서 유지할 최소 유효 커넥션 개수
- 설정하지 않으면 maximumPoolSize 와 동일
- 기본 규칙
- Hikari Docs: 설정하지 않는 것을 추천
- 즉, maximumPoolSize 와 동일 크기를 추천(= 고정 크기 풀 추천)
- 이 값이 작으면 급격한 트래픽 증가 시 성능 저하 일으킬 가능성
- 설정할 경우 다음 고려
- 트래픽이 서서히 증가 -> 최소 TPS 기준
- 트래픽이 특정 시점에 급격히 증가 -> 설정하지 말 것
- Hikari Docs: 설정하지 않는 것을 추천
- 트래픽 적은 시간대 DB 자원 사용을 줄이기 위함
idleTimeout
- 최대 유휴 시간: 사용되지 않고 풀에 머무를 수 있는 시간
- 풀에서 이 시간동안 머무른 커넥션은 종료하고 풀에서 제거
- minimumIdle < maximumPoolSize 인 경우에 적용
- 이 시간이 지났다고 바로 빠지진 않음 (Hikari Docs: 15초+)
- 기본 규칙
- 트래픽이 빠지는 시간 간격