PostgreSQL 실전 강좌 #2 커넥션 풀: max_connections의 오해와 PgBouncer

4 분 소요

서비스가 크면서 처음 만나는 PostgreSQL 운영 문제는 대개 쿼리가 아니라 커넥션입니다. 증상은 정해져 있습니다. FATAL: sorry, too many clients already, 또는 커넥션 수는 여유인데 응답이 느려지는 현상입니다. 이번 편은 이 문제의 구조와 표준 해법인 커넥션 풀을 다룹니다.

PostgreSQL 커넥션은 비쌉니다 #

전제가 되는 사실 하나. PostgreSQL은 커넥션마다 서버 프로세스를 하나씩 만듭니다. 스레드가 아니라 프로세스입니다. 접속마다 fork 비용과 수 MB의 메모리가 들고, 커넥션 수가 늘수록 프로세스 간 조정 비용이 커집니다. 그래서 두 가지 귀결이 나옵니다. 첫째, 요청마다 접속하고 끊는 패턴은 재앙입니다(접속 비용이 쿼리 비용을 압도합니다). 둘째, max_connections를 5000으로 올리는 것은 답이 아닙니다.

두 번째가 반직관적입니다. 벤치마크로 반복 확인되는 사실은, 동시 활성 커넥션이 CPU 코어 수의 몇 배를 넘어가면 총 처리량이 오히려 떨어진다는 것입니다. 코어 16개인 서버가 실제로 동시에 실행할 수 있는 쿼리는 결국 십몇 개이고, 그 이상의 커넥션은 컨텍스트 스위칭과 잠금 경합만 보탭니다. 커넥션 1000개가 필요해 보이는 서비스의 실제 요구는 “1000개의 클라이언트가 십몇 개의 실행 슬롯을 순서 좋게 나눠 쓰는 것"이고, 그 나눠 쓰기를 해 주는 것이 풀입니다.

1층: 애플리케이션 풀 #

첫 번째 층은 앱 프레임워크의 풀입니다(HikariCP, SQLAlchemy의 풀, Go database/sql 등). 서버 시작 때 커넥션 N개를 만들어 두고 요청들이 빌려 쓰는 구조로, “요청마다 접속"을 없애 줍니다. SQLAlchemy 강좌에서 다룬 그 풀입니다. 앱 인스턴스가 몇 개 안 되면 이것으로 충분합니다.

문제는 수평 확장입니다. 인스턴스당 풀 20개 × 인스턴스 50개 = 커넥션 1000개가 되어, 각자는 예의 바른 풀인데 합계가 DB를 짓누릅니다. 서버리스(Lambda 등)라면 인스턴스 수 자체가 통제 불능이라 더 심합니다. 이때 필요한 것이 두 번째 층입니다.

2층: PgBouncer 같은 외부 풀러 #

PgBouncer는 앱과 DB 사이에 서는 경량 프록시입니다. 앱들에게는 커넥션을 넉넉히 내주고, DB로는 소수의 실제 커넥션만 유지합니다. 핵심 설정은 풀 모드입니다.

모드실제 커넥션을 돌려주는 시점특징
session클라이언트 접속 종료 시호환성 최고, 절약 효과 최소
transaction트랜잭션 끝날 때마다실무 표준. 유휴 클라이언트가 실제 커넥션을 점유하지 않음
statement문장마다제약이 커서 특수 용도

실무의 기본값은 transaction 모드입니다. 웹 앱 커넥션의 대부분은 “빌려 놓고 노는” 시간이므로, 트랜잭션 단위로만 실제 커넥션을 점유하게 하면 클라이언트 수천을 실제 커넥션 수십으로 받을 수 있습니다. 대가는 명확합니다. 트랜잭션이 끝나면 다음에는 다른 실제 커넥션을 받을 수 있으므로, 세션에 상태를 남기는 기능을 쓸 수 없습니다. 세션 수준 프리페어드 스테이트먼트, SET으로 바꾼 세션 변수, 세션 단위 어드바이저리 락, LISTEN 등이 여기에 걸립니다(드라이버·PgBouncer 최신 버전에서 프리페어드 스테이트먼트 지원이 개선되어 왔지만, “세션 상태는 못 쓴다"를 기본 감각으로 두는 것이 안전합니다).

참고로 관리형 환경에서는 같은 역할을 하는 상품이 따로 있습니다. AWS라면 RDS Proxy가 그 자리입니다.

적정 크기의 감각 #

그럼 실제 커넥션(풀 크기)은 몇이 좋은가. 출발점으로 널리 쓰이는 어림식은 코어 수 × 2〜4 근처에서 시작해 실측으로 조정하는 것입니다(디스크 대기가 많은 워크로드일수록 큰 쪽). 중요한 것은 방향입니다. 성능이 아쉬울 때 풀을 줄여서 나아지는 경우가 실제로 많습니다. 판단 재료는 다음 편에서 다룰 관측입니다. pg_stat_activity에서 활성/유휴 커넥션의 비율을 보고, 대부분이 idle이라면 커넥션이 남는 것이지 모자란 것이 아닙니다.

커넥션 상태 집계
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;

정리 #

  • PostgreSQL 커넥션은 프로세스 하나입니다. 요청마다 접속은 금물이고, max_connections 증설은 해법이 아닙니다.
  • 동시 활성 커넥션이 코어 수의 몇 배를 넘으면 처리량이 오히려 떨어집니다. 필요한 것은 커넥션이 아니라 순서 좋은 공유입니다.
  • 1층은 앱 풀입니다. 인스턴스가 늘어 합계가 커지면 2층, PgBouncer 같은 외부 풀러를 세웁니다.
  • PgBouncer는 transaction 모드가 실무 표준입니다. 대신 세션 상태 기능(SET, 세션 프리페어드 등)을 포기합니다.
  • 풀 크기는 코어 × 2〜4에서 시작해 실측 조정합니다. pg_stat_activity의 idle 비율이 판단 재료이고, 이 관측 루틴이 다음 편의 주제입니다.
X