커넥션 풀

현민·2023년 10월 29일

커넥션 풀

커넥션이란 WAS와 DB 사이의 연결을 의미하고, 다음과 같이 진행된다.

  1. 데이터베이스 드라이버를 사용해 데이터베이스 커넥션 열기
  2. 데이터를 읽고 쓰기 위해 TCP 소켓 열기
  3. TCP 소켓을 사용해 데이터 통신
  4. 데이터베이스 커넥션 닫기
  5. TCP 소켓 닫기

사용자로부터 요청이 들어올 때마다 이렇게 데이터베이스를 연결하고, 해제하는 것은 굉장히 비효율적으로 보인다. 이걸 커넥션 풀을 이용해 해결할 수 있다.

커넥션 풀은 WAS가 실행되면서 DB와 미리 커넥션을 해놓은 객체들을 pool에 저장해두었다가, 클라이언트 요청이 오면 커넥션을 빌려주고, 처리가 끝나면 다시 커넥션을 반납받아 pool에 저장하는 방식이다.

장점

  • 커넥션 객체를 생성하고 해제하는 과정은 시간이 오래 걸리고 자원을 많이 소모하기 때문에 커넥션 풀을 사용하면 미리 생성된 커넥션을 재사용할 수 있어 성능이 향상된다.
  • 커넥션 수를 제한할 수 있어 과도한 접속으로 인한 서버 자원 고갈 방지가 가능하다.
  • DB 접속 모듈을 공통화하여 DB 서버 환경이 바뀔 경우 쉬운 유지 보수가 가능하다.

커넥션 풀을 무조건 크게 만들면 성능이 좋아질까?

  • WAS와 DB와의 연결은 쓰레드가 담당하고, 쓰레드 또한 쓰레드 풀을 가지고 있다. 쓰레드 풀의 크기보다 커넥션 풀의 크기가 더 커지게 되면 메모리상에서 남은 커넥션들은 작업을 하지 못하고 놀게 되기 때문에 메모리만 차지하게 된다.
  • 그렇다고 무작정 쓰레드 풀의 크기를 늘린다고 해결되지 않는다. 쓰레드의 증가는 Context switching으로 인한 한계가 존재한다.

커넥션 풀의 크기는 얼마가 적당할까?

  • MySQL의 공식문서에서는 600명의 유저에 대해 15~20개의 크기가 적당하고, 그 이상부터는 적절하게 부하 테스트를 진행하며 적당한 값을 직접 찾는 것을 추천하고 있다.
  • Hikari CP의 공식문서에 따르면 connections = ((core_count * 2) + effective_spindle_count)를 추천하고 있다.
    • core_count는 서버 환경에서의 CPU 코어 개수를 의미한다. core_count * 2를 하는 이유는 context switching으로 인한 오버헤드를 고려하더라도 데이터베이스에서 Disk I/O 혹은 DRAM이 처리하는 속도보다 CPU 속도가 월등히 빠르기 때문에 쓰레드가 Disk와 같은 작업에서 블로킹되는 시간에 다른 쓰레드의 작업을 처리할 수 있는 여유가 생기게 된다. 이러한 여유 정도에 따라 멀티 쓰레드 작업을 수행할 수 있고, Hikari CP가 제시한 공식에서는 계수를 2로 선정하여 쓰레드 개수를 지정했다.
    • effective_spindle_count는 하드디스크와 관련이 있다. 하드디스크 하나는 spindle 하나를 가진다. 이에 따라 spindle 수는 기본적으로 DB 서버가 관리할 수 있는 동시 I/O 요청 수를 말한다. 디스크가 4개 있는 경우 시스템은 동시에 4개의 I/O 요청을 처리할 수 있다. 해당 공식에서는 디스크의 효율을 고려하여 spindle_count를 더해준 것으로 보인다.

커넥션 풀의 라이브러리들

