포스트

OpenAI의 PostgreSQL 스케일링 - 8억 ChatGPT 사용자를 지원하는 방법

목차

  1. 개요
  2. 초기 설계의 한계
  3. Primary 부하 감소
  4. 쿼리 최적화
  5. 단일 장애점 완화
  6. 워크로드 격리
  7. 커넥션 풀링
  8. 캐싱 전략
  9. Read Replica 확장
  10. Rate Limiting
  11. 스키마 관리
  12. 성과와 향후 계획

개요

PostgreSQL은 수년간 ChatGPT와 OpenAI API 같은 핵심 제품을 지탱하는 가장 중요한 데이터 시스템 중 하나였다. 사용자 기반이 급격히 성장하면서 데이터베이스에 대한 요구도 기하급수적으로 증가했다. 지난 1년간 PostgreSQL 부하는 10배 이상 증가했고 계속 빠르게 상승하고 있다.

OpenAI는 이 성장을 지속하기 위한 프로덕션 인프라 발전 과정에서 새로운 인사이트를 발견했다. PostgreSQL은 많은 사람들이 이전에 생각했던 것보다 훨씬 더 큰 읽기 중심 워크로드를 안정적으로 지원하도록 확장할 수 있다.

현재 아키텍처

OpenAI는 단일 Primary Azure PostgreSQL flexible server 인스턴스와 전 세계 여러 리전에 분산된 약 50개의 Read Replica로 대규모 글로벌 트래픽을 지원한다. 8억 사용자를 위해 초당 수백만 개의 쿼리를 처리한다.

초기 설계의 한계

ChatGPT 출시 후 트래픽은 전례 없는 속도로 성장했다. 이를 지원하기 위해 애플리케이션과 PostgreSQL 데이터베이스 레이어 모두에서 광범위한 최적화를 빠르게 구현했다. 인스턴스 크기를 늘려 스케일 업하고, Read Replica를 추가하여 스케일 아웃했다.

PostgreSQL 과부하로 인한 SEV 패턴

단일 Primary 아키텍처가 OpenAI 규모의 요구를 충족할 수 있다는 것은 놀랍게 들릴 수 있다. 실제로 PostgreSQL 과부하로 인한 여러 SEV를 겪었으며, 대부분 동일한 패턴을 따른다.

  1. 업스트림 이슈로 인한 갑작스러운 데이터베이스 부하 급증
  2. 캐싱 레이어 장애로 인한 광범위한 캐시 미스
  3. 비용이 많이 드는 다중 조인이 CPU를 포화시킴
  4. 새 기능 출시로 인한 쓰기 폭풍

리소스 사용률이 올라가면 쿼리 지연시간이 증가하고 요청이 타임아웃되기 시작한다. 재시도가 부하를 더 증폭시켜 전체 ChatGPT와 API 서비스를 저하시킬 수 있는 악순환을 일으킨다.

MVCC의 한계

PostgreSQL은 읽기 중심 워크로드에서는 잘 확장되지만 높은 쓰기 트래픽 기간에는 여전히 도전에 직면한다. 이는 주로 PostgreSQL의 MVCC(Multiversion Concurrency Control) 구현 때문이다.

MVCC가 쓰기 중심 워크로드에 비효율적인 이유는 다음과 같다.

  • 쿼리가 튜플이나 단일 필드만 업데이트해도 전체 행이 복사되어 새 버전이 생성됨
  • 높은 쓰기 부하에서 상당한 쓰기 증폭 발생
  • 쿼리가 최신 버전을 검색하기 위해 여러 튜플 버전(dead tuples)을 스캔해야 하므로 읽기 증폭도 증가
  • 테이블 및 인덱스 블로트, 인덱스 유지보수 오버헤드 증가, 복잡한 autovacuum 튜닝 필요

이러한 문제에 대한 자세한 내용은 Carnegie Mellon University의 Andy Pavlo 교수와 함께 작성한 “The Part of PostgreSQL We Hate the Most” 블로그에서 확인할 수 있다.

Primary 부하 감소

과제: 단일 Writer로는 쓰기를 확장할 수 없다. 심한 쓰기 급증은 빠르게 Primary를 과부하시키고 ChatGPT와 API 같은 서비스에 영향을 줄 수 있다.

해결책: Primary의 부하를 읽기와 쓰기 모두 최대한 최소화하여 쓰기 급증을 처리할 충분한 용량을 확보한다.

읽기 트래픽 오프로드

가능한 모든 곳에서 읽기 트래픽을 Replica로 오프로드한다. 단, 쓰기 트랜잭션의 일부인 일부 읽기 쿼리는 Primary에 남아야 한다. 그런 쿼리는 효율적이고 느린 쿼리를 피하도록 집중한다.

