데이터베이스 연결 풀링의 필요성
-
데이터베이스 연결 생성 및 종료는 시간과 리소스가 많이 소요되는 작업입니다.
-
연결 풀은 미리 설정된 연결을 재사용하여 이러한 오버헤드를 줄이고 애플리케이션의 응답 속도를 향상시킵니다.
Active Record 연결 풀 구현
-
Active Record는 각 웹 및 백그라운드 프로세스(예: Puma, Sidekiq)마다 독립적인 연결 풀을 관리합니다.
-
Rails 7.2 이전: 웹 요청 또는 백그라운드 작업 완료 시까지 연결을 유지했습니다.
-
Rails 7.2 이후: 각 개별 쿼리마다 연결을 풀에서 대여하고 사용 후 즉시 반환하는 방식으로 변경되었습니다. 이는 I/O 작업이 많은 애플리케이션에서 연결 점유 시간을 줄여 동시 쿼리 실행 능력을 향상시킵니다.
-
쿼리 캐시는 이제 풀이 소유하며, 모든 연결이 공유하여 사용합니다.
주요 연결 풀 설정 옵션 (database.yml)
-
pool: 풀이 유지할 최대 연결 수.RAILS_MAX_THREADS에 기본 연결되지만, 데이터베이스가 허용하는 최대치로 설정하는 것이 일반적입니다. -
checkout_timeout: 스레드가 연결을 얻기 위해 기다리는 최대 시간 (기본 5초). 초과 시ActiveRecord::ConnectionTimeoutError발생. -
idle_timeout: 유휴 연결이 풀에서 제거되기 전까지의 시간 (기본 300초). -
reaping_frequency: Reaper가 유휴/죽은 연결을 제거하기 위해 실행되는 빈도 (기본 60초).
Active Record 연결 풀 Reaper
-
Reaper는 데이터베이스 재시작, 네트워크 문제 등으로 발생한 “죽은” 연결과
idle_timeout을 초과한 유휴 연결을 주기적으로 검사하고 제거합니다. -
유휴 연결은 불필요한 메모리 소비와 CPU 오버헤드를 유발하므로, Reaper는 풀의 건전성과 리소스 효율성에 중요합니다.
최대 데이터베이스 연결 수 계산
-
웹 프로세스 (Puma):
웹 다이노 수 * Puma 프로세스 수 * max_threads -
백그라운드 프로세스 (Sidekiq):
워커 다이노 수 * Sidekiq 프로세스 수 * concurrency -
load_async사용 시:프로세스 레벨 동시성 + global_executor_concurrency + 1(메인 스레드 + 비동기 쿼리 스레드) -
데이터베이스 플랜의 최대 동시 연결 수를 초과하지 않도록 계산하고 설정해야 합니다. Preboot 활성화 시에는 두 배의 연결이 필요할 수 있습니다.
ActiveRecord::ConnectionTimeoutError 원인 및 해결
-
풀 크기 부족:
pool설정이 애플리케이션의 동시성보다 낮을 때 발생. -
사용자 정의 스레드:
Thread.new등으로 생성된 스레드가 연결을 점유하는 경우. -
Active Storage 프록시 모드: 스트리밍 응답을 위해 추가 스레드와 연결을 사용합니다.
-
Rack Timeout: 요청 종료 시 데이터베이스 작업 중 예외가 발생하면 연결 정리가 제대로 이루어지지 않을 수 있습니다.
-
해결:
pool크기를 데이터베이스 플랜의 최대 허용치로 설정하고, 에러 발생 시 연결 풀 상세 정보를 로깅하여 문제 스레드를 식별하는 것이 중요합니다.
PgBouncer의 활용
-
PgBouncer는 PostgreSQL을 위한 외부 연결 풀러로, Active Record 풀이 단일 프로세스 내에서 작동하는 것과 달리, 애플리케이션과 데이터베이스 사이에 위치하여 모든 애플리케이션 프로세스에 걸쳐 연결을 관리합니다.
-
이는 데이터베이스 연결 한계에 도달하는 대규모/고동시성 애플리케이션에 효과적인 솔루션입니다.
연결 풀 통계 모니터링
rufus-schedulerGem 등을 사용하여 주기적으로 Active Record 연결 풀 통계를 수집하고 New Relic과 같은 APM 서비스로 전송하여 시각적으로 모니터링함으로써 잠재적 문제를 조기에 파악할 수 있습니다.