서버에 CPU 코어가 많다고 데이터베이스 앞단도 자동으로 빨라지는 건 아니야. ClickHouse Managed Postgres는 단일 스레드로 동작하는 PgBouncer가 코어 하나에 묶이는 문제를 피하려고, 여러 PgBouncer 프로세스를 하나의 플릿으로 운영하는 구성을 소개했어.1

핵심은 프로세스 수를 늘리는 데서 끝나지 않아. 모든 프로세스가 so_reuseport로 같은 포트에 붙고, 커널이 새 연결을 나눠 보낸다. 클라이언트는 여러 풀러의 주소를 알 필요 없이 기존처럼 하나의 엔드포인트에 연결하면 돼.1

한 코어에 묶인 풀러

PgBouncer는 단일 스레드라서 프로세스 하나가 CPU 코어 하나만 사용해. 16-vCPU 서버에 PgBouncer 하나만 두면 연결 풀링 작업은 한 코어에 집중되고, 나머지 코어는 이 작업에 충분히 쓰이지 못하지. Postgres가 아직 처리할 여력이 있어도 풀러가 먼저 전체 처리량을 제한할 수 있어.1

ClickHouse는 같은 AWS EC2 환경에서 단일 프로세스와 16개 프로세스 플릿을 비교했어. 풀러 서버는 16-vCPU c7i.4xlarge, Postgres는 별도 서버에 두고, 세 번째 서버에서 pgbench로 읽기 전용 작업을 발생시켰지. 트랜잭션 풀링 모드에서 클라이언트 연결을 8개부터 256개까지 늘리며 처리량과 CPU 사용량을 측정했어.1

클라이언트 연결단일 프로세스 TPS16개 프로세스 플릿 TPS
88,9106,450
3254,20364,244
6486,570219,439
12883,463320,547
25676,893336,469

연결이 적을 때는 단일 프로세스가 오히려 빨랐어. 8개 연결에서는 단일 프로세스가 8,910 TPS, 플릿이 6,450 TPS를 기록했지. 여러 프로세스에 연결을 나누는 이득이 아직 작기 때문이야.1

하지만 동시성이 커지자 차이가 벌어졌어. 단일 프로세스는 약 87,000 TPS에서 정점을 찍은 뒤 256개 연결에서 약 77,000 TPS로 내려갔고, 16개 프로세스 플릿은 약 336,000 TPS까지 올라갔어. 이 측정에서는 플릿이 단일 프로세스보다 약 4배 높은 처리량을 보였지.1

CPU 사용량도 병목의 위치를 보여 줘. pidstat 측정에서 단일 PgBouncer 프로세스는 코어 하나를 약 97% 사용했지만 16-vCPU 서버 전체 사용률은 10% 미만이었어. 플릿은 작업을 여러 코어로 분산해 약 8개 코어를 사용했고, 256개 연결을 유지한 측정에서는 단일 프로세스 서버가 약 9%, 플릿 서버가 약 52%의 CPU를 지속적으로 사용했어.1

포트만 공유하면 충분할까?

정상적인 새 연결은 so_reuseport만으로 나눌 수 있어. 문제는 쿼리 취소야.

Postgres의 취소 요청은 실행 중인 쿼리 연결과 별개의 새 연결로 들어와. 포트를 공유하면 커널이 이 요청을 원래 세션을 가진 프로세스가 아닌 다른 프로세스에 보낼 수 있어. 요청을 받은 프로세스가 해당 쿼리를 모르면 취소가 실행되지 않지.1

그래서 이 구성은 프로세스 피어링을 함께 사용해. 취소 요청을 받은 프로세스가 실제 세션 소유 프로세스로 요청을 전달하는 방식이야. 어떤 프로세스가 요청을 받더라도 플릿 전체에서 쿼리 취소가 작동하게 만드는 장치지.1

연결 예산도 프로세스 수에 맞춰 나눠야 해. 트랜잭션 풀링에서는 트랜잭션이 커밋되면 서버 연결이 풀로 돌아가지만, 클라이언트와 데이터베이스 연결 상한은 여전히 관리해야 하거든. ClickHouse는 max_client_connmax_db_connections를 프로세스 수로 나눠 각 프로세스에 배정한다고 설명해. 이렇게 해야 플릿 전체의 연결 상한을 높이면서도 Postgres 쪽 연결 수를 안전한 한도 안에 둘 수 있어.1

무엇을 봐야 할까?

이 사례의 구분선은 프로세스 개수가 아니야. 연결이 적고 Postgres가 먼저 한계에 도달한다면 단일 프로세스로도 충분할 수 있어. 반대로 높은 동시성에서 풀러의 한 코어가 먼저 포화된다면, 코어 수에 맞춘 프로세스 플릿과 포트 공유만으로는 부족하고 취소 요청 전달과 프로세스별 연결 예산까지 함께 설계해야 해.1

ClickHouse Managed Postgres는 이 구성을 모든 서버의 기본값으로 제공한다고 밝혔어. 다만 한 환경의 벤치마크 결과를 다른 환경의 기본값으로 곧바로 옮길 수는 없어. 연결 수가 적은 구간에서는 단일 프로세스가 더 빨랐고, 측정도 읽기 전용 트랜잭션 풀링 작업에 한정됐으니까. 먼저 풀러가 실제로 병목인지, 그다음에 병렬화 비용이 이득보다 작은지 확인해야 해.1

관련 개념은 연결 풀러 프로세스 플릿에서 이어서 볼 수 있어.

각주

  1. GeekNews, 「PgBouncer 처리량을 4배로 확장한 방법」 자료. ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8 ↩︎9 ↩︎10 ↩︎11 ↩︎12