쓰기 트래픽 마이그레이션

샤딩 가능하고 쓰기가 많은 워크로드는 Azure CosmosDB 같은 샤딩 시스템으로 마이그레이션했다. 샤딩하기 어렵지만 여전히 높은 쓰기 볼륨을 생성하는 워크로드는 마이그레이션에 더 오래 걸리며 진행 중이다.

애플리케이션 최적화

쓰기 부하를 줄이기 위해 애플리케이션을 공격적으로 최적화했다.

  • 중복 쓰기를 유발하는 애플리케이션 버그 수정
  • 적절한 경우 Lazy Write를 도입하여 트래픽 급증 완화
  • 테이블 필드 백필 시 과도한 쓰기 압력을 방지하기 위해 엄격한 Rate Limit 적용

쿼리 최적화

과제: PostgreSQL에서 여러 비용이 많이 드는 쿼리를 식별했다. 과거에는 이러한 쿼리의 갑작스러운 볼륨 급증이 대량의 CPU를 소비하여 ChatGPT와 API 요청을 느리게 만들었다.

해결책: PostgreSQL 쿼리를 지속적으로 최적화하여 효율적이고 일반적인 OLTP 안티 패턴을 피하도록 해야 한다.

12테이블 조인 문제

한때 12개 테이블을 조인하는 극도로 비용이 많이 드는 쿼리를 식별했다. 이 쿼리의 급증이 과거 높은 심각도 SEV의 원인이었다.

가능한 한 복잡한 다중 테이블 조인을 피해야 한다. 조인이 필요한 경우 쿼리를 분해하고 복잡한 조인 로직을 애플리케이션 레이어로 이동하는 것을 고려해야 한다.

ORM 생성 쿼리 검토

이러한 문제가 있는 쿼리 중 많은 것이 ORM(Object-Relational Mapping) 프레임워크에 의해 생성된다. ORM이 생성하는 SQL을 주의 깊게 검토하고 예상대로 동작하는지 확인하는 것이 중요하다.

장시간 실행 유휴 쿼리 방지

PostgreSQL에서 장시간 실행되는 유휴 쿼리를 찾는 것은 흔한 일이다. idle_in_transaction_session_timeout 같은 타임아웃을 구성하여 autovacuum을 차단하지 않도록 하는 것이 필수적이다.

1
2
-- 유휴 트랜잭션 타임아웃 설정
SET idle_in_transaction_session_timeout = '60s';

단일 장애점 완화

과제: Read Replica가 다운되면 트래픽을 다른 Replica로 라우팅할 수 있다. 그러나 단일 Writer에 의존하면 단일 장애점이 생긴다. 다운되면 전체 서비스가 영향을 받는다.

해결책: 대부분의 중요한 요청은 읽기 쿼리만 포함한다. Primary의 단일 장애점을 완화하기 위해 해당 읽기를 Writer에서 Replica로 오프로드했다. 이렇게 하면 Primary가 다운되어도 해당 요청은 계속 서비스할 수 있다. 쓰기 작업은 여전히 실패하지만 영향이 줄어든다. 읽기가 여전히 가능하므로 더 이상 SEV0가 아니다.

고가용성 모드

Primary 장애를 완화하기 위해 Hot Standby와 함께 HA(High-Availability) 모드로 Primary를 실행한다. Hot Standby는 지속적으로 동기화되는 Replica로 항상 트래픽 서빙을 인계받을 준비가 되어 있다. Primary가 다운되거나 유지보수를 위해 오프라인으로 전환해야 하면 다운타임을 최소화하기 위해 Standby를 빠르게 승격할 수 있다.

Azure PostgreSQL 팀은 매우 높은 부하에서도 이러한 페일오버가 안전하고 신뢰할 수 있도록 상당한 작업을 수행했다.

Read Replica 장애 처리

Read Replica 장애를 처리하기 위해 각 리전에 충분한 용량 여유를 가진 여러 Replica를 배포한다. 단일 Replica 장애가 리전 장애로 이어지지 않도록 한다.

워크로드 격리

과제: 특정 요청이 PostgreSQL 인스턴스에서 불균형한 양의 리소스를 소비하는 상황을 자주 겪는다. 이는 동일한 인스턴스에서 실행되는 다른 워크로드의 성능 저하로 이어질 수 있다. 예를 들어 새 기능 출시가 PostgreSQL CPU를 많이 소비하는 비효율적인 쿼리를 도입하여 다른 중요 기능의 요청을 느리게 만들 수 있다.

해결책: “Noisy Neighbor” 문제를 완화하기 위해 워크로드를 전용 인스턴스로 격리한다. 리소스 집약적인 요청의 갑작스러운 급증이 다른 트래픽에 영향을 주지 않도록 한다.

