[ Database - Intermediate ] - DBCP ( DB connection pool) + hikariCP, MySQL
이번에는 제가 Spring boot에서 JPA를 다루면서 DB connection도 하고, 트랜잭션 요청을 통해 DB 접근을 많이 했었는데, 이에 대한 DBCP가 hikariCP라는건 알고 있었습니다. 하지만 이에 대한 정확한 원리에 대해 잘 알고 있지 않고, 매개변수와 같은걸 설정할 때에 주의해야 할 점이 무엇이 있는지에 대해 알아보고자 간단히 정리해봅니다.
그리고 해당 포스팅에서는 백엔드 애플리케이션으로는 SpringBoot와 DBCP로는 HikariCP, DB 서버로는 MySQL을 기준으로 설명하겠습니다.
HikariCP max_connections
max_connections는 말 그대로 client와 맺을 수 있는 최대 connection의 수입니다. 이가 중요한 이유가 위 그림처럼 Backend 애플리케이션의 부하분산을 위해 스케일링을 하게 되면, connection을 사용할 수 없게 되는 상황이 생길 수 있기 때문입니다. max_connections수는 제한되어 있기 때문에, 새로 투입된 백엔드 애플리케이션이 connection을 맺고 싶어도 할 수 없게 됩니다.
HikariCP wait_timeout
wait_timeout은 DB에서 connection이라는 리소스 낭비를 막기 위한 파라미터 입니다. DB 서버에서 네트워크 단절이 생겨서 connection을 반환할 수 없는 상황이 생길 수 있습니다. 이는 DB입장에서 유효한 connection이라고 생각할 수 있기 떄문에, DB입장에서 connection이 inactive 할 때 다시 요청이 오기까지 얼마의 시간을 기다린 뒤에 close할 것인지를 결정해야 하는데, 여기에 활용되는 매개변수가 wait_timeout입니다. 아마 네트워크 프로그래밍 할 때, 많이 봤던 개념일겁니다.
HikariCP minimumIdle, maximumPoolSize
이 또한, k8s를 다룰 때, pods의 개수를 다룰 때 많이 봤던 개념입니다. minimumIdle은 말 그대로 pool이 최소한 유지해야 하는 idle connection의 수를 말하고, maximumPoolSize는 pool이 가질 수 있는 최대 connection의 수로써, maximumPoolSize가 minimumIdle보다 우선순위가 높습니다.
예를 들면, 초기에는 minimumIdle이 2니까 Idle한 connection이 pool에 2개가 있을 겁니다. 그리고 connection이 1개가 사용되고, minimumIdle은 2니까 Idle한 connection을 1개 더 만들어서 Idle한 connection 2개, busy connection 1개가 될 것입니다. 그리고 한개가 또 connection을 사용하게 되면, 2개, 2개가 될 것입니다. 여기서 또 connection을 맺게 되면, 총 connection이 5개가 될 것을 예상하지만 maximumPoolSize가 4이므로 5개가 되는 것을 막게 됩니다. 간단한 원리입니다.
참고로 위에서 busy하던 connection이 close가 되어 Idle이 4개가 되면 Idle connection 2개를 없앱니다. 그래서 Idle connection을 2개로 유지하게 됩니다. 하지만 실제로 hikariCP에서는 minimumIdle = maximumPoolSize를 권장하고 있습니다. 그 뜻은, pool size가 고정된다는 소리이겠죠?
HikariCP maxLifetime
maxLifetime은 말 그대로 connection의 생명 주기입니다. 만약 해당 시간이 지났다면, connection은 삭제됩니다. 하지만 삭제되었다가 위에서 살펴본 minimumIdle보다 작아지게 되면, 삭제되고 바로 새로운 Idle connection이 생겨나게 됩니다. 또한, 해당 connection이 active(=busy)한 상태라면 pool로 반환된 후에 바로 제거되게 됩니다. 이 때문에, connection을 다 사용했으면 반환되게 하는것이 Backend 서버쪽에서 해야하는 중요한 역할인데, 그 이유는 만약 반환되지 않았다면 해당 connection이 active한 상태인줄 알고 maxLifetime 특성 상 active connection은 반환하지 않으므로 connection을 반환하지 않게 됩니다. 결국엔 DB측에서 wait_timeout이 발동하고 나서야 connection을 반환하게 될 것입니다.
그리고 maxLifetime을 설정할 때에 주의해야 할 점은, DB의 conenction time limit보다 몇 초 짧게 설정해야 한다는 것입니다. maxLifetime, wait_timeout을 둘 다 60초로 설정했다고 해봅시다. 그런데, maxLifetime에 간신히 걸리지 않고 서버 애플리케이션에서 59초 쯤에 connection을 가지고 요청을 DB로 보냈는데, 요청이 가면서 1초가 지나 60초가 되고 DB는 해당 connection을 끊게 되어 서버측에서는 Exception이 발생할 수 있기 때문입니다.
HikariCP connectionTimeout
connectionTimeout은 트래픽이 백엔드 애플리케이션으로 몰리게 될 때에 Idle인 connection이 없는경우, connection이 몇초동안 기다리고 Exception을 뿜을지를 나타내는 것입니다.
근데 이걸 또 설정할 때 유의해야 할 점이, 사용자가 그렇게 인내심이 많지 않다는 점입니다. 만약 connectionTimeout을 30초로 해놨는데, 29초쯤에 connection이 Idle이 되어 사용한다음에 API를 Client에 반환했다고 해봅시다. 근데, 사용자는 그냥 클라이언트상에서 연결을 끊은 상황이므로 이렇게 반환해봤자 의미가 없다는 것입니다. 즉 connectionTimeout을 적당한 시간으로 설정해줘야한다는 것입니다.
적절한 connection 수를 찾기 위한 과정
우선 위와같은 상황을 가정하겠습니다. Primary DB서버를 두고, 이에 대한 HA(=High Availability)를 보장하기 위해 Replica를 Read-only로 2개를 두었다고 가정하겠습니다. 그리고 max_connections는 30으로 해두었고, 이에 대한 연결을 요청하는 백엔드 애플리케이션의 DBCP의 maximumPoolSize는 5로 고정한 상황이라고 해보겠습니다.
그 상황에서 우선 백엔드 서버라면 모니터링 환경을 구축을 해서 하드웨어 리소스 같은걸 관측할 수 있겠죠? 그리고 만약 백엔드 팀이라면 팀 네이버의 nGrinder와 같은 것을 통해서 백엔드 시스템 부하 테스트를 진행할 수 있을 겁니다. 이 과정을 통해서 request per second, avg response time등을 확인하게 됩니다.
request per second는 단위 초당 얼마나 많은 request를 처리할 수 있는가. 즉 백엔드 서버의 처리량을 확인할 수 있는 지표라고 할 수 있고, avg response time은 request에 대해 평균적인 응답 시간을 나타냅니다. 즉 API의 성능을 나타낼 수 있는 지표인 것입니다.
그리고 RPS, ART는 위와같은 그림이 당연히 나오게 될 것입니다. 이러한 상황에서 꺾이는부분에서 모니터링을 하며 백엔드 서버, DB 서버의 CPU, MEM 등등의 리소스를 확인합니다.
당연히 백엔드 서버에서의 CPU사용량이 60~70%가 넘어간다면, 서버를 하나 더 추가하는 방법을 통해 CPU 사용량을 부하분산 할 수 있는 방법을 생각해볼 수 있을 겁니다. 하지만 백엔드 서버는 괜찮은데, DB서버의 CPU, MEM사용량이 올라간다면 어떻게 해야할까요? 이때는, 다양한 방법이 있을 수 있는데 secondary 추가, cache layer추가, sharding등의 해결방안이 있을 수 있겠습니다.
그런데 백엔드, DB서버 모두 CPU, MEM사용량이 정상이라면 우리는 뭘 해줘야할까요?? 우선 thread per request 모델이라면, 서버의 active thread수를 확인해야 합니다. request가 많이 들어와서 thread pool에서 병목현상이 나타난 경우일 수 있기 때문에, thread pool의 크기가 5인데, active thread의 수도 5개다. 이라면 thread pool의 크기를 늘려주는 작업을 해주면 되겠죠. 하지만 이 또한 정상이라면 뭘 해줘야 할까요?? 바로 이때 DBCP의 active connection 수를 확인해보면 됩니다. 이때에 maximumPoolSize를 5라고 했는데, active connection수가 5라면, maximumPoolSize를 늘려볼 생각을 해야합니다.
위와같은 과정을 거치면서 부하테스트를 진행해야 합니다. 여기서 maximumPoolSize를 2개의 백서버에 15, 15로 설정했다면, DB서버의 max_connections에 도달했으므로 더이상 올릴 수는 없는 상태가 된겁니다. 만약 이렇게 했는데도 문제가 생겼다면, max_connections를 올려주어야 하는 상황인 것입니다. max_connections 30 -> 60, maximumPoolSize 15 -> 25 이런식으로 말이죠. 즉 최종적으로 사용할 백엔드 서버수를 고려하여 DBCP의 max pool size를 결정해야 하는게 핵심이라고 말할 수 있겠습니다.