PROD주권 유럽 BaaS 플랫폼대시보드 열기 →

성능 · 10분 읽음

PgBouncer 트랜잭션 풀링 설명

Affane Daylami · Fondateur · 2026년 6월 12일

블로그로 돌아가기

PgBouncer의 트랜잭션 모드는 클라이언트 연결이 끊어질 때가 아니라 각 트랜잭션이 끝날 때 PostgreSQL 연결을 해제합니다. 이것이 수십 개의 실제 서버 연결을 통해 수천 개의 HTTP 클라이언트에 서비스를 제공하는 것을 가능하게 하며 모든 짧은 쿼리 REST API에 권장되는 모드입니다.

이 영어 텍스트는 프랑스어 원본에서 자동으로 생성되었으며 아직 검토되지 않았습니다.
이 페이지는 자동으로 번역되었습니다. 영어 버전은 권위가 있습니다.

이 이득에는 특정 비용이 있습니다. 트랜잭션 모드는 한 요청에서 다음 요청까지 안정적인 Postgres 연결을 가정하는 모든 것을 자동으로 중단합니다. 세션 SET, LISTEN/NOTIFY, 권고 잠금, 트랜잭션 후에도 유지되는 커서, 준비된 명령문으로 명명됩니다. 이 기사에서는 메커니즘을 자세히 설명하고 정확한 증상과 함께 이러한 제한을 나열한 다음 프로덕션 백엔드(리포지토리에서 직접 확인된 우리)가 트랩되지 않고 이를 구성하는 방법을 보여줍니다. 여기에 인용된 성능 수치의 측정 방법은 벤치마크 방법론을 참조하세요.

필수사항

  • 트랜잭션 모드: 클라이언트 연결이 끊어질 때가 아니라 각 트랜잭션이 끝날 때 서버 연결이 해제됩니다. 이는 짧은 REST API 유형 연결을 공유하는 가장 효율적인 모드입니다.
  • 구성상 호환되지 않는 항목: 세션 SET/RESET, LISTEN/NOTIFY, 세션 권고 잠금, WITH HOLD 커서, 한 요청에서 다른 요청으로 재사용되는 임시 테이블.
  • 실제로 가장 일반적인 함정: 여러 드라이버(sqlx, asyncpg, JDBC pgjdbc 드라이버)가 기본적으로 활성화하는 명명된 준비된 문은 다른 서버 연결에서 재생될 수 있으며 로드 시 prepared statement does not exist와 같은 오류를 트리거할 수 있습니다.
  • 버전 1.21부터 PgBouncer는 트랜잭션 모드(서버 연결을 통한 LRU 캐시)에서 프로토콜 준비 명령문을 따를 수 있습니다. 이는 애플리케이션이 각 요청에서 search_path을 변경하는 경우 클라이언트 측 캐시를 비활성화하는 것을 방지하지 않습니다.
  • Aurabase 코드에서 확인되었습니다. 테넌트 풀은 pool_mode=transaction에서 statement_cache_capacity(0) 및 PgBouncer로 실행되는 반면 PostgREST는 LISTEN/NOTIFY를 통해 스키마를 다시 로드하기 위해 자발적으로 직접 연결을 유지합니다.
#
개념

PgBouncer의 3가지 풀링 모드

PgBouncer는 Postgres 서버 연결이 공통 풀로 반환될 때만 다른 세 가지 모드를 제공합니다. 공식 문서에서는 session, transaction 및 statement(pgbouncer.org/features.html, "풀링 모드" 섹션, 2026년 8월 24일 액세스)으로 명명합니다.

패션서버 연결이 느슨함세션 호환성
세션(기본값)클라이언트 연결 끊김 시전체: SET, LISTEN, 커서, 모든 것이 라이브처럼 작동합니다.
거래각 트랜잭션이 끝날 때(COMMIT/ROLLBACK)부분적: 트랜잭션에 로컬로 남아 있는 것만
성명서개별 요청 후최소: 명시적인 다중 쿼리 트랜잭션 금지