우선순위 기반 분리

요청을 저우선순위와 고우선순위 티어로 분리하고 별도의 인스턴스로 라우팅한다. 이렇게 하면 저우선순위 워크로드가 리소스 집약적이 되더라도 고우선순위 요청의 성능을 저하시키지 않는다.

다른 제품과 서비스에도 동일한 전략을 적용한다. 한 제품의 활동이 다른 제품의 성능이나 신뢰성에 영향을 주지 않도록 한다.

커넥션 풀링

과제: 각 인스턴스에는 최대 커넥션 제한이 있다 (Azure PostgreSQL에서 5,000). 커넥션이 소진되거나 너무 많은 유휴 커넥션이 누적되기 쉽다. 모든 가용 커넥션을 소진한 커넥션 폭풍으로 인한 인시던트를 경험한 적이 있다.

해결책: 데이터베이스 커넥션을 풀링하기 위해 PgBouncer를 프록시 레이어로 배포했다.

PgBouncer 효과

statement 또는 transaction 풀링 모드로 실행하면 커넥션을 효율적으로 재사용할 수 있다. 활성 클라이언트 커넥션 수를 크게 줄인다. 커넥션 설정 지연시간도 단축된다.

지표이전이후
평균 커넥션 시간50ms5ms

배포 구성

리전 간 커넥션과 요청은 비용이 많이 들 수 있다. 프록시, 클라이언트, Replica를 동일 리전에 함께 배치하여 네트워크 오버헤드와 커넥션 사용 시간을 최소화한다.

각 Read Replica는 여러 PgBouncer 파드를 실행하는 자체 Kubernetes 배포를 가진다. 동일한 Kubernetes Service 뒤에서 여러 Kubernetes 배포를 실행하여 파드 간에 트래픽을 로드밸런싱한다.

구성 주의사항

PgBouncer는 신중하게 구성해야 한다. 유휴 타임아웃 같은 설정은 커넥션 소진을 방지하는 데 중요하다.

캐싱 전략

과제: 캐시 미스의 갑작스러운 급증은 PostgreSQL 데이터베이스에 읽기 급증을 일으켜 CPU를 포화시키고 사용자 요청을 느리게 만들 수 있다.

해결책: PostgreSQL에 대한 읽기 압력을 줄이기 위해 대부분의 읽기 트래픽을 서비스하는 캐싱 레이어를 사용한다.

캐시 미스 폭풍 문제

캐시 히트율이 예기치 않게 떨어지면 캐시 미스의 버스트가 대량의 요청을 PostgreSQL로 직접 푸시할 수 있다. 이 갑작스러운 데이터베이스 읽기 증가는 상당한 리소스를 소비하여 서비스를 느리게 만든다.

캐시 잠금 메커니즘

캐시 미스 폭풍 중 과부하를 방지하기 위해 캐시 잠금(및 리싱) 메커니즘을 구현한다. 특정 키에서 미스한 단일 리더만 PostgreSQL에서 데이터를 가져온다.

동작 방식은 다음과 같다.

  1. 여러 요청이 동일한 캐시 키에서 미스
  2. 하나의 요청만 잠금을 획득하고 데이터를 검색하여 캐시를 다시 채움
  3. 다른 모든 요청은 PostgreSQL을 동시에 치는 대신 캐시가 업데이트될 때까지 대기

이는 중복 데이터베이스 읽기를 크게 줄이고 연쇄적인 부하 급증으로부터 시스템을 보호한다.

Read Replica 확장

과제: Primary는 모든 Read Replica에 WAL(Write Ahead Log) 데이터를 스트리밍한다. Replica 수가 증가하면 Primary가 더 많은 인스턴스에 WAL을 전송해야 하므로 네트워크 대역폭과 CPU 모두에 압력이 증가한다. 이는 더 높고 불안정한 Replica 지연을 유발하여 시스템을 안정적으로 확장하기 어렵게 만든다.

해결책: 지연시간을 최소화하기 위해 여러 지리적 리전에 걸쳐 약 50개의 Read Replica를 운영한다.

현재 아키텍처의 한계

현재 아키텍처에서는 Primary가 모든 Replica에 WAL을 스트리밍해야 한다. 현재 매우 큰 인스턴스 타입과 높은 네트워크 대역폭으로 잘 확장되지만, 결국 Primary를 과부하시키지 않고는 Replica를 무한정 추가할 수 없다.

Cascading Replication

이를 해결하기 위해 Azure PostgreSQL 팀과 Cascading Replication을 협력하고 있다. 중간 Replica가 다운스트림 Replica에 WAL을 릴레이하는 방식이다.

