[Database] DBCP (DB connection pool)과 hikariCP, MySQL을 기준으로
본문 바로가기

ComputerScience/Database

[Database] DBCP (DB connection pool)과 hikariCP, MySQL을 기준으로

백엔드 애플리케이션이 API 요청을 받으면 종종 데이터를 검색하거나 조작하기 위해 데이터베이스와 상호작용해야 합니다. 이 상호작용은 데이터베이스에 쿼리를 보내고 데이터베이스가 쿼리에 응답하는 과정을 포함합니다. 데이터를 처리한 후 애플리케이션은 API 요청자에게 응답을 보냅니다.

 

백엔드와 데이터베이스 간의 통신은 TCP(Transmission Control Protocol)를 통해 이루어집니다. TCP는 데이터 전송에서 높은 신뢰성을 제공하기 때문에 선택됩니다. TCP 연결을 설정하는 과정에는 연결을 열기 위한 3-way handshake와 연결을 닫기 위한 4-way handshake가 포함됩니다.

 

TCP는 신뢰할 수 있는 통신을 보장하지만, 쿼리를 실행할 때마다 Connection을 열고 닫는 과정은 시간이 많이 소요될 수 있습니다. 데이터베이스와 자주 상호작용하는 백엔드 애플리케이션의 경우, 반복적으로 Connection을 설정하고 종료하는 오버헤드가 성능을 크게 저하시킬 수 있습니다.

이 글에서는 이러한 문제를 해결하기 위한 해결책인 DBCP에 대해 다루고자 합니다.

DBCP (Database Connection Pooling)

이미지 출처: https://hudi.blog/static/1bda5b43f837f4e11d0d6934aa003aa0/e249b/database-connection-pool.png

DBCP(Database Connection Pooling)는 재사용할 수 있는 데이터베이스 Connection을 Pool로 만들어 관리한다.

다음은 DBCP의 작동 방식이다.

  1. Connection 생성: 백엔드 애플리케이션이 시작될 때, 미리 정의된 수의 데이터베이스 Connection을 생성하고 이를 Pool에 저장한다.
  2. Pool 메커니즘: 이 Connection들은 열려 있는 상태로 유휴(idle) 상태로 유지되어 필요할 때 사용됩니다. API 요청이 데이터베이스 상호작용을 필요로 하면 애플리케이션은 Pool에서 Connection을 빌려온다.
  3. 쿼리 실행: 빌려온 Connection을 사용하여 필요한 쿼리를 실행한다.
  4. Pool로 반환: 데이터베이스 작업이 완료되면 Connection을 닫지 않고 Pool로 반환하여 후속 요청에서 다시 사용할 수 있게 한다.

이 접근 방식은 반복적으로 Connection 을 열고 닫을 필요를 없애, 시간 절약과 성능 향상을 가능하게 한다.

MySQL 기준 DBCP 설정하기

DBCP 설정 시, 성능과 안정성을 위해 중요한 파라미터들이 있다. 그중에서도 max_connections와 wait_timeout은 특히 주의 깊게 설정해야 한다.

max_connections

max_connections는 MySQL 서버가 동시에 연결할 수 있는 최대 클라이언트 수를 정의한다. 이 값이 너무 낮으면 애플리케이션이 연결을 기다리느라 지연될 수 있고, 너무 높으면 서버 자원이 고갈될 위험이 있다.

예시 시나리오

만약 max_connections를 4로 설정하고, DBCP의 최대 연결 수도 4로 설정했다고 가정해보자.

백엔드 서버로 많은 요청이 들어오면 DBCP Pool의 max_connections를 모두 사용하게 된다. 이때 백엔드 서버는 클라이언트와의 TCP 연결 및 DB와의 연결을 유지하기 위해 CPU 사용량과 메모리 사용량이 급증하게 되어 과부하가 발생할 수 있다.

분산 서버 환경에서도 max_connections 값이 충분하지 않으면, 서버가 추가적인 연결을 수립할 수 없어 문제는 해결되지 않는다. 따라서 max_connections 값을 적절하게 설정하는 것이 중요하다.

wait_timeout

wait_timeout은 connection이 비활성 상태일 때, 해당 connection을 얼마나 오랫동안 유지할지를 결정한다. 이 값이 너무 작으면 유휴 상태의 연결이 자주 종료되어 다시 connection을 설정하는 오버헤드가 발생할 수 있고, 너무 길면 비정상적인 연결이 자원을 낭비하게 된다.

예시 시나리오

DB 서버에서는 이미 connection이 열려 있는 상태에서, 비정상적인 connection 종료나 connection 반환 누락, 네트워크 단절 등의 상황이 발생할 수 있다. 이러한 경우 DB는 해당 연결을 정상적으로 인식하여 계속 리소스를 차지하게 된다. wait_timeout 파라미터는 이러한 문제를 완화하기 위해 존재하며, 설정된 시간 내에 요청이 도착하면 타이머가 0으로 초기화된다.

HikariCP 설정

HikariCP는 Spring boot 2.x 버전 이상부터는 HikariCP가 사용된다. HikariCP는 빠르고 신뢰할 수 있는 JDBC Connection Pool이다. MySQL과 같은 데이터베이스와의 효율적인 연결을 지원한다. HikariCP 설정 시, 성능과 안정성을 극대화하기 위해 중요한 파라미터들을 적절히 조정해야 한다.

