OpenAI가 단일 프라이머리와 전 세계 다수의 읽기 복제본, 엄격한 최적화와 운영 전략을 통해 PostgreSQL을 초당 수백만 건의 질의까지 확장하며 ChatGPT와 API 트래픽을 안정적으로 처리한 방법과 그 과정에서 얻은 교훈.
URL: https://openai.com/index/scaling-postgresql/
수년간 PostgreSQL은 ChatGPT와 OpenAI API 같은 핵심 제품을 뒷단에서 지탱해 온 가장 중요한 데이터 시스템 중 하나였습니다. 사용자 기반이 빠르게 성장하면서 데이터베이스에 가해지는 요구도 기하급수적으로 증가했습니다. 지난 1년 동안 PostgreSQL 부하는 10배 이상 증가했으며, 지금도 빠르게 상승하고 있습니다.
이 성장을 지속하기 위해 프로덕션 인프라를 발전시키는 과정에서 새로운 사실을 확인했습니다. PostgreSQL은 많은 사람들이 기존에 가능하다고 생각했던 것보다 훨씬 더 큰 읽기 중심(read-heavy) 워크로드를 신뢰성 있게 지원하도록 확장할 수 있다는 점입니다. (초기에는 캘리포니아 대학교 버클리의 과학자 팀이 만든) 이 시스템 덕분에 우리는 단일 기본(primary) Azure PostgreSQL Flexible Server 인스턴스(새 창에서 열림)와 전 세계 여러 리전에 분산된 약 50개의 읽기 복제본(read replica)만으로 막대한 글로벌 트래픽을 처리할 수 있었습니다. 이 글은 OpenAI에서 PostgreSQL을 어떻게 확장해 엄격한 최적화와 탄탄한 엔지니어링을 통해 8억 명의 사용자에 대해 초당 수백만 건의 질의(QPS)를 지원하게 되었는지에 대한 이야기이며, 그 과정에서 얻은 핵심 교훈도 함께 다룹니다.
ChatGPT 출시 이후 트래픽은 전례 없는 속도로 증가했습니다. 이를 지원하기 위해 우리는 애플리케이션과 PostgreSQL 데이터베이스 계층 모두에서 광범위한 최적화를 빠르게 구현했고, 인스턴스 크기를 키워 스케일 업(scale up) 했으며, 더 많은 읽기 복제본을 추가해 스케일 아웃(scale out) 했습니다. 이 아키텍처는 오랫동안 훌륭히 작동해 왔고, 지속적인 개선을 통해 앞으로의 성장에도 충분한 여유를 제공하고 있습니다.
OpenAI 규모의 요구를 단일 프라이머리(single-primary) 아키텍처가 충족한다는 사실은 놀라울 수도 있습니다. 하지만 이를 실제로 구현하는 일은 단순하지 않습니다. 우리는 Postgres 과부하로 인해 발생한 여러 SEV(심각도 사건)를 겪었고, 그 양상은 대체로 비슷했습니다. 예를 들어 캐싱 계층 장애로 인한 광범위한 캐시 미스, CPU를 포화시키는 비싼 다중 조인(multi-way join)의 급증, 신규 기능 출시로 인한 쓰기 폭풍(write storm) 등 상위 계층의 문제가 데이터베이스 부하를 갑자기 끌어올립니다. 리소스 사용률이 올라가면 쿼리 지연이 증가하고 요청이 타임아웃되기 시작합니다. 이후 재시도가 부하를 더 증폭시키면서 악순환이 발생해 ChatGPT와 API 서비스 전체가 저하될 수 있습니다.
PostgreSQL은 우리 읽기 중심 워크로드에서는 잘 확장되지만, 쓰기 트래픽이 높은 구간에서는 여전히 어려움을 겪습니다. 이는 주로 PostgreSQL의 다중 버전 동시성 제어(MVCC) 구현 특성 때문에 쓰기 중심 워크로드에서 효율이 떨어지기 때문입니다. 예를 들어 어떤 쿼리가 튜플(tuple) 또는 단일 필드만 업데이트하더라도, 새 버전을 만들기 위해 행 전체(row)를 복사합니다. 쓰기 부하가 높을 때 이는 상당한 쓰기 증폭(write amplification) 으로 이어집니다. 또한 최신 값을 읽기 위해 여러 튜플 버전(죽은 튜플, dead tuples)을 스캔해야 하므로 읽기 증폭(read amplification) 도 증가합니다. MVCC는 테이블 및 인덱스 팽창(bloat), 인덱스 유지보수 오버헤드 증가, 복잡한 autovacuum 튜닝 같은 추가적인 문제도 유발합니다. (이에 대한 심층 분석은 카네기멜런대학교 Andy Pavlo 교수와 함께 작성한 블로그 글 The Part of PostgreSQL We Hate the Most(새 창에서 열림)에서 확인할 수 있으며, PostgreSQL 위키피디아 페이지에서도 인용(새 창에서 열림)되었습니다.)
이러한 한계를 완화하고 쓰기 압력을 줄이기 위해, 우리는 수평 분할이 가능한(즉 샤딩 가능한) 쓰기 중심 워크로드를 Azure Cosmos DB 같은 샤딩 시스템으로 이전해 왔고 현재도 이전 중입니다. 또한 불필요한 쓰기를 최소화하도록 애플리케이션 로직을 최적화했습니다. 더불어 현재 PostgreSQL 배포에는 새 테이블 추가를 허용하지 않습니다. 새로운 워크로드는 기본적으로 샤딩 시스템을 사용합니다.
인프라가 발전하는 동안에도 PostgreSQL은 샤딩하지 않고, 단일 프라이머리 인스턴스가 모든 쓰기를 처리하는 구조를 유지했습니다. 그 주된 이유는 기존 애플리케이션 워크로드를 샤딩하는 일이 매우 복잡하고 시간이 많이 들며, 수백 개의 애플리케이션 엔드포인트 변경이 필요하고 수개월~수년에 걸릴 수 있기 때문입니다. 워크로드가 주로 읽기 중심이고, 이미 광범위한 최적화를 적용했기 때문에 현재 아키텍처는 트래픽 증가를 계속 지원할 충분한 헤드룸(headroom)을 제공합니다. 향후 PostgreSQL 샤딩을 완전히 배제하는 것은 아니지만, 현재 및 미래 성장에 대한 여유가 충분하기 때문에 단기 우선순위는 아닙니다.
이후 섹션에서는 우리가 직면한 과제들과, 이를 해결하고 향후 장애를 방지하기 위해 적용한 광범위한 최적화를 살펴봅니다. PostgreSQL을 한계까지 밀어붙여 초당 수백만 건의 질의(QPS)로 확장한 과정입니다.
과제: 작성자가 하나뿐인(single writer) 단일 프라이머리 구성은 쓰기 확장이 불가능하다. 큰 쓰기 스파이크는 프라이머리를 빠르게 과부하시키고 ChatGPT 및 API 같은 서비스에 영향을 줄 수 있다.
해결책: 프라이머리에 걸리는 부하(읽기와 쓰기 모두)를 가능한 한 최소화해, 쓰기 스파이크를 처리할 충분한 용량을 확보합니다. 읽기 트래픽은 가능한 한 복제본으로 오프로딩(offloading)합니다. 다만 일부 읽기 쿼리는 쓰기 트랜잭션의 일부이기 때문에 프라이머리에 남아야 합니다. 이 경우 효율성을 높이고 느린 쿼리를 피하는 데 집중합니다. 쓰기 트래픽의 경우 샤딩 가능한 쓰기 중심 워크로드를 Azure CosmosDB 같은 샤딩 시스템으로 이전했습니다. 샤딩하기 더 어렵지만 쓰기량이 큰 워크로드는 이전에 시간이 더 걸리며, 현재도 그 과정이 진행 중입니다. 또한 애플리케이션을 공격적으로 최적화해 쓰기 부하를 줄였습니다. 예를 들어 중복 쓰기를 유발하던 애플리케이션 버그를 수정했고, 적절한 경우 지연 쓰기(lazy writes)를 도입해 트래픽 스파이크를 완화했습니다. 추가로, 테이블 필드 백필(backfill) 시에는 과도한 쓰기 압력을 방지하기 위해 엄격한 레이트 리밋(rate limit)을 적용합니다.
과제: PostgreSQL에서 비용이 큰(비싼) 쿼리를 여러 개 발견했다. 과거에는 이러한 쿼리의 갑작스러운 볼륨 스파이크가 CPU를 크게 소모해 ChatGPT와 API 요청이 느려지곤 했다.
해결책: 여러 테이블을 조인하는 등 소수의 비싼 쿼리만으로도 서비스 전체 성능을 크게 저하시킬 수 있고, 심지어 다운시킬 수도 있습니다. 따라서 PostgreSQL 쿼리를 지속적으로 최적화해 효율을 확보하고, 흔한 OLTP(Online Transaction Processing) 안티패턴을 피해야 합니다. 예를 들어 과거에는 12개 테이블을 조인하는 극도로 비싼 쿼리를 발견했고, 이 쿼리의 스파이크가 과거 고심각도 SEV의 원인이었습니다. 가능하다면 복잡한 다중 테이블 조인을 피해야 합니다. 조인이 필요하다면 쿼리를 분해하고 복잡한 조인 로직을 애플리케이션 계층으로 옮기는 것도 고려해야 한다는 점을 배웠습니다. 이런 문제 쿼리들은 ORM(Object-Relational Mapping) 프레임워크가 생성하는 경우가 많으므로, ORM이 만들어내는 SQL을 꼼꼼히 검토하고 기대한 대로 동작하는지 확인하는 것이 중요합니다. 또한 PostgreSQL에는 오래 실행되며 유휴 상태인(idle) 쿼리가 종종 발견됩니다. idle_in_transaction_session_timeout 같은 타임아웃을 설정하는 것은 autovacuum을 막지 않도록 하는 데 필수적입니다.
과제: 읽기 복제본이 하나 다운되면 트래픽을 다른 복제본으로 라우팅할 수 있다. 하지만 작성자가 단 하나인 구조는 단일 장애점(SPOF)을 의미한다. 작성자가 다운되면 서비스 전체가 영향을 받는다.
해결책: 가장 중요한 요청의 대부분은 읽기 쿼리만 포함합니다. 프라이머리의 단일 장애점을 완화하기 위해, 해당 읽기를 writer에서 복제본으로 오프로딩해 프라이머리가 다운되더라도 요청이 계속 처리되도록 했습니다. 쓰기는 여전히 실패하지만 영향은 줄어들며, 읽기가 유지되므로 더 이상 SEV0가 아닙니다.
프라이머리 장애를 완화하기 위해 프라이머리를 HA(고가용성) 모드로 운영하며, 항상 트래픽을 대신 처리할 준비가 된 지속 동기화 복제본인 핫 스탠바이(hot standby)를 둡니다. 프라이머리가 다운되거나 유지보수를 위해 오프라인해야 할 때, 스탠바이를 빠르게 승격(promote)해 다운타임을 최소화할 수 있습니다. Azure PostgreSQL 팀은 매우 높은 부하 하에서도 이러한 페일오버가 안전하고 신뢰성 있게 유지되도록 많은 작업을 해왔습니다. 읽기 복제본 장애에 대해서는 각 리전에 여러 복제본을 충분한 헤드룸과 함께 배치해, 단일 복제본 장애가 리전 장애로 이어지지 않도록 합니다.
과제: 특정 요청이 PostgreSQL 인스턴스 리소스를 과도하게 소비하는 상황이 자주 발생한다. 이는 동일 인스턴스에서 실행되는 다른 워크로드 성능 저하로 이어질 수 있다. 예를 들어 신규 기능 출시가 PostgreSQL CPU를 과도하게 소모하는 비효율적 쿼리를 도입해, 다른 핵심 기능 요청까지 느려지게 만들 수 있다.
해결책: “시끄러운 이웃(noisy neighbor)” 문제를 완화하기 위해, 워크로드를 전용 인스턴스로 격리해 리소스 집약적 요청의 급격한 스파이크가 다른 트래픽에 영향을 주지 않도록 합니다. 구체적으로 요청을 저우선순위와 고우선순위 티어로 나누고, 별도 인스턴스로 라우팅합니다. 이렇게 하면 저우선순위 워크로드가 리소스를 많이 소모하게 되더라도 고우선순위 요청 성능은 저하되지 않습니다. 이 전략은 서로 다른 제품과 서비스에도 동일하게 적용해, 한 제품의 활동이 다른 제품의 성능이나 신뢰성에 영향을 주지 않게 합니다.
과제: 각 인스턴스에는 최대 연결 수 제한이 있다(Azure PostgreSQL에서는 5,000). 연결이 고갈되거나 유휴 연결이 너무 많이 쌓이기 쉽다. 과거에는 사용 가능한 모든 연결을 소진시키는 커넥션 스톰(connection storm) 때문에 사고가 발생한 적이 있다.
해결책: 데이터베이스 연결을 풀링하기 위해 프록시 계층으로 PgBouncer를 배포했습니다. statement 또는 transaction 풀링 모드로 운영하면 연결을 효율적으로 재사용할 수 있어 활성 클라이언트 연결 수를 크게 줄일 수 있습니다. 또한 연결 설정 지연도 줄어듭니다. 벤치마크에서 평균 연결 시간이 50ms에서 5ms로 감소했습니다. 리전 간 연결과 요청은 비용이 클 수 있으므로, 프록시·클라이언트·복제본을 동일 리전에 공동 배치(co-locate)해 네트워크 오버헤드와 연결 점유 시간을 최소화합니다. 또한 PgBouncer는 신중하게 설정해야 하며, idle 타임아웃 같은 설정은 연결 고갈을 방지하는 데 매우 중요합니다.
과제: 캐시 미스가 갑자기 증가하면 PostgreSQL로 읽기 요청이 급증해 CPU가 포화되고 사용자 요청이 느려질 수 있다.
해결책: PostgreSQL에 대한 읽기 압력을 줄이기 위해, 우리는 대부분의 읽기 트래픽을 캐싱 계층에서 처리합니다. 그러나 캐시 적중률이 예상치 않게 떨어지면 캐시 미스가 폭발적으로 증가해 대량의 요청이 직접 PostgreSQL로 유입될 수 있습니다. 이 갑작스러운 읽기 증가는 큰 리소스를 소모해 서비스를 느리게 만듭니다. 캐시 미스 스톰 동안 과부하를 방지하기 위해, 특정 키에서 캐시 미스가 발생했을 때 단 하나의 리더만 PostgreSQL에서 데이터를 가져오도록 하는 캐시 락킹(및 리싱, leasing) 메커니즘을 구현했습니다. 동일 캐시 키에서 여러 요청이 미스가 나더라도, 오직 한 요청만 락을 획득해 데이터를 조회하고 캐시를 재채웁니다. 나머지 요청은 모두 동시에 PostgreSQL을 두드리는 대신 캐시가 업데이트될 때까지 기다립니다. 이는 중복 데이터베이스 읽기를 크게 줄이고 연쇄적인 부하 스파이크로부터 시스템을 보호합니다.
과제: 프라이머리는 모든 읽기 복제본에 WAL(Write Ahead Log) 데이터를 스트리밍한다. 복제본 수가 늘어날수록 프라이머리는 더 많은 인스턴스에 WAL을 전송해야 하므로 네트워크 대역폭과 CPU 모두에 압력이 증가한다. 이는 복제 지연(replica lag)을 높이고 불안정하게 만들어, 시스템을 신뢰성 있게 확장하기 어렵게 한다.
해결책: 지연을 최소화하기 위해 여러 지리적 리전에 걸쳐 약 50개의 읽기 복제본을 운영합니다. 그러나 현재 아키텍처에서는 프라이머리가 모든 복제본에 WAL을 스트리밍해야 합니다. 매우 큰 인스턴스 타입과 높은 네트워크 대역폭 덕분에 현재는 잘 확장되지만, 프라이머리가 결국 과부하되기 전에 복제본을 무한정 추가할 수는 없습니다. 이를 해결하기 위해 Azure PostgreSQL 팀과 함께 계단식 복제(cascading replication)(새 창에서 열림)를 협업하고 있습니다. 중간 복제본이 하위 복제본으로 WAL을 릴레이하는 방식으로, 프라이머리에 과부하를 주지 않으면서 잠재적으로 100개 이상의 복제본까지 확장할 수 있습니다. 다만 페일오버 관리 등 운영 복잡성이 추가로 증가합니다. 이 기능은 아직 테스트 중이며, 프로덕션에 배포하기 전에 견고하고 안전하게 페일오버할 수 있는지 확인할 예정입니다.
과제: 특정 엔드포인트의 갑작스러운 트래픽 스파이크, 비싼 쿼리의 급증, 또는 재시도 스톰이 CPU·I/O·연결 같은 핵심 리소스를 빠르게 고갈시켜 광범위한 서비스 저하를 유발할 수 있다.
해결책: 데이터베이스 인스턴스가 갑작스러운 트래픽 스파이크에 압도되어 연쇄 장애를 일으키지 않도록, 애플리케이션·커넥션 풀러·프록시·쿼리 등 여러 계층에 걸쳐 레이트 리밋을 구현했습니다. 또한 너무 짧은 재시도 간격은 재시도 스톰을 유발할 수 있으므로 피하는 것이 중요합니다. 더 나아가 ORM 계층을 개선해 레이트 리밋을 지원하고, 필요한 경우 특정 쿼리 다이제스트(query digest)를 완전히 차단할 수 있게 했습니다. 이런 표적형 로드 셰딩(load shedding)은 비싼 쿼리의 갑작스러운 급증에서 빠르게 회복하는 데 도움이 됩니다.
과제: 컬럼 타입 변경 같은 작은 스키마 변경조차도 _전체 테이블 재작성(full table rewrite)(새 창에서 열림)을 촉발할 수 있다. 따라서 스키마 변경은 신중하게 적용하며, 가벼운 작업으로 제한하고 테이블 전체를 재작성하는 작업은 피한다.
해결책: 전체 테이블 재작성을 유발하지 않는 특정 컬럼 추가/제거 같은 가벼운 스키마 변경만 허용합니다. 스키마 변경에는 엄격한 5초 타임아웃을 적용합니다. 인덱스를 concurrently로 생성·삭제하는 작업은 허용됩니다. 스키마 변경은 기존 테이블에만 제한됩니다. 신규 기능에 추가 테이블이 필요하다면 PostgreSQL이 아니라 Azure CosmosDB 같은 대체 샤딩 시스템에 만들어야 합니다. 테이블 필드 백필 시에는 쓰기 스파이크를 방지하기 위해 엄격한 레이트 리밋을 적용합니다. 이 과정이 때로는 일주일 이상 걸릴 수 있지만, 안정성을 보장하고 프로덕션 영향도를 방지합니다.
이번 노력은 적절한 설계와 최적화가 있다면 Azure PostgreSQL을 확장해 가장 큰 규모의 프로덕션 워크로드까지 처리할 수 있음을 보여줍니다. PostgreSQL은 읽기 중심 워크로드에서 초당 수백만 QPS를 처리하며, ChatGPT와 API 플랫폼 같은 OpenAI의 가장 중요한 제품들을 구동합니다. 우리는 약 50개의 읽기 복제본을 추가하면서도 복제 지연을 거의 0에 가깝게 유지했고, 지리적으로 분산된 리전 전반에서 낮은 지연의 읽기를 유지했으며, 미래 성장을 지원할 충분한 용량 여유를 구축했습니다.
이러한 확장은 지연을 최소화하고 신뢰성을 개선하면서 이루어졌습니다. 프로덕션에서 p99 클라이언트 측 지연을 항상 두 자릿수 밀리초(10~99ms) 수준으로 제공하고, 99.999%(파이브 나인) 가용성을 달성하고 있습니다. 또한 지난 12개월 동안 PostgreSQL 관련 SEV-0 사고는 단 한 번뿐이었습니다. (이는 ChatGPT ImageGen의 바이럴 런치(새 창에서 열림) 기간에 발생했는데, 1주일 안에 1억 명 이상의 신규 사용자가 가입하면서 쓰기 트래픽이 갑자기 10배 이상 급증했기 때문입니다.)
PostgreSQL이 우리를 여기까지 이끌어 준 성과에 만족하지만, 미래 성장에 대한 충분한 여유를 확보하기 위해 계속 한계를 밀어붙이고 있습니다. 샤딩 가능한 쓰기 중심 워크로드는 이미 CosmosDB 같은 샤딩 시스템으로 이전했습니다. 남아 있는 쓰기 중심 워크로드는 샤딩이 더 어렵지만, PostgreSQL 프라이머리에서 쓰기를 더 오프로딩하기 위해 이들 또한 적극적으로 이전 중입니다. 또한 더 많은 읽기 복제본으로 안전하게 확장할 수 있도록 Azure와 함께 계단식 복제를 활성화하는 작업을 진행하고 있습니다.
앞으로도 인프라 요구가 계속 증가함에 따라, 샤딩된 PostgreSQL이나 대체 분산 시스템 등 추가적인 확장 접근법을 계속 탐색해 나갈 계획입니다.