커넥션 풀의 라이브러리에는 대표적으로 Commons DBCP, tomcat-jdbc-pool, HikariCP 가있다.

Commons DBCP

  • Apache에서 제공하는 커넥션 풀 라이브러리.
  • 동작 방식
    • PoolableConnection 타입의 커넥션을 생성하고 생성한 커넥션에 ConnectionEventListener를 등록한다. ConnectionEventListener에는 애플리케이션이 사용한 커넥션을 풀로 반환하기 위해 JDBC 드라이버가 호출할 수 있는 콜백 메소드가 있다.
    • 이렇게 생성된 커넥션은 commons-pool의 addObject() 메소드로 커넥션 풀에 추가된다. 이때 commons-pool은 내부적으로 현재 시간을 담고 있는 타임스탬프와 추가된 커넥션의 레퍼런스를 한 쌍으로 하는 ObjectTimestampPair라는 자료구조를 생성하고 LIFO형태의 CursorableLinkedList로 관리한다.
  • 옵션
    • initialSize - 최초 커넥션을 맺을 때 pool에 생성되는 커넥션의 개수
    • maxActive : 동시에 사용할 수 있는 최대 커넥션의 개수
    • maxIdle : 커넥션 풀에 반납할 때 최대로 유지될 수 있는 커넥션의 개수
    • minIdle : 최소한으로 유지할 커넥션의 개수
    • maxWait : 사용가능한 커넥션 객체가 없는 경우, pool이 예외를 던지기 전 연결이 반환 될 때까지 대기하는 최대 시간. 기본값은 무한정이다.
      • 속성을 설정하지 않아도 일반적인 상황에서는 큰 문제가 되지 않지만 사용자가 갑자기 급증하거나 DBMS에 장애가 발생했을 때 장애를 더 크게 확산시킬 수 있어 주의해야한다.
    • 커넥션 개수와 관련된 속성 및 조건
      • maxActive ≥ initialSize
        • 동시에 사용할 수 있는 최대 커넥션의 개수가 10이고 최초 커넥션을 맺을 때 생성되는 개수가 20개라면 initialSize 값이 최대 커넥션 개수인 maxActive보다 커서 논리적으로 오류가 있는 설정이다.
      • maxIdle ≥ minIdle
        • 최대로 유지할 커넥션의 개수가 최소한으로 유지될 커넥션의 개수보다 많아야 한다.
      • initialSize = maxActive = maxIdle = minIdle
        • 위 네 가지의 설정은 동일한 값으로 통일해도 무방하다. 커넥션 개수와 관련된 가상 중요한 성능 요소는 일반적으로 커넥션의 최대 개수이기 때문에 위 항목의 설정 값 차이는 성능을 좌우하지 않는다.
  • 유효성 검사 쿼리와 Evictor 쓰레드 관련 설정으로 애플리케이션의 안정성을 높일 수 있다.
    • JDBC 커넥션의 유효성은 validationQuery옵션에 설정된 쿼리를 실행해 확인할 수 있다.다음과 같은 세 가지 테스트 옵션으로 유효성을 검사한다.
      • testOnBorrow : 커넥션 풀에서 커넥션을 얻어올 때 테스트 실행
      • testOnReturn : 커넥션 풀로 커넥션을 반환할 때 테스트 실행
      • testWhileIdle : Evictor 쓰레드가 실행될 때 커넥션 풀 안에 있는 유휴 상태의 커넥션을 대상으로 테스트 실행
      • 검증에 지나치게 자원을 소모하지 않게 testOnBorrow, testOnReturn 옵션은 fasle로 설정하고 오랫동안 대기 상태였던 커넥션이 끊어지는 현상을 막기 위해 testWhileIdle 옵션은 true로 설정하는 것이 좋다.
    • Evictor 쓰레드는 DBCP 내부에서 커넥션 자원을 정리하는 구성 요소이며 별도의 쓰레드로 실행된다.
      • Evictor 쓰레드의 역할은 크게 3가지이다.
        • 커넥션 풀 내의 유휴 상태의 커넥션 중에서 오랫동안 사용되지 않은 커넥션을 추출해 제거한다.
        • 커넥션에 대해 추가로 유효성 검사를 수행해 문제가 있을 경우 해당 커넥션을 제거한다.
        • 앞의 두 작업 이후 남아있는 커넥션의 개수가 minIdle 속성값보다 작으면 minIdle 속성값만큼 커넥션을 생성해 유지한다.

