• 목차
    • Connection Pool에 대한 개념과 기본적인 원리
    • Commons DBCP로 Connection Pool 이해하기

Connection Pool이란?

  • 클라이언트의 요청에 따라 각 어플리케이션의 스레드에서 데이터베이스에 접근하기 위해서는 Connection이 필요하다
  • Connection pool은 이런 Connection을 여러 개 생성해 두어 저장해 놓은 공간, 또는 이 공간의 Connection을 필요할 때 꺼내 쓰고 반환하는 기법을 만한다
스크린샷 2021-08-13 오후 9 44 34

DB에 접근하는 단계

  1. 웹 컨테이너가 실행되면서 DB에 연결된 Connection 객체들을 미리 생성하여 pool에 저장한다
  2. DB에 요청 시, pool에서 Connection 객체를 가져와 DB에 접근한다
  3. 처리가 끝나면 다시
스크린샷 2021-08-13 오후 9 44 45

Connection이 부족하면?

  • 모든 요청이 DB에 접근하고 있고 남은 Connection이 없다면, 해당 클라이언트를 대기 상태로 전환시키고 Pool에 Connection이 반환되면 대기 상태에 있는 클라이언트에게 순차적으로 제공된다

왜 사용할까?

  • 매 연결마다 Connection 객체를 생성하고 소멸시키는 비용을 줄일 수 있다
  • 미리 생성된 Connection 객체를 사용하기 때문에, DB 접근 시간이 단축된다
  • DB에 접근하는 Connection 수를 제한하여, 메모리와 DB에 걸리는 부하를 조정할 수 있다

Commons DBCP로 Connection Pool 이해하기

Commons DBCP 속성 설정

  • Commons DBCP의 속성은 BasicDataSource 클래스의 setter 메서드로 설정할 수 있다

  • Spring 프레임 워크를 사용한다면 다음과 같이 bean 설정으로 속성을 등록한다

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"  
    destroy-method="close"
    p:driverClassName="${db.driverClassName }"
    p:url="${db.url}"
    p:username="${db.username}"
    p:password="${db.password}"
    p:maxActive="${db.maxActive}"
    p:maxIdle="${db.maxIdle}"
    p:maxWait="${db.maxWait}"
    />

커넥션의 개수

커넥션 풀의 저장 구조

  • 커넥션 생성은 Commons DBCP에서 이루어진다
  • Commons DBCP는 PoolableConnection 타입의 커넥션을 생성하고 생성한 커넥션에 ConnectionEventListener를 등록한다
  • ConnectionEventListener에는 애플리케이션이 사용한 커넥션을 풀로 반환하기 위해 JDBC 트라이버가 호출할 수 있는 콜백 메서드가 있다
  • 생선된 커넥션은 commons-pool의 addObject() 메서드로 커넥션 풀에 추가된다
  • commons-pool은 내부적으로 현재 시간을 담고 있는 타임 스탬프와 추가된 커넥션의 레퍼런스를 한 쌍으로하는 ObjectTimestampPari라는 자료구조를생헝한다
  • 이들을 LIFO(last in first out) 형태의 CursorableLinkedList로 관리한다
스크린샷 2021-08-13 오후 9 58 31

커넥션 개수 관련 속성