minimumIdle

connection pool에서 유지하는 최소한의 idle connection 수를 의미한다. 최소 idle connection 수를 유지하여 새로운 connection 요청이 들어왔을 때 신속하게 대응할 수 있게 한다.

만약 minimumIdle을 2로 설정하면, 항상 최소 2개의 idle connection이 유지된다. 하나의 연결이 사용 중일 때, 새로운 connection 을 만들어 minimumIdle을 유지하려 한다.

maximumPoolSize

pool이 가질 수 있는 최대 connection 수를 의미한다. idle connection과 active connection의 합을 나타낸다.

최대 연결 수를 제한하여 과도한 리소스 사용을 방지하는 역할을 한다.

만약 maximumPoolSize를 4로 설정하면, pool은 최대 4개의 connection만 가질 수 있다. 만약 minimumIdle이 2이고 maximumPoolSize가 4라면, idle connection이 1이 될 때마다 새로운 연결을 만들지만, 총 connection 수가 4를 넘지 않기 위해 만들지 않을 수 있다.

maxLifetime

connection pool에서 connection의 최대 수명을 설정한다. 오래된 connection을 제거하여 리소스 누수를 방지하고, 새로운 connection을 통해 성능을 유지한다.

이 값은 DB의 wait_timeout보다 약간 짧게 설정해야 한다. 예를 들어, DB의 wait_timeout이 60초라면 maxLifetime을 55~58초로 설정하는 것이 좋다. 만약 같은 값으로 설정됐을 경우, hikariCP에서 connection을 할당받아 DB에 연결을 시도할 때, DB는 이미 maxLifetime을 초과하여 예외가 발생할 수 있기 때문이다.

maxLifetime이 초과된 연결이 active 상태로 남아 있으면, 반환 후 제거된다. 반환되지 않으면 DB의 wait_timeout에 의해 연결이 끊기며 예외가 발생할 수 있다.

connectionTimeout

pool에서 connection을 얻기 위해 기다리는 최대 시간을 설정한다. 이를 통해 일정 시간 내에 connection을 얻지 못하면 예외를 발생시켜 클라이언트가 무한 대기하는 것을 방지한다. 너무 길게 설정하면 클라이언트의 대기 시간이 길어지므로, 애플리케이션의 요구사항에 맞게 적절히 설정해야 한다. connectionTimeout을 30초로 설정하면, 연결을 얻기 위해 최대 30초까지 기다린다. 이 시간이 초과되면 예외가 발생한다.

적절한 connection 수 찾는 과정

과정 1: 모니터링 환경 구축

먼저, 서버 리소스(CPU, 메모리), 서버 스레드 수, DBCP 상태 등을 모니터링할 수 있는 환경을 구축한다. 이는 시스템 성능을 실시간으로 분석하고 병목 현상을 식별하는 데 중요하다. 모니터링 도구로는 Prometheus, Grafana, New Relic 등을 사용할 수 있다.

과정 2: 백엔드 시스템 부하 테스트

부하 테스트를 통해 시스템의 최대 처리량을 파악한다. 부하 테스트 도구로는 Ngrinder, JMeter, Gatling, Locust 등을 사용할 수 있다.

과정 3: 리소스 사용률 확인

부하 테스트 결과를 바탕으로 CPU, 메모리 등 시스템 리소스 사용률을 확인한다.

주요 지표는 Request Per Second(RPS)와 Average Response Time이다.

RPS가 요청이 증가하다가 어느 순간부터 일정 수준에서 유지되는 순간이 있고, Average Response Time초기에는 안정적이다가 요청이 많아지면 어느 순간부터 점점 증가한다. 주요 파라미터 수치를 수정하면서 그 시점을 비교한다.

과정 4: 스레드 및 커넥션 상태 확인

부하 테스트 도중, 스레드 풀과 DBCP의 상태를 분석한다.

  • Thread Per Request 모델: 스레드 풀의 크기와 활성 스레드 수를 확인한다.
    • 예: 스레드 풀이 5개이고 활성 스레드도 5개라면 스레드 풀 크기가 너무 작아 병목 현상이 발생할 수 있다.
    • 스레드 풀이 100개인데 50개만 활성 상태라면 스레드 풀 크기를 줄여 리소스를 절약할 수 있다.
  • DBCP의 활성 연결 수: HikariCP의 maximumPoolSize가 5인 경우, 활성 연결 수가 5에 도달하면 추가 연결을 생성할 수 없기 때문에 병목이 발생할 수 있다.

최적화

  1. 최적의 maximumPoolSize 설정:
    • maximumPoolSize를 초기 설정보다 점진적으로 증가시켜 적절한 값을 찾는다.
    • 예: 5에서 시작하여 부하 테스트를 통해 10, 15 등으로 늘려가며 성능 변화를 관찰한다.
  2. 리소스 최적화:
    • CPU, 메모리 사용률을 모니터링하여 필요에 따라 서버 스펙을 조정한다.
    • 캐시 레이어 추가로 DB 요청을 줄이고, 데이터베이스 샤딩으로 부하 분산을 구현한다.
  3. 스케일링:
    • 트래픽 증가를 예상하여 서버를 수평적으로 확장한다.
    • Primary DB와 Secondary DB의 역할을 분리하여 부하를 분산시킨다.

참고