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

2 hours ago 2
  • 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 서버는 이 구성을 기본으로 제공함
Read Entire Article