속성 이름 설명
initialSize BasicDataSource 클래스 생성 후 최초로 getConnection() 메서드를 호출할 때 커넥션 풀에 채워 넣을 커넥션 수
maxActive 동시에 사용할 수 있는 최대 커넥션 개수 (기본값: 8)
maxIdle 커넥션 풀에 반납할 때 최대로 유지될 수 있는 커넥션 개수 (기본값: 8)
minIdle 최소한으로 유지할 커넥션 개수 (기본값: 0)
  • 8개의 커넥션을 최대로 활용할 수 있고 4개는 사용중이고 4개는 대기 중인 상태

    스크린샷 2021-08-13 오후 11 06 15
  • 커넥션 개수와 관련된 가장 중요한 성능 요소는 일반적을 커넥션의 최대 개수이다

  • maxActive 값은 DBMS의 설정과 애플리케이션 서버의 개수, Apache, Tomcat에서 동시에 처리할 수 있는 사용자 수 등을 고려해서 설정해야 한다.

  • DBMS가 수용할 수 잇는 커넥션 개수를 확인한 후에 어플리케이션 서버 인스턴스 1개가 사용하기에 적절한 개수를 설정한다

  • 사용자가 몰려서 커넥션을 많이 사용할 때는 maxActive 값이 충분히 크지 않다면 병목 지점이 될 수 있다

  • 반대로 사용자가 적어서 사용자가 적어서 상요 중인 커넥션이 많지 않은 시스템에서는 maxActive 값을 지나치게 작게 설정하지 않는 한 성능에 영향이 없다

커넥션을 얻기 전 대기 시간

  • BasicDataSource 클래스의 maxWait 속성은 커넥션 풀 안의 커넥션이 고갈됐을 때 커넥션 반납을 대기하는 시간이며 기본값은 무한정이다
  • 사용자가 갑자기 급증하거나 DBMS에 장애가 발생했을 때 장애를 더욱 크게 확산시킬 수 있어 주의해야 한다.
  • 적절한 maxWait 값을 설정하려면 TPS(transaction per seconds)와 Tomcat에서 처리 가능한 스레드 개수 등을 이해해야 한다

TPS(transaction per seconds)

  • maxActive = 5과 maxIdle = 5, minIdle = 5로 설정한 상황을 가정한다
  • 요청 하나당 쿼리 10개를 실행한다고 가정한다
  • 각 쿼리의 평균 실행 시간은 50밀리초라고 하면 전체 10개 쿼리의 실행 시간은 500밀리초 이다.
  • 요청에 대한 초종 응답 시간은 500밀리초라고 생각할 수 있다.
스크린샷 2021-08-13 오후 11 36 50
  • 커넥션 풀에 이용 가능한 유휴 상태의 커넥션이 5개일 때는 동시에 5개의 요청을 500밀리초 동안 처리한다
  • 따라서 1초 동안에는 10개의 요청을 처리할 수 있고 성능 지수는 10TPS라고 볼 수 있다
스크린샷 2021-08-13 오후 11 39 35

TPS와 커넥션 개수와의 관계

  • 처리할 요청 수가 증가해도 커넥션 풀의 커넥션 개수가 5개이면 10TPS 이상의 성능을 낼 수 없다
  • 1번부터 5번까지 요청이 실행되는 동안은 커넥션 풀에 여분의 커넥션이 없다
  • 6번부터 10번까지 요청은 대기(wait)상태가 되 여분의 커넥션이 생길 때까지 maxWait 값만큼 기다린다
스크린샷 2021-08-13 오후 11 39 41
  • 이를 해결하는 가장 수운 방법은 maxActive 값을 높여서 커넥션 풀의 개수를 늘리는 것이다
스크린샷 2021-08-13 오후 11 42 44
  • 커넥션의 개수를 5에서 10으로 늘리면 전체적인 성능도 10TPS로 증가한다
  • 하지만 일반적을 DBMS의 리소스는 다른 서비스와 공유해 사용하는 경우가 많기 때문에 무조건 커넥션 개수를 크게 설정할 수 없다
  • 따라서 예상 접속자 수와 서비스의 실제 부하를 측정해 최적의 값을 설정하는 것이 중요하다
  • 대기 시간 (wait) 값 조절이 커넥션의 개수를 무한히 늘리지 않고 최적의 시스템을 환겨을 구축하는데 중요한 역할을 한다
  • maxWait 값을 어떻게 설정했는지가 일시적인 과부하 상태에서 드러나는 시스템의 전체적인 견고함을 결정짓는다
