이 이득에는 특정 비용이 있습니다. 트랜잭션 모드는 한 요청에서 다음 요청까지 안정적인 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 연결)는 꼭 필요한 시간 동안만 점유됩니다.
이 마지막 설명은 요점을 요약합니다. 트랜잭션 모드는 "내 애플리케이션 세션"과 "내 Postgres 연결" 사이의 링크를 의도적으로 끊기 때문에 작동합니다. 이 링크를 기반으로 한 모든 것이 중단됩니다. 다음 섹션에는 정확히 무엇인지 나열되어 있습니다.
트랜잭션 풀링 모드에서 중단되는 사항
공식 PgBouncer 문서에는 동일한 클라이언트의 두 요청 간에 서버 연결이 재활용되는 즉시 의미를 잃는 PostgreSQL 기능이 명시적으로 나열되어 있습니다.
| 영향을 받는 기능 | 왜 깨지나요? | 전형적인 증상 |
|---|---|---|
| 설정 / 세션 설정 | 설정은 즉시 재활용할 수 있는 연결에 적용됩니다. | 두 요청 사이에 매개변수가 무작위로 잊혀진 것 같습니다. |
| 듣기 / 알림 | 알림을 받기 위해 지속적인 연결을 가정합니다. | 클라이언트에게 알림이 전혀 전송되지 않거나 간헐적으로만 전송됩니다. |
| 세션 권고 잠금 | 잠금은 논리 클라이언트가 아닌 서버 연결에 의해 유지됩니다. | 예상 완료 전에 잠금이 해제되거나 절대 해제되지 않습니다. |
| WITH HOLD 슬라이더 | 그것을 개시한 거래 이후에도 살아남아야 합니다. | 다음 반복 시 "커서가 존재하지 않습니다" 오류 발생 |
| 임시 테이블 | 트랜잭션이 아닌 Postgres 세션과 관련됨 | 다음 쿼리에서 테이블이 "사라집니다" |
| 이름이 지정된 준비된 진술 | 특정 서버 연결에서 준비되고 다른 서버 연결에서 재생됨 | 로드 중 "준비된 문...이 존재하지 않습니다." |
이러한 제한 사항 중 대부분은 단일 연결이 일반적으로 모든 트래픽을 처리하는 로컬 개발에서는 나타나지 않습니다. 여러 클라이언트가 실제로 풀을 공유하고 서버 연결이 실제로 동일한 논리 클라이언트의 두 요청 사이에서 손을 바꾸는 실제 로드 상태에서 나타납니다. 연기 테스트에서는 거의 드러나지 않습니다.
준비된 진술: 가장 오해받는 한계
대부분의 최신 Postgres 드라이버는 애플리케이션 코드에서 명시적으로 요청하지 않고 기본적으로 프로토콜 측에서 명명된 요청을 준비합니다. 이것이 바로 이 함정을 예측하기 어렵게 만드는 이유입니다.
프로토콜 준비 문은 Parse시점에 특정 서버 연결에 이름이 지정되고 캐시됩니다. 트랜잭션 모드에서는 동일한 논리 클라이언트의 두 요청 사이에 이 연결을 다른 클라이언트에 재할당할 수 있습니다. 그런 다음 드라이버가 준비되지 않은 연결에서 동일한 명령문 이름을 재생하면 Postgres는 명시적 오류(일반적으로 sqlx 클라이언트의 경우 prepared statement "sqlx_s_N" does not exist)로 응답합니다. 동작은 간헐적입니다. 각 호출에서 재현할 수 있는 결정적 버그가 아니라 로드 시 연결이 어떻게 수행되는지에 따라 달라집니다.
클라이언트 측 수정은 언어에 관계없이 동일합니다. 트랜잭션 모드에서 풀러를 통과하는 모든 풀에 대해 명명된 준비된 문의 캐시를 비활성화하거나 명명되지 않은 쿼리를 강제 실행합니다. sqlx를 사용하는 Rust에서는 연결 옵션에서 statement_cache_capacity(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로 이동하는 모든 백엔드에 적용할 수 있는 간단한 체크리스트입니다.
- 애플리케이션 코드를 감사합니다. 비트랜잭션
SET,LISTEN/NOTIFY, 세션 권고 잠금,WITH HOLD커서 및 쿼리 간에 재사용되는 임시 테이블을 찾습니다. - 명시적 트랜잭션 내에서 세션 SET을 LOCAL SET로 바꿉니다. 이는 다음 연결에서 누출되지 않고 COMMIT/ROLLBACK에서 정리되므로 연결 재활용이 제대로 유지되는 유일한 설정입니다.
- 풀이 풀러를 통과하고 스키마 또는 역할이 한 요청에서 다른 요청으로 변경되는 경우 드라이버 측 준비된 명령문 캐시를 비활성화합니다. 성능 비용은 실제적이지만 측정 가능하며 테넌트 간 누출 위험보다 훨씬 낮습니다.
- 실제로 세션 모드(마이그레이션, 관리 스크립트, LISTEN/NOTIFY에 의존하는 모든 것)가 필요한 연결을 다른 모든 트래픽에 대해 트랜잭션 모드를 포기하는 대신 풀러가 아닌 직접 연결로 격리합니다.
- 다른 프로젝트에서 복사한 임의의 그림이 아닌 Postgres의 실제
max_connections에 상대적인 크기default_pool_size및max_client_conn. - 단순한 연기 테스트가 아닌 실제 부하에서 테스트합니다. 준비된 문 오류 및 세션 설정 누출은 단일 로컬 연결에서는 거의 나타나지 않습니다.
- 프로덕션 단계에서 PgBouncer 관리 콘솔에서
SHOW POOLS및SHOW STATS을 모니터링하여 클라이언트 측에 표시되기 전에 풀 포화 상태를 파악합니다.
항상 세션 대신 트랜잭션 모드를 선택해야 합니까?
아니요. 하지만 대부분의 REST API에 대한 올바른 기본 선택입니다. 세션 모드는 신속하게 리팩터링할 수 없는 세션 기능에 크게 의존하는 레거시 애플리케이션이나 풀링 이득이 마이그레이션 노력을 보상하지 못하는 낮은 트래픽의 경우 여전히 선호됩니다.
PgBouncer는 이 풀링 모델의 유일한 구현이 아닙니다. Supavisor(Supabase)와 PgCat은 부하 분산과 클러스터링에서 서로 다른 절충안을 가진 두 가지 최근 대안입니다. 토폴로지에 따라 세 가지 중에서 선택하려면 자세한 비교인 PgBouncer vs Supavisor vs PgCat를 참조하세요.
자주 묻는 질문
프로덕션에서 트랜잭션 모드가 활성화되면 가장 자주 나타나는 질문입니다.