클릭하우스(ClickHouse)가 자사의 관리형 포스트그레스(Postgres) 서비스에서 데이터베이스 연결 풀러(connection pooler)인 PgBouncer의 처리량(throughput)을 최대 4배까지 향상시키는 방법을 공개했습니다. PgBouncer는 단일 스레드(single-threaded) 애플리케이션으로, 아무리 많은 CPU 코어를 가진 서버에서도 단 하나의 코어만 사용하여 연결 풀링(connection pooling)을 처리하는 한계가 있었습니다. 이로 인해 Postgres 서버의 성능이 충분함에도 불구하고 PgBouncer가 병목 현상을 일으켜 전체 시스템의 처리량을 제한하는 문제가 발생했습니다.
클릭하우스는 이 문제를 해결하기 위해 여러 개의 PgBouncer 프로세스를 실행하는 방식을 채택했습니다. 서버의 가용 코어(vCPU) 수에 비례하여 PgBouncer 프로세스들을 띄우고, 모든 프로세스가 `so_reuseport` 옵션을 사용하여 동일한 포트(port)에 바인딩(bind)하도록 설정했습니다. 이렇게 하면 리눅스 커널(kernel)이 들어오는 클라이언트 연결을 여러 PgBouncer 프로세스에 자동으로 분산시켜 로드 밸런싱(load-balancing)을 수행합니다. 클라이언트 입장에서는 여전히 단일 엔드포인트(endpoint)에 연결하는 것처럼 보이며, 뒤에서 여러 PgBouncer가 동작하는 것을 알 수 없습니다. 하지만 `so_reuseport` 방식의 한 가지 문제점은 쿼리 취소(query cancellation) 요청이 잘못된 PgBouncer 프로세스로 전달될 수 있다는 점입니다. 이를 해결하기 위해 클릭하우스는 PgBouncer 프로세스들 간에 피어링(peering) 기능을 구현하여, 취소 요청이 잘못된 프로세스에 도착하더라도 실제 세션(session)을 담당하는 프로세스로 전달되도록 했습니다. 또한, `max_client_conn` 및 `max_db_connections`와 같은 연결 예산(connection budget)도 전체 프로세스 수에 따라 분할하여, 전체 PgBouncer 플릿(fleet)이 Postgres에 과부하를 주지 않으면서도 더 많은 동시 연결을 처리할 수 있도록 했습니다.
실제 하드웨어 테스트 결과, 16개의 가상 CPU(vCPU)를 가진 AWS EC2 인스턴스에서 단일 PgBouncer 프로세스는 최대 약 8만 7천 TPS(초당 트랜잭션 수)를 기록한 반면, 16개의 PgBouncer 프로세스로 구성된 플릿은 약 33만 6천 TPS까지 성능이 향상되어 약 4배의 처리량 증가를 보였습니다. 단일 프로세스 구성에서는 PgBouncer가 하나의 코어에 97% 이상 고정되어 병목 현상을 일으켰지만, 플릿 구성에서는 여러 코어에 작업이 분산되어 전체 CPU 활용률이 52%에 달하면서도 여전히 Postgres와 로드 생성기가 한계에 도달하기 전까지 여유가 있었습니다. 이처럼 클릭하우스의 접근 방식은 PgBouncer가 더 이상 시스템의 병목이 아닌 단순한 연결 파이프라인(plumbing) 역할을 하도록 만들어, Postgres 기반 애플리케이션의 확장성을 크게 개선했습니다. 모든 클릭하우스 관리형 포스트그레스 서버에는 이 설정이 기본으로 적용되어 제공됩니다.
