Post

HikariCP 커넥션 풀

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은 일단 연결이 수립되고 나면, 실제 데이터를 주고받는 작업은 훨씬 가볍고 효율적으로 낮은 peakrates가 나타났다. 반면 No Pooling은 매번 연결을 맺는 작업이 네트워크 대역폭을 폭발적으로 사용하는 무거운 작업을 수행해 높은 peakrates가 나타났다.


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:

  1. We prefer HikariCP for its performance and concurrency. If HikariCP is available, we always choose it.
  2. Otherwise, if the Tomcat pooling DataSource is available, we use it.
  3. Otherwise, if Commons DBCP2 is available, we use it.
  4. 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-jdbc or spring-boot-starter-data-jpa starters, you automatically get a dependency to HikariCP.


HikariCP

HikariCP Configuration

https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby

Essential

  • dataSourceClassName

    This is the name of the DataSource class provided by the JDBC driver. Note that you do not need this property if you are using jdbcUrl for “old-school” DriverManager-based JDBC driver configuration.

  • jdbcUrl

    This property directs HikariCP to use “DriverManager-based” configuration. When using this property with “old” drivers, you may also need to set the driverClassName property, but try it first without.

  • username

    This property sets the default authentication username used when obtaining Connections from the underlying driver.

  • password

    This 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이다.
  • 스핀들(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가지 주요 설정 값은 다음과 같다.

  1. cachePrepStmts

    • PreparedStatement 캐싱 기능을 켜는 활성화 스위치
      • true로 설정하지 않으면 prepStmtCacheSizeprepStmtCacheSqlLimit 설정은 효과가 없다.
    • Default: false
    • Recommended: true
  2. prepStmtCacheSize

    • 캐시를 활성화했을 때 각 DB Connection마다 캐시할 PreparedStatement(SQL 쿼리)의 개수
      • 기본값인 25개를 보수적으로 잡은 수치로, 캐싱 과정에서 오버헤드가 발생할 수 있기 때문에 자주 사용되는 쿼리가 충분히 캐시에 저장될 수 있도록 넉넉하게 설정하는 것이 좋다.
    • Default: 25
    • Recommended: 250 ~ 500
  3. prepStmtCacheSqlLimit

    • 캐시에 저장할 PreparedStatement(SQL 쿼리)의 최대 길이 (바이트(byte) 단위로 제한)
      • 길이가 너무 짧으면 복잡한 쿼리는 캐싱 대상에서 제외한다. Hibernate와 같은 ORM 프레임워크는 자동으로 긴 SQL 쿼리를 생성하는 경우가 많아 기본값 256은 부족하기 때문에 대부분의 자동 생성 쿼리까지 캐시할 수 있도록 길이를 충분히 늘려주는 것이 좋다.
    • Default: 256
    • Recommended: 2048
  4. useServerPrepStmts (서버 측 캐시 사용)

    • 클라이언트(JDBC 드라이버) 측 캐시 대신 데이터베이스 서버 자체 PreparedStatement 처리 기능을 사용할지 결정
      • 최신 MySQL 서버는 PreparedStatement를 서버 단에서 파싱하고 실행 계획을 저장해두는 기능을 지원한다.
    • Default: false
    • Recommended: true
This post is licensed under CC BY 4.0 by the author.