본문 바로가기

Programming/Java & JSP & Spring

[Spring boot] Hikari CP 적용 & 커넥션 누수 이슈

데이터베이스와 애플리케이션을 연결해주는 커넥션 풀은 다양한 오픈소스 라이브러리가 존재한다.

 

  • Apache Commons DBCP
  • Tomcat JDBC
  • Bone CP
  • Hikari CP
  • 등등..

Spring boot 2.0부터는 Hikari CP를 default 커넥션 풀로 채택하고 있다.

따라서, 최근 프로젝트를 생성한 경우라면, 기본적으로 Hikari CP를 사용하기 때문에 굳이 다른 커넥션 풀로 전환하는 일은 드물 것이다.

# Hikari CP 적용

Spring Boot가 기본적으로 데이터베이스에 대한 설정을 자동으로 생성해주기 때문에, 해당 설정을 특별히 제외하지 않았고, Spring Boot 2.0 이상의 프로젝트를 생성했다면 property 설정만으로 간단하게 Hikari CP를 구성할 수 있다.

 

하지만, 다양한 설정을 커스텀하게 셋업해주기 위해 Datasource를 직접 만들어 구성해주는 편이다.

직접 Datasource를 설정해주기 위해서는 Spring boot에서 자동으로 만들어주는 데이터베이스 설정을 제외시킨다.

@SpringBootApplication(exclude = [DataSourceAutoConfiguration::class])

이후, Hikari CP를 설정해준다.

fun getHikariDataSource(): HikariDataSource {
    return HikariConfig().apply {
        driverClassName = "com.mysql.cj.jdbc.Driver"
        jdbcUrl = ""
        username = ""
        password = ""
        maximumPoolSize = 100
        maxLifetime = 1000L
        connectionTimeout = 3000L
        leakDetectionThreshold = 10000L
    }
}

# Hikari CP 옵션

Hikari CP에서는 다양한 옵션을 제공한다. (https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby)

필자는 기본적으로 default 설정값을 존중해주려 하는 편이다.

 

