본문으로 건너뛰기

1. 연결 풀 크기의 오해와 진실

  • 많은 개발자들이 데이터베이스 연결 풀을 설정할 때 직관과 다른 몇 가지 원칙들을 이해해야 합니다.
  • 특히 연결 풀의 크기를 결정할 때 "더 많은 것이 더 좋다"는 일반적인 오해가 있습니다.

1.1 흔한 오해: 대규모 동시 사용자

  • 예를 들어, Facebook 규모는 아니지만 동시에 10,000명의 사용자가 초당 20,000건의 트랜잭션을 발생시키는 웹사이트가 있다고 가정해보겠습니다.
  • 이런 경우 연결 풀의 크기는 얼마나 되어야 할까요?
  • 놀랍게도, 이 질문의 핵심은 "얼마나 크게"가 아니라 "얼마나 작게" 설정할 수 있느냐입니다.
  • Oracle의 Real-World Performance 그룹의 테스트에 따르면, 연결 풀 크기를 2048개에서 96개로 줄이는 것만으로도 응답 시간이 100ms에서 2ms로 약 50배 개선되었습니다.

2. 왜 더 적은 연결이 더 나은가?

2.1 컴퓨터 과학의 기본 원리

  • 최근 컴퓨팅의 여러 영역에서 "적은 것이 더 낫다"는 원칙이 입증되고 있습니다.
  • 예를 들어, nginx가 4개의 스레드로 100개의 프로세스를 가진 Apache 웹서버보다 더 나은 성능을 보이는 이유는 무엇일까요?
  • 컴퓨터 과학의 기본 원리로 돌아가 보면:
    • 단일 CPU 코어는 실제로 한 번에 하나의 스레드만 실행할 수 있습니다
    • 운영체제의 시분할(time-slicing) 방식으로 여러 스레드를 "동시에" 실행하는 것처럼 보이게 합니다
    • 기본적인 컴퓨팅 법칙: 단일 CPU에서 A와 B를 순차적으로 실행하는 것이 시분할을 통해 "동시에" 실행하는 것보다 항상 빠릅니다

2.2 제한된 리소스의 영향

  • 데이터베이스의 주요 병목 현상은 세 가지 카테고리로 나눌 수 있습니다:
    • CPU
    • 디스크
    • 네트워크
노트

메모리도 고려할 수 있지만, 메모리 접근 속도는 디스크나 네트워크보다 수 차원 빠르기 때문에 일반적으로 주요 병목 요인이 되지 않습니다.

2.2.1 CPU 코어와 연결 수의 관계

  • 만약 디스크와 네트워크를 무시한다면, 8개의 CPU 코어가 있는 서버에서는 8개의 연결이 최적의 성능을 제공할 것입니다.
  • 이보다 많은 연결은 컨텍스트 스위칭 오버헤드로 인해 성능이 저하되기 시작합니다.

2.2.2 디스크 I/O의 영향

  • 전통적인 하드 디스크 작업에는 다음과 같은 지연 요소가 있습니다:
    • 탐색 시간 (seek time): 읽기/쓰기 헤드가 데이터 위치로 이동하는 시간
    • 회전 지연 (rotational latency): 디스크 플래터가 회전하여 원하는 데이터가 다시 헤드 아래로 오는 시간
    • 데이터 전송 시간
  • 이러한 I/O 대기 시간 동안 스레드는 "블록" 상태가 되며, 이때 OS는 다른 스레드의 코드를 실행할 수 있습니다.
  • 이러한 I/O 블로킹으로 인해 물리적 CPU 코어 수보다 더 많은 연결/스레드로도 더 많은 작업을 처리할 수 있게 됩니다.
SSD 사용 시 흔한 오해

"SSD가 더 빠르니까 더 많은 스레드를 사용할 수 있다"라고 생각하기 쉽지만, 이는 정반대입니다. SSD는 탐색 시간이나 회전 지연이 없어 더 빠르기 때문에, 블로킹이 덜 발생하고 따라서 CPU 코어 수에 더 가까운 더 적은 수의 스레드가 더 나은 성능을 보입니다. 더 많은 스레드는 오직 블로킹이 실행 기회를 만들어낼 때만 더 나은 성능을 보입니다.

3. 최적의 연결 풀 크기 계산

3.1 기본 공식

connections = ((core_count * 2) + effective_spindle_count)
  • 해당 공식은 PostgreSQL 프로젝트에서 수년간의 벤치마크를 통해 잘 들어맞았던 공식
  • core_count: 하이퍼스레딩을 제외한 실제 CPU 코어 수
  • effective_spindle_count:
    • 데이터가 완전히 메모리에 캐시된 경우 0
    • 캐시 히트율이 낮아질수록 실제 스핀들 수에 가까워짐
    • 스핀들은 디스크 플래터(원판)를 회전시키는 축입니다
    • 하나의 물리적 하드 디스크는 하나의 스핀들을 가집니다
  • 아직까지 이 공식이 SSD에서 얼마나 잘 작동하는지에 대한 분석은 이루어지지 않았습니다.

3.2 실제 적용 예시

4코어 i7 서버와 하나의 하드 디스크를 가진 시스템의 경우:

최적 연결 수 = ((4 * 2) + 1) = 9
정보

이정도 설정으로도 간단한 쿼리를 실행하는 3,000명의 동시 사용자를 초당 6,000 TPS로 처리할 수 있습니다.

4. 풀 잠금(Pool-locking) 고려사항

  • 애플리케이션에서 하나의 스레드가 여러 데이터베이스 연결을 동시에 필요로 하는 경우가 있습니다.
  • 이런 상황에서 데드락을 방지하기 위해 적절한 연결 풀 크기를 계산하는 방법을 알아보겠습니다.
pool size = Tn x (Cm - 1) + 1
  • Tn: 최대 동시 실행 스레드 수
  • Cm: 단일 스레드가 보유하는 최대 동시 연결 수

4.1 예시 계산

시나리오: 3개의 스레드가 각각 4개의 연결을 필요로 하는 경우

// 하나의 스레드가 수행하는 작업 예시
Thread thread = new Thread(() -> {
Connection sourceDb = pool.getConnection(); // 1번째 연결
Connection targetDb1 = pool.getConnection(); // 2번째 연결
Connection targetDb2 = pool.getConnection(); // 3번째 연결
Connection logDb = pool.getConnection(); // 4번째 연결

try {
// 데이터 동기화 작업 수행
} finally {
// 연결 반납
}
});
  • 실행 중인 스레드 수: 3개
  • 각 스레드가 필요로 하는 연결 수: 4개 (예: 여러 DB간 데이터 복사)
pool size = 3 x (4 - 1) + 1 = 10

5. 특수한 상황에서의 고려사항

5.1 트랜잭션 혼합 처리

  • 장시간 실행 트랜잭션과 매우 짧은 트랜잭션이 혼재된 시스템은 튜닝이 가장 어렵습니다. 이런 경우의 해결 방안:
  • 두 개의 별도 풀 인스턴스 생성
    • 장시간 실행 작업용 풀
    • "실시간" 쿼리용 풀

5.2 장시간 실행 트랜잭션 중심 시스템

  • 이런 시스템에서는 외부 제약 조건이 연결 수를 결정하는 경우가 많습니다:
    • 작업 실행 큐가 동시 실행 작업 수를 제한
    • 이 경우 풀 크기에 맞춰 작업 큐 크기를 조정해야 함
핵심 원칙

작은 풀을 유지하고, 연결을 기다리는 스레드로 풀이 포화되도록 설정하세요. 10,000명의 동시 사용자가 있다고 해서 10,000개의 연결이 필요한 것은 아닙니다.