스크린샷 2021-08-13 오후 11 47 36
  • 적당한 maxWait 값은 Commons DBCP 외에 Tomcat의 동작 방식도 고려해야 한다
  • Tomcat은 스레드 기반으로 동작해 사용자의 요청을 처리한다
  • Tomcateh 내부에 스레드 풀을 가지고 잇어서 사용자의 요청이 들어올 때마다 스레드 풀에서 하나씩 스레드를 꺼내 요청을 처리한다
  • 1 ~ 5번의 요청이 처리되기 전에 또 다른 요청이 들어온다
  • 즉 동시에 6개의 요청이 들어왔을 때 6번 요청은 여분의 커넥션이 없으므로 maxWait 값 만큼 기다린다 (Tomcat의 스레드가 기다리는 주체이다)

적절한 maxWait 값은?

  • 위의 그림에서 maxWait 속성에 설정한 시간이 10,000 밀리초이면 처리량을 넘어서는 요청의 스레드는 10초 동안 대기 상태에 있게 된다
  • 그리고 사용자의 요청이 계속 증가하면 결국 Tomcat 스레드 풀의 모든 스레드가 소진돼 Tomcat은 아래와 같은 오류를 출력하며 응답을 멈출 것이다
    1
    심각: All threads (512) are currently busy, waiting. Increase maxThreads (512) or check the servlet status
  • 10초 동안의 대기 상태가 해제되고 커넥션을 획득해 사용자의 요청을 열심히 처리하고 응답을 보내도 그 응답을 받을 사용자는 이미 떠나고 난 뒤인 경우도 있다
  • 이럴 경우 사용자가 인내할 수 있는 시간을 넘어서는 maxWait 값은 아무런 의미가 없다
  • 반대의 경우에는 커넥션 풀에 여분이 없을 때마다 오류가 반환된다
  • maxWait 값도 사용자의 대기 가능한 시간 같은, 어플리케이션의 특성 및 자원의 상황을 고려해야한다
  • 만약 갑작스럽게 사용자가 증가해 maxWait 값 안에 커넥션을 얻지 못하는 빈도가 늘어난다면 maxWait을 줄여서 스레드가 한도에 도달하지 않도록 한다
  • 전체 시스템의 장애는 피하고 ‘간헐적 오류’가 발생하는 정도로 장애의 영향을 줄인다
  • 이런 상황이 자주 있다면 Commons DBCP의 maxActive 값과 Tomcat의 maxThread 값을 동시에 늘이는 것을 고려한다
  • 그러나 시스템 자원의 한도를 넘어선다면 시스템을 확충해야한다

커넥션의 검사와 정리

  • 유효성 검사 쿼리 (validation query)와 Evictor 스레드 관련 설정으로도 애플리케이션의 안정성을 높일 수 있다

유효성 검사쿼리의 설정

  • JDBC 커넥션의 유효성에 대한 validationQuery 옵션
    • testOnBorrow: 커넥션 풀에서 커넥션을 얻어올 때 테스트 실행(기본값: true)
    • testOnReturn: 커넥션 풀로 커넥션을 반환할 때 테스트 실행(기본값: false)
    • testWhileIdle: Evictor 스레드가 실행될 때 (timeBetwwenEvictionRunMills > 0) 커넥션 풀 안에 있는 유휴 상태의 커넥션을 대상으로 테스트 실행 (기본값: false)
  • 검증에 지나치게 자원을 소모하지 않게 testOnBorrow와 testOnReturn 옵션을 false로 설정한다
  • 오랫동안 대기상태였던 커넥션이 끊어지는 현상을 막기위해 testWhileIdle 옵션은 true로 설정한다
  • 부적절한 상태의 커넥션이 반납되었을 때 testWhileIdle = true이면 커넥션 풀에서 오류가 발생하는 커넥션을 제외할 수 있다

