# PgBouncer 처리량을 4배로 확장한 방법

> Clean Markdown view of GeekNews topic #31356. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=31356](https://news.hada.io/topic?id=31356)
- GeekNews Markdown: [https://news.hada.io/topic/31356.md](https://news.hada.io/topic/31356.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-07-13T02:33:57+09:00
- Updated: 2026-07-13T02:33:57+09:00
- Original source: [clickhouse.com](https://clickhouse.com/blog/pgbouncer-clickhouse-managed-postgres)
- Points: 1
- Comments: 1

## Topic Body

- ClickHouse Managed Postgres는 단일 스레드인 PgBouncer가 CPU 코어 하나에 묶이는 한계를 피하기 위해, 코어 수에 비례한 **다중 프로세스 플릿**을 기본 구성으로 운영함
- 모든 프로세스가 `so_reuseport`로 같은 포트에 바인딩되고 커널이 새 연결을 분산하므로, 클라이언트에는 **단일 엔드포인트**로 보임
- 별도 연결로 들어오는 Postgres 취소 요청이 세션을 보유하지 않은 프로세스에 도착할 수 있어, **프로세스 피어링**으로 실제 세션 소유자에게 전달함
- 16-vCPU `c7i.4xlarge`에서 단일 프로세스는 약 87,000 TPS가 정점이었지만, 16개 프로세스 플릿은 약 **336,000 TPS**까지 올라 약 4배의 처리량을 기록함
- 연결이 적으면 단일 프로세스가 비슷하거나 조금 빠를 수 있지만, 높은 동시성에서는 한 코어가 병목이 되므로 연결 예산을 플릿 전체에 나눠 **Postgres 과다 연결 없이** 처리량과 연결 상한을 확장해야 함

---

### 단일 프로세스가 만드는 CPU 병목
- PgBouncer는 **단일 스레드**이므로 시스템의 CPU 수와 관계없이 프로세스 하나가 코어 하나만 사용함
- 16-vCPU 서버에서도 연결 풀링 작업은 코어 하나에 집중되고 나머지 15개 코어는 유휴 상태가 됨
  - 이 때문에 Postgres의 처리 여력이 소진되기 전에 풀러가 전체 처리량을 제한함
- ClickHouse Managed Postgres는 사용 가능한 코어 수에 비례해 PgBouncer 프로세스 수를 정하는 **프로세스 플릿**을 운영함

### `so_reuseport`를 이용한 연결 분산
- 플릿의 모든 프로세스는 `so_reuseport`를 활성화하고 **같은 포트**에 바인딩함
- 커널이 들어오는 연결을 각 프로세스에 분산하므로, 클라이언트는 하나의 엔드포인트에 연결하고 뒤에서 여러 PgBouncer가 동작한다는 사실을 알 필요가 없음
- PgBouncer 문서도 단일 스레드 프로세스 여러 개로 여러 코어를 활용하는 방법으로 `so_reuseport`를 안내함

### 쿼리 취소를 보장하는 프로세스 피어링
- Postgres의 **취소 요청**은 실행 중인 쿼리 연결과 별개인 새 연결을 통해 취소 키와 함께 들어옴
- `so_reuseport`를 사용하면 커널이 새 취소 연결을 해당 세션을 보유한 프로세스가 아닌 다른 프로세스에 전달할 수 있음
  - 요청을 받은 프로세스가 해당 쿼리를 알지 못하면 취소가 실행되지 않음
- PgBouncer 프로세스끼리 **피어링**하면 잘못된 프로세스에 도착한 요청을 실제 세션 소유 프로세스로 전달할 수 있음
- 이에 따라 어떤 프로세스가 요청을 받더라도 플릿 전체에서 쿼리 취소가 정상 작동함

### 트랜잭션 풀링과 연결 예산
- 풀링은 **트랜잭션 모드**로 동작하며, 트랜잭션이 커밋되는 즉시 서버 연결이 풀로 반환됨
- `max_client_conn`과 `max_db_connections`는 프로세스 수로 나눠 각 프로세스에 할당함
- 연결 예산을 분할하면 플릿 전체 연결 수를 Postgres의 안전한 한도 안에 유지하면서 집계 연결 상한을 높일 수 있음
- 단일 프로세스는 자체 `max_client_conn`을 넘는 신규 클라이언트를 다음 오류와 함께 거부함
  - `FATAL: no more connections allowed (max_client_conn)`

### 동일 하드웨어 벤치마크
- 두 구성은 동일한 AWS EC2 환경에서 측정함
  - 풀러 서버: 16-vCPU `c7i.4xlarge`
  - Postgres: 별도 서버
  - 부하 생성기: 세 번째 서버에서 `pgbench` 사용
  - 작업 부하: 읽기 전용(select-only), 트랜잭션 풀링 모드
- 비교 대상은 PgBouncer **단일 프로세스**와 16개 프로세스 플릿이며, 인스턴스 유형·Postgres·작업 부하는 동일함
- 클라이언트 연결을 8개에서 256개까지 늘리며 처리량과 16코어 서버의 CPU 사용량을 측정함

| 클라이언트 | 단일 프로세스 TPS | 단일 프로세스 서버 CPU | 플릿 TPS | 플릿 서버 CPU |
|---:|---:|---:|---:|---:|
| 8 | 8,910 | 0.8% | 6,450 | 2.9% |
| 32 | 54,203 | 5.2% | 64,244 | 12.3% |
| 64 | 86,570 | 8.3% | 219,439 | 31.9% |
| 128 | 83,463 | 8.1% | 320,547 | 45.9% |
| 256 | 76,893 | 7.7% | 336,469 | 48.9% |

### 처리량과 CPU 활용 결과
- 단일 프로세스는 약 **87,000 TPS**에서 정점을 찍은 뒤 부하가 커질수록 성능이 낮아져, 클라이언트 256개에서 약 77,000 TPS를 기록함
- 16개 프로세스 플릿은 더 많은 코어를 활용하면서 약 **336,000 TPS**까지 증가해 단일 프로세스 대비 약 4배의 처리량을 보임
- `pidstat` 측정에서 단일 PgBouncer 프로세스는 부하 중 CPU 약 97%로 코어 하나를 가득 사용했지만, 16-vCPU 서버 전체 사용률은 10% 미만에 머묾
- 플릿은 서버 전체에 작업을 분산해 약 8개 코어를 사용했으며, Postgres와 부하 생성기가 한계에 도달했을 때도 추가 여력이 남아 있었음
- 클라이언트 256개를 유지한 측정에서는 단일 프로세스 서버가 약 9%, 플릿 서버가 약 **52% CPU**를 지속적으로 사용함
- EC2 CloudWatch의 외부 측정에서는 단일 프로세스 인스턴스의 평균 CPUUtilization이 약 16%, 플릿은 약 60%였음
  - CloudWatch 수치는 게스트 내부 측정보다 조금 높지만, 단일 PgBouncer가 16-vCPU 대부분을 활용하지 못한다는 차이는 동일함

### 동시성에 따른 구성 선택
- 연결 수가 적을 때는 병렬화할 작업이 없고 플릿의 연결이 여러 프로세스에 분산되므로, **단일 프로세스가 충분하거나 조금 더 빠를 수 있음**
  - 클라이언트 8개에서는 단일 프로세스가 8,910 TPS, 플릿이 6,450 TPS를 기록함
- 동시성이 높아지면 단일 프로세스가 점유한 코어 하나가 처리량의 벽이 되고, 플릿과의 성능 격차가 커짐
- PgBouncer보다 Postgres가 먼저 한계에 도달하는 환경에서는 단일 프로세스도 적절한 기본값임
- 풀러가 처리량을 제한하기 시작하면 코어 수에 맞춘 프로세스 플릿, `so_reuseport` 기반 포트 공유, 프로세스 피어링을 함께 사용해야 병목을 해소할 수 있음
- 모든 **ClickHouse Managed Postgres** 서버는 이 구성을 기본으로 제공함

## Comments



### Comment 61669

- Author: neo
- Created: 2026-07-13T02:33:58+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=48872874) 
- [https://github.com/yandex/odyssey](<https://github.com/yandex/odyssey>)를 쓰면 됨. **Odyssey**는 확장 가능한 PgBouncer임
  - 가장 검증됐고 PostgreSQL에 자연스럽게 맞는 **PgBouncer**부터 제대로 조정해 사용했으며, 준비된 문 지원처럼 오랫동안 남아 있던 문제도 해결돼 아주 잘 작동하고 있음  
    PostgreSQL 연결을 **1만 개 이상**으로 확장한 고객도 많고, 앞으로 Odyssey나 pgdog 같은 대안도 검토할 예정임. 다만 개인적으로 PostgreSQL에 1만 개 넘는 연결을 두는 방식은 선호하지 않으며, 수백 개만으로도 충분히 확장할 수 있다고 봄
  - 관련된 흥미로운 사실로, **ClickHouse**도 원래 Yandex가 개발했음
  - Yandex는 러시아 회사이므로 사용하지 않는 편이 낫다고 봄

- Kubernetes에서 **PgBouncer**를 운영하면 한 머신에 여러 프로세스를 띄우기도 쉽고, 여러 머신에 분산하기도 간단함  
  Azure가 VM 유지보수 과정에서 전체 서버군에 순차 장애를 일으키곤 해서 다중 머신 구성이 특히 유용함
  - PgBouncer에서 PostgreSQL까지 **네트워크 홉**이 하나 더 생길 때 처리량에 어떤 영향을 주는지 궁금함. 차이는 작을 것으로 예상하지만 확인해 볼 만함

- 여기서 말하는 **피어링**의 개념은 이해하지만 PostgreSQL에서 써본 적은 없음  
  A) PostgreSQL에 이를 쉽게 구성하는 모드나 설정이 있는가? 오류가 나지 않을 때까지 취소 요청을 피어들에게 순차 전달하거나, 잘못 받은 프로세스가 올바른 프로세스를 식별할 수 있는 메타데이터가 취소 요청에 담기는 구조를 상상하고 있음  
  B) 모든 PostgreSQL 프로세스가 `so_reuseport`로 클라이언트 요청을 받는다면 피어 간 통신에는 별도의 **프로세스 간 통신(IPC)** 수단이 필요해 보이는데, 실제로 어떤 메커니즘을 쓰는가?
  - 원문은 AI가 쓴 티가 남. 취소 처리는 PostgreSQL 서버 프로세스가 아니라 **PgBouncer 기능**이며, 피어링도 PgBouncer 프로세스 사이에서 이뤄짐  
    PgBouncer 프로세스 ID를 취소 키에 포함하면 비교적 간단히 구현할 수 있을 듯함
  - 토큰에 정보를 인코딩함. 이것이 PostgreSQL에서 토큰의 **32바이트 제한을 제거한 동기** 중 하나였음: [https://www.postgresql.org/message-id/508d0505-8b7a-4864-a68...](<https://www.postgresql.org/message-id/508d0505-8b7a-4864-a681-e7e5edfe32aa@iki.fi>)  
    26번 슬라이드부터 참고할 수 있음: [https://www.pgevents.ca/events/pgconfdev2024/sessions/sessio...](<https://www.pgevents.ca/events/pgconfdev2024/sessions/session/107/slides/48/2024-05-29-the-postgres-protocol%20(split).pdf>)  
    PgBouncer 유지관리자인 Jelte의 발표 영상도 있음: [https://www.youtube.com/watch?v=X-nCHcZ6vQU](<https://www.youtube.com/watch?v=X-nCHcZ6vQU>)

- [pgdog](<https://github.com/pgdogdev/pgdog>)을 사용 중인데 필요한 용도에 아주 잘 맞음
  - Pgdog은 오래전 Instacart에서 부딪힌 **PgBouncer의 단점**을 해결하고, PostgreSQL을 수평 확장하는 더 탄탄한 기반, 즉 샤딩을 제공하려고 만들었음
  - 앞으로 pgdog 제공도 검토할 예정임. PgBouncer를 선택한 이유는 [https://news.ycombinator.com/item?id=48873867](<https://news.ycombinator.com/item?id=48873867>)에 더 자세히 적혀 있음
  - 최근 PostgreSQL **역방향 프록시** 프로젝트들이 번갈아 소개되고 있음. Pgdog을 만든 이유는 여기서 볼 수 있음: [https://news.ycombinator.com/item?id=48819308](<https://news.ycombinator.com/item?id=48819308>)

- PgBouncer가 왜 **취소 요청**까지 신경 써야 하는지 궁금함. PostgreSQL에 그대로 전달하고, 취소된 질의에 PostgreSQL이 정상 응답 대신 오류를 반환하면 해당 연결을 맡은 PgBouncer가 처리하면 되지 않는가?
  - 질의 취소는 기존 질의 연결과 별도로 전달되므로 PgBouncer가 요청을 **정확한 서버와 연결**에 보내야 함  
    PgBouncer는 클라이언트에 가짜지만 추적 가능한 취소 PID와 비밀값을 주고, 취소 요청이 돌아오면 실제 서버·프로세스의 PID와 비밀값을 찾아 전달함. 또한 클라이언트는 질의가 실행 중이라고 믿지만 PgBouncer는 이미 끝나 연결이 재사용됐음을 아는 상황도 있으므로, 해당 연결과 PID가 여전히 그 질의를 실행 중인지 확인해야 함

- 마이크로서비스에서 연결 풀을 통해 PostgreSQL 서버 접근을 통제하는 용도인가? **모놀리식 백엔드**라면 필요하지 않아 보임  
  대부분의 괜찮은 백엔드 프레임워크에는 연결 풀이 내장돼 있으며, 마이크로서비스가 필요하지도 권장되지도 않는 사용 사례의 98%는 이것으로 해결됨
  - 호스트마다 백엔드 애플리케이션이 하나의 큰 바이너리로 실행된다면 맞음. 하지만 백엔드가 상태를 공유하지 않는 다수의 **포크 프로세스**로 구성된다면 분명 필요함

- PgBouncer는 훌륭한 소프트웨어임. 많이 활용하고 있으며 **데이터베이스 운영**을 상당히 간단하게 만들어 줌

- `so_reuseport`를 처음 알게 됐는데 흥미로움. 구성의 핵심은 이것과 **피어링**으로 보이는데, 피어링이 PgBouncer에 내장돼 있고 설정도 간단한가?
  - 그렇다. [https://www.pgbouncer.org/config.html#section-peers](<https://www.pgbouncer.org/config.html#section-peers>)를 참고하면 됨

- Kubernetes에서도 **피어링**을 사용할 수 있는가? 이 환경에서는 포트를 재사용할 필요가 없을 텐데, 각 Pod가 별도의 연결 풀을 가지고 독립적으로 동작하게 되는지도 궁금함

- 지금 46세인데, 23세 때 PostgreSQL의 **무거운 연결 모델**을 보고 충격받았던 기억이 남. 그 뒤로도 개선되지 않은 것인가?
  - 상당히 개선됐으며 이제 **수천 개 연결**도 제법 잘 처리함: [https://techcommunity.microsoft.com/blog/adforpostgresql/imp...](<https://techcommunity.microsoft.com/blog/adforpostgresql/improving-postgres-connection-scalability-snapshots/1806462>)  
    다만 연결 풀을 쓰지 않으면 PostgreSQL이 프로세스를 포크해야 하므로 새 연결 생성에 수십 밀리초 이상의 비용이 항상 듦. 연결 풀 없이 작성된 애플리케이션도 이상적이지 않지만 흔히 존재함  
    애플리케이션 프레임워크도 변했으며, 서버리스 아키텍처는 PostgreSQL이 문제를 겪기 시작하는 수만 개의 연결을 만들 수 있음. 개인적으로 수백 개 넘는 연결은 선호하지 않지만 요즘에는 충분히 현실적인 상황임
  - 당시에는 여러 회사가 Java가 제시한 **스레딩 모델**에 처음으로 쓸 만한 해답을 내놓던 시기였음. 이는 Windows의 모델에 기능을 더한 형태였고 Solaris조차 Java 처리에 버거워했으며, HP-UX는 더 심했고 SGI가 HP보다 나았는지는 기억나지 않음  
    Java 호환성을 확보하는 과정에서 많은 회사가 동시성 처리 방식을 크게 바꿔야 했음. PostgreSQL과 SQLite도 비슷한 시기에 설계됐지만, 이런 스레딩 논의가 생기기 전부터 고부하 시스템을 배포해 온 업계 베테랑들이 오래된 하드웨어 사용자까지 지원하며 만든 시스템이었음
