PgBouncer 처리량을 4배로 확장한 방법
(clickhouse.com)- 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), 트랜잭션 풀링 모드
- 풀러 서버: 16-vCPU
- 비교 대상은 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 서버는 이 구성을 기본으로 제공함
댓글과 토론
Hacker News 의견들
-
https://github.com/yandex/odyssey를 쓰면 됨. Odyssey는 확장 가능한 PgBouncer임
- 가장 검증됐고 PostgreSQL에 자연스럽게 맞는 PgBouncer부터 제대로 조정해 사용했으며, 준비된 문 지원처럼 오랫동안 남아 있던 문제도 해결돼 아주 잘 작동하고 있음
PostgreSQL 연결을 1만 개 이상으로 확장한 고객도 많고, 앞으로 Odyssey나 pgdog 같은 대안도 검토할 예정임. 다만 개인적으로 PostgreSQL에 1만 개 넘는 연결을 두는 방식은 선호하지 않으며, 수백 개만으로도 충분히 확장할 수 있다고 봄 - 관련된 흥미로운 사실로, ClickHouse도 원래 Yandex가 개발했음
- Yandex는 러시아 회사이므로 사용하지 않는 편이 낫다고 봄
- 가장 검증됐고 PostgreSQL에 자연스럽게 맞는 PgBouncer부터 제대로 조정해 사용했으며, 준비된 문 지원처럼 오랫동안 남아 있던 문제도 해결돼 아주 잘 작동하고 있음
-
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...
26번 슬라이드부터 참고할 수 있음: https://www.pgevents.ca/events/pgconfdev2024/sessions/sessio...
PgBouncer 유지관리자인 Jelte의 발표 영상도 있음: https://www.youtube.com/watch?v=X-nCHcZ6vQU
- 원문은 AI가 쓴 티가 남. 취소 처리는 PostgreSQL 서버 프로세스가 아니라 PgBouncer 기능이며, 피어링도 PgBouncer 프로세스 사이에서 이뤄짐
-
pgdog을 사용 중인데 필요한 용도에 아주 잘 맞음
- Pgdog은 오래전 Instacart에서 부딪힌 PgBouncer의 단점을 해결하고, PostgreSQL을 수평 확장하는 더 탄탄한 기반, 즉 샤딩을 제공하려고 만들었음
- 앞으로 pgdog 제공도 검토할 예정임. PgBouncer를 선택한 이유는 https://news.ycombinator.com/item?id=48873867에 더 자세히 적혀 있음
- 최근 PostgreSQL 역방향 프록시 프로젝트들이 번갈아 소개되고 있음. Pgdog을 만든 이유는 여기서 볼 수 있음: https://news.ycombinator.com/item?id=48819308
-
PgBouncer가 왜 취소 요청까지 신경 써야 하는지 궁금함. PostgreSQL에 그대로 전달하고, 취소된 질의에 PostgreSQL이 정상 응답 대신 오류를 반환하면 해당 연결을 맡은 PgBouncer가 처리하면 되지 않는가?
- 질의 취소는 기존 질의 연결과 별도로 전달되므로 PgBouncer가 요청을 정확한 서버와 연결에 보내야 함
PgBouncer는 클라이언트에 가짜지만 추적 가능한 취소 PID와 비밀값을 주고, 취소 요청이 돌아오면 실제 서버·프로세스의 PID와 비밀값을 찾아 전달함. 또한 클라이언트는 질의가 실행 중이라고 믿지만 PgBouncer는 이미 끝나 연결이 재사용됐음을 아는 상황도 있으므로, 해당 연결과 PID가 여전히 그 질의를 실행 중인지 확인해야 함
- 질의 취소는 기존 질의 연결과 별도로 전달되므로 PgBouncer가 요청을 정확한 서버와 연결에 보내야 함
-
마이크로서비스에서 연결 풀을 통해 PostgreSQL 서버 접근을 통제하는 용도인가? 모놀리식 백엔드라면 필요하지 않아 보임
대부분의 괜찮은 백엔드 프레임워크에는 연결 풀이 내장돼 있으며, 마이크로서비스가 필요하지도 권장되지도 않는 사용 사례의 98%는 이것으로 해결됨- 호스트마다 백엔드 애플리케이션이 하나의 큰 바이너리로 실행된다면 맞음. 하지만 백엔드가 상태를 공유하지 않는 다수의 포크 프로세스로 구성된다면 분명 필요함
-
PgBouncer는 훌륭한 소프트웨어임. 많이 활용하고 있으며 데이터베이스 운영을 상당히 간단하게 만들어 줌
-
so_reuseport를 처음 알게 됐는데 흥미로움. 구성의 핵심은 이것과 피어링으로 보이는데, 피어링이 PgBouncer에 내장돼 있고 설정도 간단한가?- 그렇다. https://www.pgbouncer.org/config.html#section-peers를 참고하면 됨
-
Kubernetes에서도 피어링을 사용할 수 있는가? 이 환경에서는 포트를 재사용할 필요가 없을 텐데, 각 Pod가 별도의 연결 풀을 가지고 독립적으로 동작하게 되는지도 궁금함
-
지금 46세인데, 23세 때 PostgreSQL의 무거운 연결 모델을 보고 충격받았던 기억이 남. 그 뒤로도 개선되지 않은 것인가?
- 상당히 개선됐으며 이제 수천 개 연결도 제법 잘 처리함: https://techcommunity.microsoft.com/blog/adforpostgresql/imp...
다만 연결 풀을 쓰지 않으면 PostgreSQL이 프로세스를 포크해야 하므로 새 연결 생성에 수십 밀리초 이상의 비용이 항상 듦. 연결 풀 없이 작성된 애플리케이션도 이상적이지 않지만 흔히 존재함
애플리케이션 프레임워크도 변했으며, 서버리스 아키텍처는 PostgreSQL이 문제를 겪기 시작하는 수만 개의 연결을 만들 수 있음. 개인적으로 수백 개 넘는 연결은 선호하지 않지만 요즘에는 충분히 현실적인 상황임 - 당시에는 여러 회사가 Java가 제시한 스레딩 모델에 처음으로 쓸 만한 해답을 내놓던 시기였음. 이는 Windows의 모델에 기능을 더한 형태였고 Solaris조차 Java 처리에 버거워했으며, HP-UX는 더 심했고 SGI가 HP보다 나았는지는 기억나지 않음
Java 호환성을 확보하는 과정에서 많은 회사가 동시성 처리 방식을 크게 바꿔야 했음. PostgreSQL과 SQLite도 비슷한 시기에 설계됐지만, 이런 스레딩 논의가 생기기 전부터 고부하 시스템을 배포해 온 업계 베테랑들이 오래된 하드웨어 사용자까지 지원하며 만든 시스템이었음
- 상당히 개선됐으며 이제 수천 개 연결도 제법 잘 처리함: https://techcommunity.microsoft.com/blog/adforpostgresql/imp...