세션 모드는 가장 허용적이지만 확장성 측면에서 가장 효과적이지 않습니다. Postgres 연결은 두 요청 사이에 아무 작업도 수행하지 않더라도 연결되어 있는 한 클라이언트용으로 예약된 상태로 유지됩니다. 명령문 모드는 매우 특정한 경우(읽기 전용 프록시, 상태 확인)를 위해 예약되어 있으며 고전적인 명시적 트랜잭션도 중단합니다. 트랜잭션 모드는 실제로 REST API를 지배하는 절충안입니다. 각 HTTP 요청은 일반적으로 단일 짧은 Postgres 트랜잭션에 해당합니다.

#
메커니즘

트랜잭션 모드 작동 방식, 연결별 연결

트랜잭션 모드에서 PgBouncer는 클라이언트가 트랜잭션을 열 때만 클라이언트에 서버 연결을 연결하고 COMMIT 또는 ROLLBACK 시 이를 풀에 반환합니다. 두 트랜잭션 사이에서 동일한 클라이언트가 완전히 다른 서버 연결에 다시 할당될 수 있습니다.

구체적으로 20의 default_pool_size를 사용하면 PgBouncer는 특정 시간에 실제로 진행 중인 트랜잭션이 소수인 수백 명의 동시 클라이언트를 흡수할 수 있습니다. 트래픽은 많지만 트랜잭션이 짧은 REST API에 대한 트랜잭션 모드를 정당화하는 것은 바로 이 비율입니다. 희귀한 리소스(서버 측 메모리가 비싼 Postgres 연결)는 꼭 필요한 시간 동안만 점유됩니다.

pgbouncer.iniini
[pgbouncer]
listen_port = 5432

; La connexion serveur est libérée dès la fin de chaque transaction
pool_mode = transaction

default_pool_size = 80
max_client_conn = 1000

; Ne pas utiliser avec des sessions stateful (SET, advisory locks, LISTEN/NOTIFY)

이 마지막 설명은 요점을 요약합니다. 트랜잭션 모드는 "내 애플리케이션 세션"과 "내 Postgres 연결" 사이의 링크를 의도적으로 끊기 때문에 작동합니다. 이 링크를 기반으로 한 모든 것이 중단됩니다. 다음 섹션에는 정확히 무엇인지 나열되어 있습니다.

#
한도

트랜잭션 풀링 모드에서 중단되는 사항

공식 PgBouncer 문서에는 동일한 클라이언트의 두 요청 간에 서버 연결이 재활용되는 즉시 의미를 잃는 PostgreSQL 기능이 명시적으로 나열되어 있습니다.

영향을 받는 기능왜 깨지나요?전형적인 증상
설정 / 세션 설정설정은 즉시 재활용할 수 있는 연결에 적용됩니다.두 요청 사이에 매개변수가 무작위로 잊혀진 것 같습니다.
듣기 / 알림알림을 받기 위해 지속적인 연결을 가정합니다.클라이언트에게 알림이 전혀 전송되지 않거나 간헐적으로만 전송됩니다.
세션 권고 잠금잠금은 논리 클라이언트가 아닌 서버 연결에 의해 유지됩니다.예상 완료 전에 잠금이 해제되거나 절대 해제되지 않습니다.
WITH HOLD 슬라이더그것을 개시한 거래 이후에도 살아남아야 합니다.다음 반복 시 "커서가 존재하지 않습니다" 오류 발생
임시 테이블트랜잭션이 아닌 Postgres 세션과 관련됨다음 쿼리에서 테이블이 "사라집니다"
이름이 지정된 준비된 진술특정 서버 연결에서 준비되고 다른 서버 연결에서 재생됨로드 중 "준비된 문...이 존재하지 않습니다."
함정이 항상 즉각적으로 발생하는 것은 아닙니다.

