HikariCP 커넥션 풀
HikariCP 이해 및 커넥션 풀 설정 최적화하기
Connection Pool
애플리케이션에서 DB를 사용할 때 연결에 필요한 JVM GC 처리나, TCP 연결 생성/종료에 따른 I/O 처리와 같은 DB 연결 비용이 든다. 매번 새롭게 DB를 연결하기 위해 소모되는 비용을 줄이기 위해 커넥션 풀(Connection Pool)을 사용한다. 이름에 알 수 있듯이 미리 Connection 객체(DB와 연결한 객체)를 모아둔 컨테이너로 객체를 재사용하고, 사용 가능한 객체가 없을 때만 (최대 연결 객체 크기 미만인 경우) 새로운 연결 객체를 만드는 방식으로 비용을 줄인다.
Pooling vs No Pooling
Pooling 사용 여부에 따른 실행 시간과 네트워크 트래픽을 모니터링해 비교해봤다.
Pooling
1
connectionpool.PoolingVsNoPoolingTest-Pooling -- Elapsed runtime: 1s93ms
1 2 3
TX: cum: 873KB peak: 1.95Mb rates: 416b 83b 249Kb RX: 0B 0b 0b 0b 0b TOTAL: 873KB 1.95Mb 416b 83b 249Kb
No Pooling
1
connectionpool.PoolingVsNoPoolingTest-NoPoolng -- Elapsed runtime: 6s333ms
1 2 3
TX: cum: 8.79MB peak: 11.8Mb rates: 0b 6.94Mb 2.71Mb RX: 0B 0b 0b 0b 0b TOTAL: 8.79MB 11.8Mb 0b 6.94Mb 2.71Mb
1. 실행 시간
- Pooling:
1s 93ms - No Pooling:
6s 333ms
Pooling을 사용했을 때 약 6배 더 빨랐다. Pooling은 이미 만들어진 연결을 재사용하므로 작업 시작 시간이 거의 즉각적인 반면, No Pooling은 매번 DB와 연결을 설정하는 과정을 거쳐야 해서 훨씬 느렸다.
2. 총 네트워크 트래픽
- Pooling:
873 KB - No Pooling:
8.79 MB
Pooling을 사용하지 않았을 때 약 10배 더 많은 네트워크 데이터를 전송했다. 연결을 새로 만들 때마다 DB 서버와 인증 및 설정을 위한 통신(Handshake)으로 인해 네트워크 트래픽이 발생했다. Pooling은 처음에 만들어진 연결을 계속 재사용하므로 이 오버헤드가 거의 없었다.
3. 네트워크 패턴
- Pooling: Peak
1.95Mb/s, 전반적으로 낮은 rates (83b) - No Pooling: Peak
11.8Mb/s, 훨씬 높은 rates (6.94Mb)
Pooling은 일단 연결이 수립되고 나면, 실제 데이터를 주고받는 작업은 훨씬 가볍고 효율적으로 낮은 peak와 rates가 나타났다. 반면 No Pooling은 매번 연결을 맺는 작업이 네트워크 대역폭을 폭발적으로 사용하는 무거운 작업을 수행해 높은 peak와 rates가 나타났다.
Connection Pool 사용하기
Spring Docs를 살펴보면 Spring Boot은 HikariCP를 기본으로, 아래와 같은 HikariCP → Tomcat → DBCP2 순서로 Connection Pool 구현체를 선택한다.
https://docs.spring.io/spring-boot/reference/data/sql.html#data.sql.datasource.connection-pool
Supported Connection Pools
Spring Boot uses the following algorithm for choosing a specific implementation:
- We prefer HikariCP for its performance and concurrency. If HikariCP is available, we always choose it.
- Otherwise, if the Tomcat pooling
DataSourceis available, we use it.- Otherwise, if Commons DBCP2 is available, we use it.
- If none of HikariCP, Tomcat, and DBCP2 are available and if Oracle UCP is available, we use it.
If you use the
spring-boot-starter-jdbcorspring-boot-starter-data-jpastarters, you automatically get a dependency to HikariCP.
HikariCP
HikariCP Configuration
https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby
Essential
dataSourceClassNameThis is the name of the
DataSourceclass provided by the JDBC driver. Note that you do not need this property if you are usingjdbcUrlfor “old-school” DriverManager-based JDBC driver configuration.jdbcUrlThis property directs HikariCP to use “DriverManager-based” configuration. When using this property with “old” drivers, you may also need to set the
driverClassNameproperty, but try it first without.usernameThis property sets the default authentication username used when obtaining Connections from the underlying driver.
passwordThis property sets the default authentication password used when obtaining Connections from the underlying driver.
Connection Pool 적용하기
Connection Pool의 Connection 생명 주기, Connection Pool 크기, Connection 획득 상태를 설정할 수 있다.
Connection 생명 주기 관리
- 개별 Connection이 어떻게 유지 및 종료되는지 결정하는 프로퍼티
maxLifetime- Connection이 살아있을 수 있는 최대 시간
- Default: 1800000 (30 minutes)
idleTimeout- Idle Connection이 Connection Pool에 머무를 수 있는 최대 시간
- Default: 600000 (10 minutes)
keepaliveTime- Idle Connection이 끊어지지 않도록 주기적으로 신호를 보내는 간격
- Default: 120000 (2 minutes)
Connection Pool 크기 관리
- 전체 Connection Pool의 크기를 조절하기 위한 프로퍼티
maximumPoolSize- Connection Pool이 가질 수 있는 최대 커넥션 수
- Default: 10
minimumIdle- Connection Pool이 유지하려고 노력하는 최소 Idle Connection 수
- HikariCP는 이 값을 설정하지 않고
maximumPoolSize와 동일하게 두어 고정 크기 풀로 사용하는 것을 권장 - Default: same as maximumPoolSize(10)
Connection 획득 상태 관리
- 어플리케이션이 Connection을 획득하고 사용하기 위한 프로퍼티
connectionTimeout- Connection을 얻기 위해 기다리는 최대 시간
- Default: 30000ms (30 seconds)
About Pool Sizing
https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
DB의 Connection Pool의 크기는 생각보다 훨씬 작아야 한다. 의외로 Connection Pool 크기는 ‘얼마나 크게’가 아니라 ‘얼마나 작게’ 만들어야 하는지가 핵심이다.
컴퓨터는 CPU 코어 수만큼의 작업만 진짜로 동시에 처리할 수 있다. 스레드 수가 코어 수를 초과하면 운영체제는 여러 스레드를 아주 빠르게 번갈아 가며 실행시키는 시분할(time-slicing) 기법으로 마치 동시에 처리하는 것처럼 보이게 한다. 하지만 오히려 컨텍스트 스위칭에는 비용이 들어, 스레드가 코어 수보다 많아지면 오히려 성능이 저하된다.
단 스레드가 디스크나 네트워크 작업으로 인해 대기(I/O Wait) 상태에 빠질 때는 예외다. 이 시간에는 CPU가 다른 스레드의 작업을 처리할 수 있기 때문에 CPU 코어 수보다 약간 더 많은 커넥션을 두는 것이 효율적일 수 있다. 추가로 SSD처럼 더 빠른 저장 장치를 사용하면 I/O 대기 시간이 줄어들기 때문에 오히려 더 적은 수의 커넥션이 필요하다.
아래 Connection Pool 크기와 교착 상태를 방지하기 위한 최소 Connection Pool 크기를 계산하는 공식을 바탕으로,maximumPoolSize를 설정할 때 참고할 수 있다.
Connection Pool 크기 계산
PostgreSQL에서 제안하는 공식은 다음과 같다.
Connection 수 = ((CPU 코어 수 * 2) + 스핀들 수)
- CPU 코어 수
- 물리적인 코어 수 (하이퍼스레딩 제외)
- 스핀들(spindle) 수
- 데이터가 저장된 물리적인 하드 디스크의 수
- 데이터가 메모리에 모두 캐시되거나 SSD를 사용한다면 이 값은 0에 가깝다.
- 최종 계산: 4코어 CPU와 1개의 하드 디스크를 가진 서버
Connection 수 = ((4 * 2) + 1) = 9
예를 들어 AWS EC2 t4g.small 인스턴스는 위 공식에 따르면 Connection Pool이 4개부터 시작해야 한다.
- CPU 코어 수: 2개
- AWS EC2
t4g.small인스턴스는 2개의 vCPU를 가진다. - 여기서 vCPU는 물리적인 코어 2개에 해당한다.
- 따라서
CPU 코어 수는 2이다.
- AWS EC2
- 스핀들(spindle) 수: 0개
- EC2 인스턴스는 물리적인 하드 디스크가 아닌 EBS(Elastic Block Store)라는 네트워크 연결 스토리지를 사용한다.
- EBS는 대부분 SSD 기반이므로 디스크 I/O 대기 시간이 매우 짧다. 따라서 공식의
스핀들 수는 0으로 간주한다.
- 최종 계산: AWS EC2 t4g.small 인스턴스
Connection 수 = ((2 * 2) + 0) = 4
교착 상태(Deadlock) 방지
핵심 원칙은 “작은 Connection Pool을 만들고, 애플리케이션의 스레드들이 Connection을 얻기 위해 대기하게 만드는 것”이다. 수천 명의 사용자를 위해 수천 개의 Connection을 만드는 것은 지양해야 한다. 하나의 스레드가 여러 개의 Connection을 동시에 점유해야 하는 경우, 교착 상태를 피하기 위한 최소 Connection Pool 크기를 계산하는 공식은 다음과 같다. 단, 이는 최적의 크기가 아닌, 교착 상태를 피하기 위한 최소값일 뿐이다.
최소 Connection Pool 크기 = Tn x (Cm - 1) + 1
Tn: 애플리케이션에서 동시에 실행될 수 있는 최대 스레드(worker) 수- 애플리케이션을 실행하는 웹 서버(WAS)나 애플리케이션 자체의 스레드 풀 설정에서 결정한다.
- 톰캣(Tomcat) 서버는
server.tomcat.threads.max로 최대 스레드 수를 설정할 수 있다. (기본 200)server.tomcat.threads.max는Tomcat 서버가 동시에 처리할 수 있는 최대 요청(request)의 수이다.- 즉, 최대 200개(기본인 경우)의 스레드(worker)가 동시에 사용자들의 요청을 처리할 수 있다.
Cm: 하나의 스레드가 한 번에 점유해야 하는 최대 Connection 수- 개발자가 작성한 애플리케이션의 코드 로직에 의해 결정된다.
- 잘 설계된 애플리케이션이라면 하나의 스레드는 한 번에 하나의 커넥션만 사용한다.
MySQL Configuration
https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
일반적으로 권장되는 HikariCP의 MySQL 설정은 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
jdbcUrl=jdbc:mysql://localhost:3306/simpsons
username=test
password=test
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
dataSource.useServerPrepStmts=true
dataSource.useLocalSessionState=true
dataSource.rewriteBatchedStatements=true
dataSource.cacheResultSetMetadata=true
dataSource.cacheServerConfiguration=true
dataSource.elideSetAutoCommits=true
dataSource.maintainTimeStats=false
MySQL JDBC 드라이버가 PreparedStatement를 캐싱해 성능을 최적화 하는 4가지 주요 설정 값은 다음과 같다.
cachePrepStmtsPreparedStatement캐싱 기능을 켜는 활성화 스위치true로 설정하지 않으면prepStmtCacheSize나prepStmtCacheSqlLimit설정은 효과가 없다.
- Default: false
- Recommended: true
prepStmtCacheSize- 캐시를 활성화했을 때 각 DB Connection마다 캐시할
PreparedStatement(SQL 쿼리)의 개수- 기본값인 25개를 보수적으로 잡은 수치로, 캐싱 과정에서 오버헤드가 발생할 수 있기 때문에 자주 사용되는 쿼리가 충분히 캐시에 저장될 수 있도록 넉넉하게 설정하는 것이 좋다.
- Default: 25
- Recommended: 250 ~ 500
- 캐시를 활성화했을 때 각 DB Connection마다 캐시할
prepStmtCacheSqlLimit- 캐시에 저장할
PreparedStatement(SQL 쿼리)의 최대 길이 (바이트(byte) 단위로 제한)- 길이가 너무 짧으면 복잡한 쿼리는 캐싱 대상에서 제외한다. Hibernate와 같은 ORM 프레임워크는 자동으로 긴 SQL 쿼리를 생성하는 경우가 많아 기본값 256은 부족하기 때문에 대부분의 자동 생성 쿼리까지 캐시할 수 있도록 길이를 충분히 늘려주는 것이 좋다.
- Default: 256
- Recommended: 2048
- 캐시에 저장할
useServerPrepStmts(서버 측 캐시 사용)- 클라이언트(JDBC 드라이버) 측 캐시 대신 데이터베이스 서버 자체
PreparedStatement처리 기능을 사용할지 결정- 최신 MySQL 서버는
PreparedStatement를 서버 단에서 파싱하고 실행 계획을 저장해두는 기능을 지원한다.
- 최신 MySQL 서버는
- Default: false
- Recommended: true
- 클라이언트(JDBC 드라이버) 측 캐시 대신 데이터베이스 서버 자체