따라서, Hikari CP 옵션을 설정해줄 때 주의해야할 점만 살펴보면 다음과 같다.

 

  • maxLifeTime의 값은 mysql의 wait_timeout 보다 몇초정도 짧게 설정한다. (뒤에서 자세히 설명)
  • connection pool size를 thread 개수보다 넉넉히 가져가준다. (Hikari CP 데드락 이슈 https://techblog.woowahan.com/2663/)
  • minimumIdle의 기본 값은 maximumPoolSize이므로, idleTimeout을 설정해주지 않는 이상 따로 손보지 않아도 된다.
  • leakDetectionThreashold는 커넥션이 설정 시간보다 길게 잡고 있다면 누수로 판단하고 WARN 로그를 출력한다.

# Hikari CP 커넥션 유효성 검사

>> 참고: https://pkgonan.github.io/2018/04/HikariCP-test-while-idle

 

위 블로그 글에 잘 정리되어있지만, 한번 더 정리하면 다음과 같다.

 

mysql의 입장에서는 오래동안 사용되고 있지 않은 커넥션이 있다면 해당 커넥션을 close하고 할당된 리소스를 회수한다.

  • wait_timeout이 관련된 설정값이다.
  • show global variables like '%timeout' 명령어로 설정값을 확인해볼 수 있다.
  • 기본값은 8시간이다.

이렇게 mysql이 회수하면, 커넥션 풀 입장에서는 close 여부를 바로 알 수 없고, 해당 커넥션을 사용하려고 할 때, 비로서 문제가 있다는 것을 알게된다.

 

따라서 이러한 문제를 해결하기 위해 commons-dbcp의 경우 evictor라는 별도의 쓰레드로 커넥션을 validation한다.

즉, 주기적인 health check로 mysql에게 회수 당하는 것을 방지할 수 있다.

(해당 커넥션으로 health check 쿼리를 하게되므로, mysql의 wait_timeout은 초기화되고 다시 8시간을 카운팅하게 된다.)

 

Hikari CP의 경우 유사한 목적으로 HouseKeeper 커넥션 풀 관리 쓰레드를 이용하지만 validation에 대한 기본적인 철학이 다르다.

기존의 커넥션 풀들은 너무 잦은 validation으로 과하게 DBMS에 부하를 발생시킨다는 것이다.

 

즉, Hikari CP는 testWhileIdle 같은 옵션이 존재하지 않고, 대신 maxLifeTime의 옵션이 존재한다.

 

  • maxLifeTime은 커넥션이 커넥션 풀에 최대로 머무를 수 있는 시간을 뜻한다.
  • maxLifeTime을 mysql의 wait_timeout보다 짧게 설정한다면, mysql이 회수하기전에 커넥션 풀이 커넥션을 새롭게 맺으므로 별다른 이슈가 없다.
  • maxLifeTime의 기본값은 30분, mysql의 wait_timeout 기본값은 8시간이다.
  • maxLifeTime으로 커넥션이 끊기고 새롭게 커넥션을 맺는 대상은 active하지 않은 커넥션들 대상이다. (따라서 계속 active 상태의 커넥션이라면 대상이 되지 않는다.)
  • maxLifeTime에 모든 커넥션들이 내려가고 새롭게 맺지 않는다. 순차적으로 이뤄진다.

# Hikari CP 커넥션 누수

Hikari CP를 적용하고 테스트하다보니 커넥션이 부족하다는 오류가 발생했다.

처음에는 Hikari CP 데드락 이슈인줄 알고, 커넥션 풀 사이즈를 늘려주었다. 하지만, 커넥션 부족이 뜨는 시간만 지연될 뿐 이슈가 해결되진 않았다.

 

이슈 파악을 위해 Hikari 관련 로그를 DEBUG 레벨로 낮추고, leakDetectionThreshold 설정을 해주었다.

모니터링 해보니, Hikari 커넥션이 active 상태에서 close되지 않았다.

 

또한 커넥션 누수 감지로그가 출력되었는데, 공통적으로 모두 querydsl 관련 코드에서 로그가 출력되었다.

조금 더 알아보니, 트랜잭션 내부가 아닌 곳에서 querydsl의 transform 기능을 사용하면 커넥션 누수가 발생한다는 글을 발견했다. (https://colin-d.medium.com/querydsl-에서-db-connection-leak-이슈-40d426fd4337)

 

해당 이슈를 해결하기 위해 querydsl.transform을 사용하는 모든 곳에 @Transactional 어노테이션을 붙여 해결했다.

(트랜잭션을 사용함으로써 사이드이펙트가 발생할 수 있으므로 readonly를 추천)

 

이슈는 해결했지만 의문점이 하나 남았다.

기존 commons dbcp의 커넥션풀을 사용할 때는 왜 이러한 누수가 발견되지 않았나?

 

결론부터 말하자면, 기존에도 누수가 발생하긴 했지만, commons dbcp의 설정으로 누수가 발견되지 않았던 것이다.

누수를 피할 수 있었던 옵션은 removeAbandonedTimeout 설정이다.

 

해당 옵션을 설정해주면 커넥션이 활성화 상태에서 쿼리를 일정시간동안 하고 있지 않다면, 커넥션을 끊어낸다.

Hikari CP에는 이러한 옵션이 존재하지 않는다. (기본적으로 Hikari는 최대한 DBA의 설정을 따라가는 철학으로 보인다.)

 

따라서, Hikari 에서는 특히나 주의를 해주고 DB 설정을 적절히 잘해줘야한다.

 

  • 근본적으로 누수가 발생하지 않도록 주의한다.
    • EntityManager 사용 주의 & 트랜잭션이 없는 곳에서 쿼리 주의
  • mysql의 wait_timeout 설정
    • 기본값 8시간은 너무 길다. maxLifeTime 값보다 작지 않은 선에서 적절히 조절한다.
  • leakDetectionThreshold 설정
    • 커넥션 누수 감지 로깅을 설정해준다.
    • 누수를 감지만 할뿐, 조치는 직접 해야한다.

# Hikari CP 성능?

Hikari CP는 성능이 뛰어난 것을 강조하고 있다. (https://github.com/brettwooldridge/HikariCP#checkered_flag-jmh-benchmarks)

그래서 커넥션 풀을 교체해주는 것만으로도 드라마틱한 성능을 보여주겠구나. 라고 생각을 했다.

하지만, Hikari CP에서 자랑하는 것만큼 성능이 바뀐 것을 체감하지 못했다.

 

직접 commons dbcp와 hikari를 비교해보며 부하테스트를 진행했지만, TPS 측면에서 달라진 것이 거의 없기 때문이다.

# Hikari CP 전환 결론

Hikari CP로 전환했던 가장 큰 이유는 성능 측면이였다. 하지만 위에서 말한대로 크게 달라진 것이 없어서 허무했다.

그럼에도 불구하고 전환하고 싶었던 이유는 이미 spring boot 2.0부터 기본적으로 제공하는 커넥션 풀이기 때문이다.

그만큼 안정되고 보장된 커넥션 풀이라 기본적으로 제공하지 않을까? 라는 생각으로 전환을 마쳤다.

성능에 관해서는 좀 더 모니터링해봐야 정확히 파악이 가능할 것 같다.