이러한 제한 사항 중 대부분은 단일 연결이 일반적으로 모든 트래픽을 처리하는 로컬 개발에서는 나타나지 않습니다. 여러 클라이언트가 실제로 풀을 공유하고 서버 연결이 실제로 동일한 논리 클라이언트의 두 요청 사이에서 손을 바꾸는 실제 로드 상태에서 나타납니다. 연기 테스트에서는 거의 드러나지 않습니다.

#
일반적인 함정

준비된 진술: 가장 오해받는 한계

대부분의 최신 Postgres 드라이버는 애플리케이션 코드에서 명시적으로 요청하지 않고 기본적으로 프로토콜 측에서 명명된 요청을 준비합니다. 이것이 바로 이 함정을 예측하기 어렵게 만드는 이유입니다.

프로토콜 준비 문은 Parse시점에 특정 서버 연결에 이름이 지정되고 캐시됩니다. 트랜잭션 모드에서는 동일한 논리 클라이언트의 두 요청 사이에 이 연결을 다른 클라이언트에 재할당할 수 있습니다. 그런 다음 드라이버가 준비되지 않은 연결에서 동일한 명령문 이름을 재생하면 Postgres는 명시적 오류(일반적으로 sqlx 클라이언트의 경우 prepared statement "sqlx_s_N" does not exist)로 응답합니다. 동작은 간헐적입니다. 각 호출에서 재현할 수 있는 결정적 버그가 아니라 로드 시 연결이 어떻게 수행되는지에 따라 달라집니다.

클라이언트 측 수정은 언어에 관계없이 동일합니다. 트랜잭션 모드에서 풀러를 통과하는 모든 풀에 대해 명명된 준비된 문의 캐시를 비활성화하거나 명명되지 않은 쿼리를 강제 실행합니다. sqlx를 사용하는 Rust에서는 연결 옵션에서 statement_cache_capacity(0)를 거칩니다.

pool.rsrust
let connect_options = url
    .parse::<PgConnectOptions>()?
    .statement_cache_capacity(0);

// 해당 항목: asyncpg ->statement_cache_size=0, pgjdbc -> prepareThreshold=0

버전 1.21부터 PgBouncer는 서버 측 문제의 일부를 완화합니다. 트랜잭션 모드에서 프로토콜 준비 명령문을 따르고 max_prepared_statements를 통해 크기가 조정되는 연결당 LRU 캐시를 사용하여 할당된 연결에서 즉시 준비할 수 있습니다. 이렇게 하면 누락 횟수가 줄어들지만 search_path가 한 요청에서 다른 요청으로 변경되는 다중 테넌트 풀에서 클라이언트 캐시를 비활성화하지 않아도 됩니다. 캐시된 계획은 Parse시점에 확인된 테이블의 내부 식별자(OID)를 고정하고 다른 스키마에서 이를 재생하면 단순한 오류가 아닌 잘못된 테넌트의 데이터가 반환될 수 있습니다.

#
체크인된 코드

Aurabase가 트랜잭션 모드에서 PgBouncer를 구성하는 방법

Aurabase 저장소는 공유 데이터 플레인(deploy/helm/aurabase/templates/infra/pgbouncer.yaml) 앞에 PgBouncer를 pool_mode=transaction로 배포하고 테넌트의 각 전용 Postgres 인스턴스(deploy/cnpg/tenant-pooler.yaml) 앞에 동일하게 구성된 Pooler CNPG를 배포합니다. 두 경로 모두 위에서 설명한 것과 동일한 원칙을 적용합니다.