이 접근 방식은 Primary를 압도하지 않고 잠재적으로 100개 이상의 Replica로 확장할 수 있게 한다. 그러나 특히 페일오버 관리와 관련하여 추가적인 운영 복잡성도 도입한다.

이 기능은 아직 테스트 중이다. 프로덕션에 롤아웃하기 전에 견고하고 안전하게 페일오버할 수 있는지 확인할 예정이다.

Rate Limiting

과제: 특정 엔드포인트에 대한 갑작스러운 트래픽 급증, 비용이 많이 드는 쿼리의 급증, 또는 재시도 폭풍은 CPU, I/O, 커넥션 같은 중요한 리소스를 빠르게 소진시켜 광범위한 서비스 저하를 유발할 수 있다.

해결책: 여러 레이어에 걸쳐 Rate Limiting을 구현했다.

레이어목적
애플리케이션엔드포인트별 제한
커넥션 풀러커넥션 수 제한
프록시요청 처리량 제한
쿼리특정 쿼리 다이제스트 제한

갑작스러운 트래픽 급증이 데이터베이스 인스턴스를 압도하고 연쇄 장애를 일으키는 것을 방지한다.

재시도 폭풍 방지

과도하게 짧은 재시도 간격을 피하는 것도 중요하다. 짧은 간격은 재시도 폭풍을 일으킬 수 있다.

ORM 레이어 Rate Limiting

ORM 레이어를 개선하여 Rate Limiting을 지원하고 필요한 경우 특정 쿼리 다이제스트를 완전히 차단할 수 있게 했다. 이 대상화된 형태의 로드 셰딩은 비용이 많이 드는 쿼리의 갑작스러운 급증으로부터 빠른 복구를 가능하게 한다.

스키마 관리

과제: 컬럼 타입 변경 같은 작은 스키마 변경도 전체 테이블 재작성을 일으킬 수 있다. 따라서 스키마 변경을 신중하게 적용한다. 경량 작업으로 제한하고 전체 테이블을 재작성하는 것은 피한다.

해결책: 전체 테이블 재작성을 일으키지 않는 경량 스키마 변경만 허용한다.

스키마 변경 정책

작업정책
특정 컬럼 추가/제거테이블 재작성을 일으키지 않는 것만 허용
스키마 변경 타임아웃엄격한 5초 타임아웃 적용
인덱스 생성/삭제CONCURRENTLY 허용
새 테이블 추가PostgreSQL이 아닌 CosmosDB 같은 샤딩 시스템에 추가
테이블 필드 백필엄격한 Rate Limit 적용

백필 소요 시간

테이블 필드를 백필할 때 쓰기 급증을 방지하기 위해 엄격한 Rate Limit을 적용한다. 이 프로세스는 때때로 일주일 이상 걸릴 수 있지만 안정성을 보장하고 프로덕션 영향을 방지한다.

성과와 향후 계획

이 노력은 올바른 설계와 최적화를 통해 Azure PostgreSQL을 가장 큰 프로덕션 워크로드를 처리하도록 확장할 수 있음을 보여준다.

달성한 성과

지표수치
초당 쿼리 수수백만 QPS
Read Replica 수약 50개
복제 지연거의 0에 가까움
p99 클라이언트 측 지연시간낮은 두 자릿수 ms
가용성99.999% (Five-nines)
지난 12개월 SEV-0 인시던트1건

유일한 SEV-0 인시던트

지난 12개월 동안 유일한 SEV-0 PostgreSQL 인시던트는 ChatGPT ImageGen의 바이럴 출시 중 발생했다. 일주일 내에 1억 명 이상의 신규 사용자가 가입하면서 쓰기 트래픽이 갑자기 10배 이상 급증했다.

향후 계획

PostgreSQL이 얼마나 멀리 왔는지 만족하지만 미래 성장을 위한 충분한 활주로를 확보하기 위해 계속 한계를 밀어붙이고 있다.

진행 중인 작업

  • 샤딩 가능한 쓰기 중심 워크로드를 CosmosDB 같은 샤딩 시스템으로 이미 마이그레이션 완료
  • 남은 쓰기 중심 워크로드는 샤딩하기 더 어려움 - PostgreSQL Primary에서 쓰기를 더 오프로드하기 위해 적극적으로 마이그레이션 중
  • Azure와 협력하여 훨씬 더 많은 Read Replica로 안전하게 확장할 수 있도록 Cascading Replication 활성화 작업 중

장기적 탐색

앞으로 인프라 요구가 계속 증가함에 따라 추가적인 확장 접근 방식을 계속 탐색할 것이다. 샤딩된 PostgreSQL이나 대안적인 분산 시스템을 포함한다.

Reference