tomcat-jdbc-pool

  • tomcat에 내장되어 사용되고 있다.
  • Commons DBCP 라이브러리를 바탕으로 만들어져 있다.
  • 스프링 부트 2.0 하위 버전에서 사용하는 기본 DBCP이다.

HikariCP

  • 스프링 부트 2.0부터 기본으로 사용하는 커넥션 풀이다.
  • 왜 스프링부트에서 HikariCP를 선택했을까?
    • HikariCP는 성능 최적화에 초점을 맞춰 개발된 라이브러리로, 가볍고 빠른 속도를 제공하기 때문이다.
    • 아래 사진과 같이 HikariCP팀에서 공개한 벤치마크를 보면 다른 커넥션풀 라이브러리보다 확실히 빠른 것을 알 수 있다.
  • 동작 방식
    • 쓰레드가 커넥션을 요청하면 커넥션 풀의 방식에 따라 유휴 커넥션을 찾아 반환한다. 이전에 사용했던 커넥션이 존재하는지 확인하고, 이를 우선적으로 반환하는 특징이 있다.
    • 가능한 커넥션이 존재하지 않으면, handOffQueue를 Polling하면서 다른 쓰레드가 커넥션을 반납하기를 기다리고, 지정한 timeout 시간까지 대기하다가 시간이 만료되면 예외를 던진다.
    • 최종적으로 사용한 커넥션을 반납하면 커넥션 풀이 커넥션 사용 내역을 기록하고 handOffQueue에서 커넥션을 받으려고 기다리는 쓰레드가 있다면 handOffQueue에 커넥션을 삽입한다. handOffQueue를 Polling하던 쓰레드는 커넥션을 획득하고 작업을 이어나간다.
  • 옵션
    • minimum-idle : 유지 가능한 최소 커넥션 개수
    • maximum-pool-size : 유지 가능한 최대 커넥션 개수
    • idle-timeout : 커넥션이 풀에서 사용하지 않는 상태로 남을 수 있는 최대 시간
    • max-lifetime : 커넥션의 최대 유지 가능 시간
    • connection-timeout : 풀에서 커넥션을 구할때 대기 시간
  • HikariCP에서 커넥션 풀 크기 산정시 주의점
    • Pool-locking 현상으로 인한 데드락 상태의 발생 가능성을 고려해야 한다.
    • 다음은 간단한 예시이다.
      1. Thread-1이 작업을 수행하기 위해 2개의 커넥션이 필요하고, 커넥션풀의 사이즈는 1이라고 가정.
      2. Thread-1이 작업을 수행하기 위해 커넥션을 받아온다.
      3. 커넥션풀에 존재하는 커넥션은 1개이므로 1개만 Thread-1에게 건네준다.
      4. Thread-1은 1개의 커넥션으로 작업을 수행할 수 없으므로 다른 쓰레드로부터 1개의 커넥션이 반납될동안 기다린다.
      5. 하지만 반납될 커넥션이 존재하지 않는다. Thread-1은 자기자신이 커넥션을 반납하는 것을 기다리는 상황이고 데드락을 의미한다.
    • HikariCP 공식 문서에서는 pool size = Tn * (Cm - 1) + 1 공식대로 최대 풀 크기를 설정하면 데드락을 피할 수 있다고 하고 있다.
      • Tn : 전체 쓰레드 개수
      • Cm : 하나의 Task에서 동시에 필요한 커넥션 수

참고

0개의 댓글