Evictor 스레드와 관련된 속성

  • Evictor 스레드는 Commons DBCP 내부에서 커넥션의 자원을 정리하는 구성요소이며 별도의 스레드로 실행된다

  • 관련 속성

    • timeBetweenEvictionRunsMills: Evictor 스레드가 동작하는 간격, 기본값은 -1이며 Evictor 스레드의 실행이 비활성화되어있다
    • numTestsPerEvictionRun: Evictor 스레드 동작 시 한 번에 검사할 커넥션 수
    • minEvictableIdleTimeMillis: 스레드 동작 시 커넥션의 유휴 시간을 확인해 설정 값 이상일 경우 커넥션을 제거한다 (기본값: 30분)
  • 역할

    1. 커넥션 풀 내의 유휴 상태의 커넥션 중에서 오랫동안 사용되지 않은 커넥션을 추출해 제거한다

      • Evictor 스레드 실행 시 설정된 numTestsPerEvictionRun 속서값만큼 CursorableLinkedList의 ObjectTimestampPar을 확인한다
      • ObjectTimestampPair의 타임 스탬프 값과 현재 시간의 타임 스탬프 값의 차이가 minEvictableIdleTimeMillis 속성을 초과하면 해당 커넥션을 제거한다
      • 커넥션 숫자를 적극적으로 줄여야한다면 사용하지 않기
    2. 커넥션에 대해서 추가로 유효성 검사를 수행해 문제가 있을 경우 해당 커넥션을 제거한다

      • testWhileIdle = true 일때만 이 동작을 수행한다
      • 첫 번째 작업 시 minEvictableIdleTimeMillis 속성값을 초과하지 않은 커넥션에 대해서 추가로 유효성 검사를 수행하는 것이다.
    3. 앞의 두 작업 이후 남아있는 커넥션의 개수가 minIdle 속성값보다 작으면 minIdle 속성값 만큼 커넥션을 생성해 유지한다

      testWhileIdle=true && timeBetweenEvictionRunMillis > 0이면 위의 3가지 역할을 다 수행하고,

      testWhileIdle=false && timeBetweenEvictionRunMillis > 0이면 두 번째 동작은 수행하지 않는다.

  • 주의점

    • Evictor 스레드는 동작 시에 커넥션 풀에 잠금(lock)을 걸고 동작하기 때문에 너무 자주 실행하면 서비스 실행에 부담을 줄 수 있다
    • numTestsPerEvictionRun 값을 너무 크게 설정하면 Evictor 스레드가 검사해야하는 커넥션 수가 많아져 잠금 상태에 있는 시간이 길어지므로 실행에 부담이 된다.
  • 유용한 방안

    • IDC(internet data center) 정책에 따라서는 서버 간의 소켓 연결 후 정해진 시간 이상 아무런 패킷도 주고받지 않으면 연결을 종료한다
    • timeBetweenEvictionRunsMills 속성으로 의도하지 않게 연결이 끊어지는 것을 방어할 수 있다.
    • ex) 30분 동안 통신이 없을 때 연결이 끊어지는 정책으로 네트워크를 운영한다면, BasicDataSource가 풀링(pooling)하는 커넥션의 수가 30개라고 가정할 때
      • 30분 안에 모든 커넥션에 유효성 검사 쿼리를 한 번씩은 실행하는 것이 바람직하다.
      • Evictor 스레드가 5분에 한 번씩 실행되도록 설정했을 때 30분 동안 Evictor 스레드 실행 횟수는 6번이므로 매번 5개의 커넥션을 검사해야 전체 커넥션을 테스트할 수 있다.
      • 30분 안에 5분마다 Evctor 스레드가 실행되면 6번 실행되지만 오차를 감안해 5번으로 가정하면 이때 설정해야 할 numTestsPerEvictionRun 값은 다음과 같이 구할 수 있다
      • 6 * numTestsPerEvictionRun > 30개
      • 따라서 numTestsPerEvictionRun 속성값은 최소 6 이상이어야 한다. 일반적인 공식으로 정리하면 다음과 같다
      • ('IDC 정책에서 허용하는 최대 유휴 커넥션 유지 시간' / timeBetweenEvictionRunsMillis 속성값) * numTestsPerEvictionRun 속성값) > 전체 커넥션 개수

reference

읽어보면 좋은 것들