
Postgres 커넥션 풀러 얘기다. 15년 넘게 PgBouncer가 사실상 표준이었다. 근데 최근 몇 년 사이에 pgcat(Rust), Supavisor(Elixir) 같은 대안이 부쩍 눈에 띈다. 우리 팀에서도 지난 분기에 새 서비스용 풀러를 뭘로 갈지 논의가 있었고, 실제로 각각을 조금씩 다르게 굴려봤다. 이 글은 그 경험을 바탕으로 세 개를 나열해보는 글이다. "이거 써라"는 아니고, 어떤 상황에서 뭐가 맞더라 정도의 정리.
세 개가 각자 어떤 물건인가
PgBouncer는 여전히 단일 바이너리에 2MB짜리 프로세스로 커넥션 수천 개를 받아낸다. 트랜잭션 모드 풀링, 심플한 config, 문서화 잘 되어 있고, 튜닝 노하우가 인터넷에 널려있다. 단점은 단일 스레드라는 것. CPU 하나를 다 태우면 그게 한계다. so_reuseport 옵션이 붙으면서 여러 인스턴스를 같은 포트에 띄우는 게 가능해졌지만, 어쨌든 근본적으로 프로세스 하나 = 코어 하나 모델이다.
pgcat은 Rust로 다시 쓴 물건이다. 멀티스레드로 돌아가고, 읽기/쓰기 분리(query routing)와 샤딩을 config로 정의할 수 있다. PgBouncer 프로토콜과 호환되어서 클라이언트 쪽 코드 변경 없이 넘어갈 수 있다는 게 큰 매력이다. 최근 벤치마크를 보면 클라이언트 수가 50개를 넘어가면 pgcat이 PgBouncer보다 처리량이 확연히 좋아진다. 다만 운영 성숙도는 PgBouncer만 못하다. issue 트래커 보면 아직 엣지 케이스가 종종 나온다.
Supavisor는 Supabase가 만든 Elixir(BEAM VM) 기반 풀러다. 얘는 성격이 좀 다르다. 커넥션을 수십만 개 받는다는 걸 자랑거리로 내세운다. Erlang VM의 라이트웨이트 프로세스 모델을 그대로 가져와서 클라이언트 커넥션당 프로세스 하나를 띄우는 구조인데, 이게 서버리스나 엣지 환경처럼 순간적으로 많은 커넥션이 확 들어오는 워크로드에 잘 맞는다. 대신 트랜잭션 하나당 레이턴시는 PgBouncer/pgcat보다 확실히 높다.
성능은 실제로 얼마나 차이나나
우리 팀에서 스테이징에 세 개를 다 붙여서 pgbench로 돌려봤다. 조건은 다음과 같다.
- Postgres 16.4, r6i.xlarge (4 vCPU, 32GB)
- 풀러: c6i.large (2 vCPU) 인스턴스 하나에 각각 배포
- 쿼리: pgbench SELECT-only, 5분씩
결과를 대충 요약하면 이렇다. 클라이언트 20개까지는 PgBouncer가 tps도 제일 높고 P99 레이턴시도 제일 낮다. 100개 클라이언트 지점부터는 pgcat이 앞선다. Supavisor는 두 경우 다 지고 있는데, 500개, 1000개로 늘리면 그때부터 얘만 살아남는다. PgBouncer는 이 지점에서 이미 CPU 100%를 찍고 큐가 쌓인다.
숫자만 놓고 보면 pgcat이 이겨 보이지만, 함정이 하나 있다. 일반적인 백엔드 서비스에서 클라이언트 커넥션이 100개를 넘는 경우는 생각보다 드물다. 파드 30개에 각 커넥션 풀 10개면 300개 아니냐고 할 수 있는데, 그 300개가 동시에 활성 상태인 경우가 거의 없다. 우리도 실측해보니 피크 타임에도 활성 트랜잭션은 30~50개 수준이었다. 그 구간에서는 PgBouncer가 여전히 최강이다.
그래서 우리가 어떻게 결정했나
내부 논의 결과 상황별로 셋을 다 쓰기로 했다.
메인 서비스, 트래픽은 크지만 활성 커넥션은 수십 개 수준인 API 서버들 앞에는 PgBouncer를 그대로 둔다. 굳이 바꿀 이유가 없다. pool_mode = transaction, default_pool_size = 20 정도의 익숙한 설정으로 몇 년째 안정적이다. 새 도구를 학습하는 비용보다 얻는 게 없다는 결론.
새로 만드는 데이터 파이프라인, ETL 워커 쪽에는 pgcat을 도입한다. 여기는 활성 커넥션이 진짜 많다. 배치 잡 여러 개가 동시에 커넥션을 잡고, 읽기 전용 replica로 라우팅해야 하는 쿼리도 많다. pgcat의 query routing과 멀티스레드가 여기서 빛난다. postgres://user:pass@pgcat:6432/shard_0?application_name=etl 이런 식으로 붙는데, PgBouncer로 이걸 하려면 인스턴스를 여러 개 띄우거나 애플리케이션에서 라우팅해야 해서 더 복잡했을 거다.
Supavisor는 후보에서 뺐다. 이유는 두 개인데, 첫째로 우리 워크로드가 서버리스가 아니라서 얘의 강점을 못 쓴다. 둘째로 Elixir 스택은 팀 안에서 아무도 안 다뤄봤다. 뭐가 터졌을 때 디버깅할 사람이 없는 게 제일 무섭다. Lambda/Cloud Run처럼 커넥션이 폭발적으로 튀는 환경이라면 진지하게 봤을 텐데, 지금은 아니다.
실제 도입할 때 주의할 것들
pgcat을 도입하면서 발견한 것 몇 개.
pool_mode = transaction에서 prepared statement가 여전히 문제다. PgBouncer 1.21부터 프로토콜 레벨 prepared statement 지원이 개선됐지만, 라이브러리마다 다르게 구현하기 때문에 클라이언트 쪽 설정이 여전히 필요하다. pgcat도 마찬가지. JDBC의 prepareThreshold=0, asyncpg의 statement_cache_size=0, sqlx의 .disable_statement_logging() 같은 옵션 세팅 안 하면 조용히 실패한다. 이건 어느 풀러를 쓰든 똑같이 봐야 하는 이슈다.
pgcat의 config는 TOML인데, 리로드가 이쁘게 안 된다. SIGHUP 보내면 대부분의 설정이 반영되지만 pool 관련 설정 몇 개는 재시작이 필요하다. 이거 문서에 명확히 안 써있어서 우리도 한 번 헛수고했다. 프로덕션 도입 전에 config 변경 → 리로드 시나리오를 반드시 테스트해봐야 한다.
Supavisor는 안 써봤지만, 벤치마크 글들을 종합해보면 클라이언트가 적을 때 P99 레이턴시가 확실히 높다는 게 반복적으로 지적된다. Elixir VM의 스케줄러 오버헤드 같아 보이는데, 정확한 원인은 코드를 못 봐서 잘 모르겠다.
마무리
결론은 뻔한 얘기다. 워크로드 안 보고 도구부터 정하지 말자. pgcat이 벤치마크에서 이긴다고 해서 모든 상황에서 이기는 건 아니다. 지금 PgBouncer로 잘 굴러가고 있으면 그냥 두는 게 답일 수도 있다. 우리 팀의 결정이 반드시 맞는다고 생각하지도 않는다. Supavisor를 뺀 것도 조직 사정 때문이지 기술적으로 안 좋아서가 아니다.
혹시 pgcat이나 Supavisor를 프로덕션에서 오래 돌려본 팀 있으면 어떤 이슈 만나셨는지 궁금하다. 특히 pgcat의 샤딩 기능을 실전에서 쓴 사례가 있으면 얘기 나눠보고 싶다.
'IT > DB 운영' 카테고리의 다른 글
| PgBouncer transaction pool에서 prepared statement 쓰기, 이젠 진짜 된다 (0) | 2026.08.18 |
|---|---|
| PostgreSQL 논리 복제 슬롯, failover에서 살리는 법 (17 이후 실전 가이드) (0) | 2026.08.09 |
| Redis vs Valkey, 지금 갈아탈 때인가 (1) | 2026.08.02 |
| PgBouncer transaction pool로 갈아탄 다음 날 새벽에 벌어진 일 (0) | 2026.07.31 |
| Redis Sentinel이 failover를 안 해준 새벽 이야기 (0) | 2026.07.20 |