최적의 데이터베이스 성능을 위한 연결 풀 미세 조정
James Reed
Infrastructure Engineer · Leapcell

소개
고성능 애플리케이션의 세계에서 데이터베이스 연결은 종종 중요한 병목 현상이 됩니다. 애플리케이션이 데이터베이스와 상호 작용해야 할 때마다 연결 설정 및 해제에 대한 오버헤드가 발생합니다. 높은 동시성과 빈번한 데이터베이스 액세스를 경험하는 애플리케이션의 경우, 이 오버헤드는 전체 시스템 성능과 리소스 활용도를 크게 저하시킬 수 있습니다. 이를 완화하기 위해 PgBouncer 및 내장된 애플리케이션 레벨 풀과 같은 연결 풀링 솔루션은 필수적입니다. 이러한 도구는 열려 있는 데이터베이스 연결 세트를 관리하여 애플리케이션이 각 요청에 대해 새 연결을 만드는 대신 재사용할 수 있도록 합니다. 그러나 연결 풀을 사용하는 것만으로는 충분하지 않습니다. 잘못 구성된 풀은 역설적으로 성능을 저하시키거나 근본적인 문제를 숨길 수 있습니다. 이 문서는 PgBouncer 및 애플리케이션 레벨 연결 풀의 매개변수를 최적화하여 최고의 데이터베이스 성능을 달성하고 애플리케이션이 원활하고 효율적으로 실행되도록 보장하는 기술과 과학을 탐구합니다.
핵심 개념 및 원칙
연결 풀링을 이해하는 데 기본적인 몇 가지 핵심 용어와 원칙을 정의한 후 최적화 전략에 대해 자세히 알아보겠습니다.
핵심 용어
- 연결 풀 (Connection Pool): 연결 풀링 구성 요소에서 유지 관리하는 데이터베이스 연결 캐시입니다. 애플리케이션에서 연결이 필요한 경우 풀에서 하나를 요청합니다. 사용 후 연결은 재사용을 위해 풀로 반환됩니다.
- PgBouncer: PostgreSQL을 위한 경량 단일 프로세스 연결 풀러입니다. 클라이언트 애플리케이션과 PostgreSQL 서버 사이에 위치하여 데이터베이스 서버의 연결 오버헤드를 크게 줄여줍니다.
- 애플리케이션 레벨 연결 풀 (Application-Level Connection Pool): 애플리케이션 코드 내 또는 라이브러리(예: Java용 HikariCP, Python용 SQLAlchemy의
QueuePool
)를 통해 직접 구현된 연결 풀입니다. - DB 서버의 연결 제한 (Connection Limit (on DB server)): PostgreSQL 서버가 수락하도록 구성된 최대 동시 데이터베이스 연결 수 (
postgresql.conf
의max_connections
). 이 제한을 초과하면 연결에 실패합니다. pool_size
(또는 애플리케이션 풀의maximum_pool_size
/max_connections
): 풀 자체가 유지 관리할 최대 데이터베이스 연결 수입니다. 이는 조정해야 할 중요한 매개변수입니다.min_pool_size
(또는minimum_idle
): 풀이 유지하려고 시도하는 최소 유휴 연결 수입니다. 이렇게 하면 트래픽이 적은 기간에도 연결을 쉽게 사용할 수 있습니다.idle_timeout
(또는max_idle_time
): 유휴 연결이 풀에서 닫히기 전에 유지되는 최대 시간입니다. 리소스 회수에 유용합니다.max_lifetime
(oconnection_timeout
PgBouncer에서,max_age
): 연결이 유휴 상태인지 또는 사용 중인지에 관계없이 풀에서 유지되는 최대 시간입니다. 오래된 연결 문제를 방지하거나 정기적인 재인증을 보장하는 데 유용합니다.wait_timeout
(oconnection_timeout
획득용): 사용 가능한 연결이 없을 때 클라이언트가 풀에서 연결을 가져오기 위해 대기하는 최대 시간입니다. 초과하면 시간 초과 오류가 반환됩니다.- PgBouncer의 연결 모드 (Connection Modes (PgBouncer)):
- 세션 풀링 (
pool_mode = session
): 연결은 "세션" 전체 기간(연결이 끊어질 때까지) 동안 클라이언트에 할당됩니다. 이것은 가장 안전한 모드이며 직접 연결과 가장 유사하게 작동합니다. - 트랜잭션 풀링 (
pool_mode = transaction
): 각 트랜잭션(COMMIT
또는ROLLBACK
) 후에 연결이 풀로 반환됩니다. 이 모드는 더 높은 연결 재사용률을 제공하지만 세션별 상태를 주의해서 처리해야 합니다. - 문장 풀링 (
pool_mode = statement
): 각 문장 후에 연결이 풀로 반환됩니다. 이 모드는 가장 높은 재사용률을 제공하지만 가장 제한적이며 세션 컨텍스트 손실로 인해 많은 애플리케이션과 호환되지 않는 경우가 많습니다.
- 세션 풀링 (
- Thundering Herd 문제: 제한된 수의 리소스만 사용 가능한 경우 많은 클라이언트가 동시에 리소스를 가져오려고 하면 경합 및 성능 저하로 이어집니다. 연결 풀은 이를 우아하게 관리해야 합니다.
최적화 원칙
연결 풀 최적화의 주요 목표는 다음 사항 간의 균형을 맞추는 것입니다.
- 연결 설정 오버헤드 최소화: 기존 연결을 재사용하여.
- 데이터베이스 리소스 활용도 극대화: 필요한 것보다 더 많은 연결을 유지하지 않음으로써.
- 데이터베이스 서버 과부하 방지: 총 활성 연결을 제한하여.
- 애플리케이션 응답성 보장: 연결을 신속하게 제공하거나 리소스가 실제로 고갈된 경우 신속하게 실패함으로써.
PgBouncer 매개변수 최적화
PgBouncer는 클라이언트 연결을 더 적은 수의 서버 연결로 다중화하는 PostgreSQL의 훌륭한 프런트엔드 역할을 합니다.
PgBouncer 구성 (pgbouncer.ini
)
[databases] mydb = host=127.0.0.1 port=5432 dbname=mydb auth_user=pgbouncer_user [pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 ; PgBouncer 자체의 연결 제한 max_client_conn = 1000 ; PgBouncer가 수락할 최대 클라이언트 연결 수. 높아야 함. default_pool_size = 20 ; 각 데이터베이스에 지정되지 않은 경우 풀의 기본 크기. ; pool_size = 20 ; 'mydb' 데이터베이스의 경우 특정하며 default_pool_size를 재정의함 ; 핵심 풀링 매개변수 pool_mode = transaction ; 대부분의 웹 애플리케이션에 가장 일반적임. 세션 상태가 있는 앱의 경우 'session'. reserve_pool_size = 2 ; 비상용으로 PgBouncer가 유지하는 연결. reserve_pool_timeout = 5.0 ; 클라이언트가 예약 연결을 기다리는 초. ; 서버 연결 관리 server_reset_query = DISCARD ALL ; 트랜잭션/문장 풀링에 중요함. server_check_delay = 10 ; PgBouncer가 서버 상태를 확인하는 빈도. server_lifetime = 3600 ; 서버 연결이 사용될 수 있는 최대 초. ; 클라이언트 연결 관리 client_idle_timeout = 300 ; 이 시간만큼 유휴 상태인 클라이언트 연결을 닫음. client_login_timeout = 60 ; 클라이언트가 로그인할 최대 시간. ; 고부하 튜닝 max_db_connections = 0 ; 서버의 *각 데이터베이스당* 최대 연결 수. 0은 PgBouncer로 무제한을 의미함(서버의 max_connections 사용). ; max_db_connections = 50으로 설정하고 pool_size = 20이면 PgBouncer는 DB에 20개의 연결만 사용함. ; max_db_connections = 20으로 설정하고 pool_size = 50이면 PgBouncer는 효과적으로 20으로 제한됨. ; 일반적으로 pool_size는 주어진 풀에 대한 DB의 효과적인 제한임. max_user_connections = 0 ; 서버 측의 *각 사용자당* 최대 연결 수. max_server_conn = 100 ; PgBouncer에서 모든 백엔드 서버로의 최대 연결 수. 모든 pool_size의 합보다 크거나 같아야 함.
PgBouncer의 최적화 전략:
pool_mode
:transaction
(대부분의 웹 앱에 권장): 뛰어난 연결 재사용률을 제공합니다. 애플리케이션 로직이 트랜잭션 간에 재설정되지 않는 세션별 변수 또는 임시 테이블에 의존하지 않도록 합니다.server_reset_query = DISCARD ALL
은 세션 상태를 정리하는 데 중요합니다.session
: 애플리케이션이 세션별 설정, 클라이언트가 관리하지 않는 준비된 문장 또는 여러 트랜잭션에 걸친 임시 테이블에 크게 의존하는 경우 사용합니다. 연결 재사용률은 낮지만 세션 일관성을 보장합니다.statement
(거의 사용되지 않음): 각 쿼리가 완전히 독립적인 애플리케이션에만 해당됩니다. 재사용률이 매우 높지만 세션 상태가 지속되면 오류가 발생하기 쉽습니다.
pool_size
: 이것이 가장 중요한 매개변수입니다.- 시작점: 좋은 경험 법칙은
(CPU 코어 수 * 2) + 실제 스핀들 수
입니다. 최신 SSD 기반 시스템의 경우(CPU 코어 수 * 2) + 실행 중인 워커 프로세스 수 / 2
를 고려하십시오. - 모니터링이 핵심: 합리적인 값(예: 중간 정도의 바쁜 애플리케이션의 경우 20-50)으로 시작한 다음 PgBouncer와 PostgreSQL 서버 모두에서 활성 연결을 모니터링합니다. PgBouncer의 큐잉(
SHOW STATS;
또는SHOW POOLS;
사용)을 관찰합니다. 클라이언트가 반복적으로 연결을 기다리는 경우pool_size
를 늘립니다. 데이터베이스 서버가 과부하 상태이면 줄입니다. - 데이터베이스 서버의
max_connections
를 초과하지 마십시오: 모든 PgBouncer 인스턴스가 단일 DB에 연결하는 모든pool_size
의 합은 이상적으로는max_connections - 일부 버퍼
보다 작아야 합니다.
- 시작점: 좋은 경험 법칙은
reserve_pool_size
: 작은 수(1-2)는 메인 풀이 포화될 때 비상 안전 장치 역할을 할 수 있습니다.server_lifetime
: 합리적인 값(예: 1시간,3600
초)으로 설정합니다. 이는 유휴 서버 연결을 주기적으로 닫고 다시 설정하여 오래 지속되는 오래된 연결이나 데이터베이스 측의 메모리 누수 문제를 완화합니다.client_idle_timeout
: 유휴 클라이언트 연결을 닫으면max_client_conn
슬롯이 해제됩니다. 애플리케이션의 가장 긴 예상 유휴 시간보다 약간 더 긴 값으로 설정합니다.max_client_conn
: PgBouncer에 연결되는 최대 예상 동시 클라이언트보다 높은 값으로 설정합니다. 이것은 백엔드 데이터베이스가 아닌 PgBouncer 자체에 대한 순수한 제한입니다.
애플리케이션 레벨 연결 풀 최적화
많은 프레임워크와 ORM은 내장된 연결 풀링을 제공합니다. Java용 HikariCP와 Python용 SQLAlchemy를 예로 들겠습니다.
HikariCP (Java Spring Boot 예제)
// application.properties 또는 application.yml spring.datasource.url=jdbc:postgresql://localhost:6432/mydb spring.datasource.username=myuser spring.datasource.password=mypassword # HikariCP 특정 구성 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.idle-timeout=300000 ; 5분 spring.datasource.hikari.max-lifetime=1800000 ; 30분 spring.datasource.hikari.connection-timeout=5000 ; 연결할 때까지 5초 spring.datasource.hikari.pool-name=MySpringBootHikariPool spring.datasource.hikari.auto-commit=true ; 일반적으로 웹 앱의 경우 true
HikariCP의 최적화 전략:
maximum-pool-size
(pool_size
와 동일):- 지침: 동일한
(CPU 코어 수 * 2) + 실제 스핀들 수
경험 법칙을 사용합니다. 그러나 PgBouncer를 통해 연결하는 경우 일반적으로 PostgreSQL에 직접 연결하는 경우보다 낮아야 합니다. 모든 애플리케이션 인스턴스의maximum-pool-size
합계와 다른 직접 연결은 해당 데이터베이스에 대한 PgBouncer의pool_size
또는 DB의max_connections
보다 작아야 합니다. - 모니터링: 데이터베이스 동시 연결, 애플리케이션 요청 지연 시간 및 "연결 대기 중" 메트릭을 모니터링합니다. 연결이 자주 소진되어 시간 초과가 발생하는 경우
maximum-pool-size
를 점진적으로 늘립니다. 데이터베이스 CPU 또는 I/O가 가벼운 애플리케이션 로드에서 높은 경우 줄입니다.
- 지침: 동일한
minimum-idle
: 합리적인 수(예: 5-10)로 설정합니다. 일부 유휴 연결을 준비해두면 트래픽 급증 시 새 연결을 시작할 필요가 줄어들지만 리소스 낭비를 피하기 위해 너무 높게 설정하지 마십시오.idle-timeout
:max-lifetime
보다 짧게 설정합니다. 진정으로 유휴 연결을 닫습니다. 좋은 값은 5-10분입니다.max-lifetime
: 오래된 연결을 방지하는 데 중요합니다. 데이터베이스의wait_timeout
또는 PgBouncer의server_lifetime
보다 짧게 설정합니다. 연결을 주기적으로 다시 시작하면 (DB 앞에 로드 밸런서를 사용하는 경우) 클러스터의 다른 데이터베이스 노드에 대한 로드 밸런싱을 보장하는 데 도움이 될 수 있습니다. 30-60분(1800000
-3600000
ms)과 같은 값이 일반적입니다.connection-timeout
: 애플리케이션이 풀에서 연결을 가져오기 위해 얼마나 오래 기다릴지를 결정합니다. 사용자 대면 애플리케이션의 경우 더 짧은 시간 초과(예: 5초)가 실패를 빠르게 처리하는 데 더 좋습니다. 백그라운드 작업의 경우 더 긴 시간 초과가 허용될 수 있습니다.
SQLAlchemy (Python 예제)
from sqlalchemy import create_engine from sqlalchemy.pool import QueuePool import os DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://myuser:mypassword@localhost:6432/mydb") engine = create_engine( DATABASE_URL, poolclass=QueuePool, pool_size=20, # 풀의 최대 연결 수 max_overflow=0, # pool_size를 초과하여 0을 허용함 (총 = pool_size + max_overflow) pool_timeout=10, # 연결을 가져오기 위해 최대 10초 대기 pool_recycle=1800, # 30분(1800초) 후 연결 재활용 pool_pre_ping=True # 사용하기 전에 연결 유효성 검사 ) # 사용 예 # with engine.connect() as connection: # result = connection.execute(text("SELECT 1")) # print(result.scalar())
SQLAlchemy의 최적화 전략:
pool_size
: HikariCP와 유사한 원칙입니다. 모니터링 및 워크로드에 따라 조정합니다. PgBouncer를 사용하는 경우 이pool_size
는 PgBouncer의pool_size
와 관련되어야 합니다.max_overflow
: 이 매개변수는 트래픽 급증을 처리하기 위해 풀이pool_size
를 일시적으로 초과할 수 있도록 허용합니다. 즉시 연결 고갈을 방지할 수 있지만pool_size
가 부족한 것을 숨기거나 데이터베이스에 더 많은 압력을 가할 수도 있습니다.0
또는 매우 작은 숫자로 유지하고pool_size
를 올바르게 계산하는 것이 더 안전한 경우가 많습니다.pool_timeout
: 풀에서 연결을 가져오기 위해 대기하는 시간입니다. 응답성(예: 5-10초)을 목표로 합니다.pool_recycle
:max_lifetime
과 동일합니다. 오래된 연결을 방지하는 데 중요합니다. 데이터베이스wait_timeout
보다 짧게 설정합니다.pool_pre_ping=True
: 유용하지만 각 연결 사용 전 또는pool_checkin
시점에 작은 오버헤드(SELECT 1
쿼리)가 추가됩니다. 연결이 살아 있음을 보장하여 오래된 연결로 인한 오류를 방지합니다. "서버가 예기치 않게 연결을 닫았습니다"와 같은 오류를 자주 접하는 경우 사용합니다.
두 유형의 풀에 대한 일반적인 모범 사례
- 모니터링, 모니터링, 모니터링: 이 점을 아무리 강조해도 지나치지 않습니다. 활성 연결, 유휴 연결, 연결 획득 시간, 대기 시간, 연결 시간 초과 및 데이터베이스 CPU/IO를 추적합니다.
pg_stat_activity
(PostgreSQL), PgBouncer의SHOW STATS
및SHOW POOLS
, 애플리케이션 레벨 메트릭(예: HikariCP 메트릭을 사용한 Prometheus)과 같은 도구를 사용합니다. pool_size
균형: 애플리케이션이 데이터베이스에서 가져오는 총 연결 수는 데이터베이스 서버의max_connections
를 초과해서는 안 됩니다. PgBouncer를 사용하는 경우 주어진 데이터베이스에 대한 모든 PgBouncerpool_size
의 합은 이상적으로 관리 작업에 대한 버퍼를 남겨두어야 합니다.pool_size
를 가능한 한 작게, 필요한 만큼 크게 유지: 더 큰 풀은 애플리케이션과 데이터베이스 모두에서 더 많은 메모리를 소비합니다. 과도한 유휴 연결 없이 최대 부하를 처리할 만큼 충분한 연결을 확보하는 스위트 스팟을 찾습니다.max_lifetime
/pool_recycle
사용: 이는 데이터베이스 재시작으로 인해 연결이 잘못된 상태로 남을 수 있는 일시적인 네트워크 문제 또는 데이터베이스 재시작으로 인한 문제를 방지하는 데 필수적입니다. 또한 데이터베이스 측의 메모리 관리에 도움이 됩니다.- 적절한
timeout
값 설정: 클라이언트가 연결을 위해 무기한 기다리지 않도록 합니다. 리소스가 실제로 사용할 수 없는 경우 신속하게 실패합니다. - 다계층 접근 방식 고려: 종종 PgBouncer와 애플리케이션 레벨 풀링을 조합하는 것이 최적입니다. PgBouncer는 많은 클라이언트에서 오는 높은 볼륨의 단기 연결을 처리하고, 애플리케이션 풀은 종종 PgBouncer에 대한 적은 수의 연결에 대해 애플리케이션 코드 내에서 강력한 관리를 제공합니다.
결론
PgBouncer 또는 애플리케이션 레벨 솔루션의 연결 풀 매개변수를 최적화하는 것은 모든 경우에 적용되는 작업이 아닙니다. 이는 애플리케이션의 워크로드에 대한 깊은 이해, 신중한 구성 및 지속적인 모니터링이 필요합니다. pool_size
, idle_timeout
, max_lifetime
과 같은 매개변수를 전략적으로 조정함으로써 데이터베이스 오버헤드를 크게 줄이고, 애플리케이션 응답성을 개선하며, 리소스 활용도를 효율적으로 보장하여 궁극적으로 더 성능이 뛰어나고 안정적인 시스템을 만들 수 있습니다. 성공의 열쇠는 리소스 가용성과 데이터베이스 안정성 간의 균형을 맞추고 관찰된 성능 메트릭을 기반으로 구성을 조정하는 데 적극적인 자세를 취하는 것입니다.