소스 코드에는 안정성 이유뿐만 아니라 이러한 선택에 대한 구체적인 보안 이유가 문서화되어 있습니다. 테넌트 간에 공유되는 Postgres 풀은 재사용된 연결에서 프로젝트마다 다른 search_path을 배치합니다. 캐시된 준비된 문은 Parse시점에 해결된 테이블의 OID를 고정합니다. 동일한 연결의 다른 테넌트에 대해 이를 재생하면 단순한 애플리케이션 오류가 아니라 격리 우회인 첫 번째 테넌트의 스키마에 대해 쿼리가 실행됩니다. 따라서 statement_cache_capacity(0)는 트랜잭션 모드에서 CNPG 풀러를 통과하는 전용 인스턴스를 포함하여 예외 없이 적용됩니다.

두 번째 세션 위생 조치: 각 연결이 풀로 반환되면 후크가 DISCARD ALL를 실행합니다(설정 재설정, 서버 측에서 준비된 명령문 할당 취소, 권고 잠금 해제, 커서 및 임시 테이블 제거). 이 후크가 없으면 이전 요청으로 인한 세션 잔여물이 동일한 재활용 연결을 재사용하는 다른 테넌트의 다음 요청에서 누출될 수 있습니다.

Exception accepted: dedicated PostgREST instances remain in direct connection to the primary, without going through the pooler. PostgREST schema reloading relies on LISTEN/NOTIFY, which assumes a persistent connection, exactly the functionality that transaction mode breaks (detail already documented in our article on PostgREST compatibility at Aurabase). RLS settings per request are passed to SET LOCAL within an explicit transaction, the only way to remain compatible with a pool that can change server connections at any COMMIT (see our article onmulti-tenant RLS isolation).

#
실용 가이드

애플리케이션을 중단하지 않고 트랜잭션 모드를 활성화하세요.

직접 Postgres 연결에서 트랜잭션 모드의 PgBouncer로 이동하는 모든 백엔드에 적용할 수 있는 간단한 체크리스트입니다.

  1. 애플리케이션 코드를 감사합니다. 비트랜잭션 SET, LISTEN/NOTIFY, 세션 권고 잠금, WITH HOLD 커서 및 쿼리 간에 재사용되는 임시 테이블을 찾습니다.
  2. 명시적 트랜잭션 내에서 세션 SET을 LOCAL SET로 바꿉니다. 이는 다음 연결에서 누출되지 않고 COMMIT/ROLLBACK에서 정리되므로 연결 재활용이 제대로 유지되는 유일한 설정입니다.
  3. 풀이 풀러를 통과하고 스키마 또는 역할이 한 요청에서 다른 요청으로 변경되는 경우 드라이버 측 준비된 명령문 캐시를 비활성화합니다. 성능 비용은 실제적이지만 측정 가능하며 테넌트 간 누출 위험보다 훨씬 낮습니다.
  4. 실제로 세션 모드(마이그레이션, 관리 스크립트, LISTEN/NOTIFY에 의존하는 모든 것)가 필요한 연결을 다른 모든 트래픽에 대해 트랜잭션 모드를 포기하는 대신 풀러가 아닌 직접 연결로 격리합니다.
  5. 다른 프로젝트에서 복사한 임의의 그림이 아닌 Postgres의 실제 max_connections에 상대적인 크기 default_pool_size 및 max_client_conn .
  6. 단순한 연기 테스트가 아닌 실제 부하에서 테스트합니다. 준비된 문 오류 및 세션 설정 누출은 단일 로컬 연결에서는 거의 나타나지 않습니다.
  7. 프로덕션 단계에서 PgBouncer 관리 콘솔에서 SHOW POOLS 및 SHOW STATS을 모니터링하여 클라이언트 측에 표시되기 전에 풀 포화 상태를 파악합니다.
#
결정

항상 세션 대신 트랜잭션 모드를 선택해야 합니까?

아니요. 하지만 대부분의 REST API에 대한 올바른 기본 선택입니다. 세션 모드는 신속하게 리팩터링할 수 없는 세션 기능에 크게 의존하는 레거시 애플리케이션이나 풀링 이득이 마이그레이션 노력을 보상하지 못하는 낮은 트래픽의 경우 여전히 선호됩니다.

PgBouncer는 이 풀링 모델의 유일한 구현이 아닙니다. Supavisor(Supabase)와 PgCat은 부하 분산과 클러스터링에서 서로 다른 절충안을 가진 두 가지 최근 대안입니다. 토폴로지에 따라 세 가지 중에서 선택하려면 자세한 비교인 PgBouncer vs Supavisor vs PgCat를 참조하세요.

#
자주 묻는 질문

자주 묻는 질문

프로덕션에서 트랜잭션 모드가 활성화되면 가장 자주 나타나는 질문입니다.

PgBouncer 트랜잭션 풀링 모드란 무엇입니까?+
이는 클라이언트 연결이 끊어질 때가 아니라 각 트랜잭션이 끝날 때 Postgres 연결이 다른 클라이언트에 재할당되는 PgBouncer(세션 및 명령문 포함)의 3가지 모드 중 하나입니다. 이를 통해 실제로 열려 있는 Postgres 연결보다 더 많은 경쟁 클라이언트에 서비스를 제공할 수 있습니다.
준비된 문이 트랜잭션 모드에서 충돌하는 이유는 무엇입니까?+
명명된 준비된 문은 특정 서버 연결에서 준비됩니다. 트랜잭션 모드에서는 두 요청 사이에 이 연결을 다른 클라이언트에 다시 할당할 수 있습니다. 드라이버가 준비되지 않은 연결에서 명령문 이름을 재생하는 경우 Postgres는 준비된 명령문이 존재하지 않는다는 오류를 반환합니다. 수정 사항은 드라이버 측에서 준비된 명령문 캐시를 비활성화하는 것으로 구성됩니다(sqlx의 경우 statement_cache_capacity(0), asyncpg의 경우 state_cache_size=0).
트랜잭션 모드에서 PgBouncer 뒤에서 LISTEN/NOTIFY를 사용할 수 있나요?+
아니요, 안정적이지 않습니다. LISTEN/NOTIFY는 알림을 받기 위해 지속적인 연결을 가정하지만 트랜잭션 모드에서는 이를 보장하지 않습니다. 표준 관행은 풀러 외부의 Postgres에 대한 직접 연결을 통해 LISTEN/NOTIFY(예: PostgREST)에 의존하는 구성 요소를 전달하는 것입니다.
트랜잭션 모드에서 SET 대신 SET LOCAL을 사용해야 합니까?+
예, 주어진 요청에 적용되어야 하는 모든 설정에 대해 체계적으로 적용됩니다. SET LOCAL은 COMMIT 또는 ROLLBACK 시 자동으로 정리되므로 두 트랜잭션 간에 변경될 수 있는 서버 연결을 안전하게 사용할 수 있습니다. 클래식 SET은 동일한 재활용 서버 연결을 복구하는 다음 클라이언트로 누출될 수 있습니다.
트랜잭션 모드가 RLS(행 수준 보안)와 함께 작동합니까?+
예, RLS 정책에서 사용하는 JWT 클레임 또는 세션 변수가 세션 SET이 아닌 트랜잭션 내부의 LOCAL SET에 설정되어 있다면 가능합니다. 이는 다중 테넌트 RLS 격리에 대한 문서에 설명된 패턴입니다.
PgBouncer, Supavisor, PgCat: 어느 것을 선택해야 할까요?+
세 가지 모두 유사한 풀링 모델을 구현하지만 클러스터링, 로드 분산 및 생태계에 차이가 있습니다(Supavisor는 Supabase에서 개발하고 PgCat은 Rust로 작성함). 선택은 무엇보다도 배포 토폴로지와 기존 운영 제약 조건에 따라 달라집니다. 자세한 내용은 전용 비교를 참조하세요.

배포할 준비가 되셨나요?

5분 만에 백엔드를 완성하세요.

신용카드 불필요 · 500MB 무료 · 50,000 MAU