메인 콘텐츠로 건너뛰기

OpenAI 대규모 트래픽을 위한 PostgreSQL 확장 전략과 최적화 방법

요약

클립으로 정리됨 (생성형 AI 도구 활용)

출처 및 참고 : https://openai.com/index/scaling-postgresql/

핵심 요약

OpenAI는 단일 PostgreSQL 기본 인스턴스와 수십 개의 리드 레플리카만으로, 주로 읽기 위주의 초대형 트래픽(수백만 QPS)을 처리하도록 설계했다.

핵심은 "쓰기 최소화 + 읽기 분산 + 위험 격리 + 방어적 운영(캐시, 풀링, 레이트 리밋, 보수적 스키마 변경)"이다.

PostgreSQL 자체를 마법처럼 바꾸기보다, 애플리케이션·인프라·운영 전체를 함께 조율해 한계 근처까지 밀어붙인 사례라고 볼 수 있다.

단일 기본 인스턴스로도 버티는 구조의 핵심

OpenAI의 PostgreSQL 구조는 직관과 달리 "단일 기본(primary) + 다수 리드 레플리카"라는 매우 단순한 형태를 유지한다.

이 단일 기본 인스턴스가 모든 쓰기를 담당하고, 거의 50개에 달하는 리드 레플리카가 전 세계에서 읽기 트래픽을 분산 처리한다.

일반적으로 이 규모면 샤딩을 고려하지만, 기존 수많은 애플리케이션 엔드포인트와 트랜잭션 로직을 모두 샤딩 구조로 재설계하는 것은 비용과 리스크가 너무 크다.

대신 "읽기 위주"라는 워크로드 특성을 최대한 살리고, 쓰기가 많은 부분만 다른 시스템으로 탈출시키는 방식으로 PostgreSQL을 가능한 오랫동안 단일 인스턴스 구조로 유지하는 전략을 택했다.

PostgreSQL의 약점: MVCC와 쓰기 부담

PostgreSQL의 MVCC(multiversion concurrency control)는 동시성을 잘 지원하지만, 쓰기 입장에서는 비효율을 낳는다.

한 행의 한 필드만 변경해도 실제로는 전체 튜플(행) 복사본을 새 버전으로 작성하기 때문에, 쓰기량이 크게 부풀어 오른다.

이전 버전(죽은 튜플)이 쌓이면 읽기 시에도 여러 버전을 훑어야 해서 읽기 증폭이 생기고, 테이블·인덱스 부풀어오름(bloat), 인덱스 유지 비용 증가, autovacuum 튜닝 복잡도 같은 부작용이 따라온다.

즉 PostgreSQL은 대량 쓰기에는 구조적으로 불리하기 때문에, "쓰기 자체를 줄이고, 쓰기 많은 워크로드를 밖으로 빼는 것"이 스케일링의 첫 단계가 된다.

쓰기를 줄이고, 샤딩 가능한 워크로드부터 외부로 이동

OpenAI는 쓰기 부담을 줄이기 위해 먼저 "샤딩이 가능한(write-heavy) 도메인"을 PostgreSQL에서 떼어내 Azure Cosmos DB 같은 샤딩 시스템으로 옮긴다.

예를 들면 사용자별로 완전히 분리될 수 있는 로그, 이벤트, 일부 설정 데이터 같은 것들은 파티셔닝·샤딩이 상대적으로 쉽기 때문에 이런 대상이 된다.

애플리케이션 레벨에서도 중복 저장, 불필요한 업데이트를 찾고 없애며, 꼭 실시간이 아니어도 되는 쓰기는 지연 기록(lazy write)로 바꾸어 트래픽 스파이크를 완화한다.

컬럼 값 채우기(backfill)처럼 대량 쓰기가 필요한 작업은 강력한 레이트 리밋을 걸어 장기간에 걸쳐 천천히 수행함으로써, 운영 중인 서비스에 부담을 주지 않도록 설계한다.

읽기 부하는 레플리카로, 하지만 "쓰기 트랜잭션 내 읽기"는 더 조심

읽기 대부분은 레플리카로 내보내지만, 쓰기 트랜잭션 안에서 수행되는 읽기 쿼리는 기본 인스턴스에서만 처리해야 한다.

따라서 이 "기본 인스턴스에서 실행되는 읽기 쿼리"는 특히 더 비용이 낮아야 하고, 느린 쿼리가 절대 허용되지 않는다.

이 때문에 질의 최적화가 매우 중요해지며, ORM이 생성한 복잡한 조인 쿼리, 불필요한 다중 테이블 조인, 과도한 결과 집합 조회가 반복적으로 점검 대상이 된다.

결과적으로, 기본 인스턴스의 목표는 "쓰기+필수 읽기만 처리하고, 그 외는 모두 밖으로 밀어낸다"는 방향으로 설계된다.

쿼리 최적화: 복잡한 조인은 DB가 아니라 애플리케이션이 감당

운영 중 발견되는 장애 패턴 중 하나는, 갑자기 특정 복잡한 쿼리의 요청량이 튀면서 CPU를 잡아먹어 전체 지연이 커지는 경우다.

예를 들어 10개가 넘는 테이블을 한 번에 조인하는 질의가 스파이크를 일으키면, 그 순간 다른 모든 요청까지 느려지거나 타임아웃이 발생한다.

여기서의 교훈은 단순하다. OLTP 시스템(PostgreSQL)을 복잡한 OLAP 쿼리처럼 쓰지 말고, 다단계 조인이나 복잡한 로직은 가능한 한 애플리케이션 계층으로 끌어올려 여러 번의 단순 쿼리로 쪼개는 편이 훨씬 안전하다.

또한 idle_in_transaction_session_timeout 같은 설정으로 오래 열려 있는 세션이 autovacuum을 막지 않도록 하고, 장시간 유지되는 트랜잭션이 시스템 전체를 붙잡지 않게 한다.

단일 기본 인스턴스의 단점: 단일 장애 지점에 대응하는 법

쓰기 노드가 하나뿐이라는 것은 곧 "그 노드가 죽으면 전체 서비스가 위험하다"는 뜻이다.

OpenAI는 여기서 장애 영향을 줄이기 위해, "대부분의 중요 요청을 읽기-only로 설계하고 이를 레플리카로 보내는 것"을 철저히 적용한다.

이렇게 하면 기본 인스턴스에 장애가 발생해도, 쓰기 기능은 일시 중단되지만 읽기 기능(예: 많은 사용자 요청)은 레플리카에서 계속 동작할 수 있어 전체 장애 심각도가 낮아진다.

추가로, 기본 인스턴스는 고가용성(HA) 모드로 운영되며, 항상 준비된 hot standby를 유지해 빠른 승격(failover)이 가능하도록 한다. 이 과정에서 Azure PostgreSQL 팀과 협업해, 매우 높은 부하 상황에서도 안전하게 장애 조치가 이루어지도록 튜닝했다.

'시끄러운 이웃' 방지: 워크로드를 논리적으로 분리

동일한 PostgreSQL 클러스터에서 서로 다른 제품·기능이 함께 돌아가면, 한쪽의 비효율적인 쿼리나 트래픽 스파이크가 다른 중요한 기능까지 함께 망가뜨린다.

이를 막기 위해 OpenAI는 요청을 중요도에 따라 고우선순위/저우선순위로 나누고, 서로 다른 인스턴스나 클러스터로 분리해 운영한다.

새로운 기능에서 나오는 트래픽이나 실험적 기능은 저우선순위 인스턴스로 보내, 만약 쿼리가 비효율적이거나 요청량이 예측보다 크더라도 핵심 서비스 성능에는 영향을 주지 않게 한다.

이러한 논리적 격리는 제품 간에도 적용되어, 특정 제품의 프로모션이나 이벤트로 인한 트래픽 폭증이 다른 제품의 안정성을 위협하지 않도록 한다.

커넥션 풀링: PgBouncer로 연결 수·지연 시간 통제

클라이언트가 직접 PostgreSQL에 연결하면, 인스턴스당 최대 연결 수(예: 5,000개)에 쉽게 도달하거나, 연결이 놀고 있어도 자원을 잠식하는 문제가 생긴다.

OpenAI는 PgBouncer를 프록시로 두고, statement/transaction 단위 풀링을 사용해 실제 DB 연결 수를 크게 줄였다.

이 방식은 새 연결을 생성하는 대신 풀에 있는 연결을 재사용하기 때문에, 평균 연결 설정 시간이 50ms에서 5ms 수준으로 줄어드는 효과도 있었다.

PgBouncer는 각 리드 레플리카마다 여러 파드로 배치되고, 같은 지역(region) 내에서 애플리케이션·프록시·DB를 함께 배치해 네트워크 지연을 최소화한다.

단, 풀링 자체도 관리가 필요하므로 idle timeout 등 설정을 신중히 조정해, 사용되지 않는 연결이 누적되어 다시 한 번 자원을 고갈시키는 일이 없도록 한다.

캐시와 캐시 미스 폭풍을 견디는 '락/리스' 패턴

읽기 부하를 줄이기 위한 가장 강력한 도구는 캐시다.

하지만 캐시 레이어에 장애가 나거나, 특정 구간에서 캐시 적중률이 급락하면, 한꺼번에 많은 요청이 DB를 직접 두드려 CPU가 순식간에 포화될 수 있다.

이를 막기 위해 OpenAI는 캐시 키 단위로 "락 또는 리스"를 적용한다. 같은 키에 대한 요청이 동시에 캐시 미스를 내더라도, 오직 한 요청만 DB에서 데이터를 가져와 캐시를 채우고, 나머지 요청은 그 과정이 끝날 때까지 기다린다.

이 패턴은 흔히 "cache stampede" 문제를 해결하는 고전적 방법인데, 여기서는 PostgreSQL 과부하를 막는 중요한 보호막 역할을 한다.

수십 개 리드 레플리카를 더 늘리기 위한 복제 전략

현재 구조에서는 기본 인스턴스가 모든 리드 레플리카에 WAL(Write-Ahead Log)을 직접 스트리밍한다.

레플리카 수가 늘어날수록 기본 인스턴스는 더 많은 네트워크 대역과 CPU를 WAL 전송에 사용해야 하고, 이는 복제 지연(lag) 증가와 변동성을 가져온다.

이 구조로는 무한 증설이 어렵기 때문에, 중간 레플리카가 다른 레플리카로 WAL을 전달하는 "계층형(캐스케이딩) 복제"를 Azure PostgreSQL 팀과 함께 도입 준비 중이다.

이 방식의 장점은 기본 인스턴스의 부담은 유지하면서 레플리카 수를 훨씬 늘릴 수 있다는 점이고, 단점은 장애 시 어느 레플리카를 승격할지, 중간 노드 장애에 어떻게 대응할지 등 운영 복잡도가 크게 올라간다는 점이다.

레이트 리밋과 선택적 '로드 셰딩'으로 장애를 국소화

트래픽 스파이크, 고비용 쿼리, 짧은 간격의 과도한 재시도는 CPU, I/O, 연결 수를 한꺼번에 소진시키며 연쇄 장애를 만든다.

OpenAI는 애플리케이션 레벨, 커넥션 풀러, 프록시, 심지어 특정 쿼리 패턴(쿼리 다이제스트) 수준까지 레이트 리밋과 차단 기능을 적용한다.

ORM 레벨에서 특정 패턴의 쿼리에 제한을 걸거나 완전히 막아, 잘못된 코드 배포나 새로운 기능에서 나온 비효율적 쿼리가 전체 서비스에 파급되지 않도록 한다.

이처럼 "비싼 쿼리나 폭주하는 엔드포인트를 빠르게 골라서 버린다"는 공격적 로드 셰딩은, 전체 시스템을 지키기 위한 일종의 안전장치로 작동한다.

스키마 변경은 최대한 가볍게, 새 테이블은 다른 시스템으로

PostgreSQL에서 일부 스키마 변경은 전체 테이블 재작성(full table rewrite)을 유발하며, 운영 중 대규모 테이블에 이를 수행하면 장시간 락과 대규모 I/O로 심각한 장애로 이어질 수 있다.

그래서 OpenAI는 운영 중인 PostgreSQL에 대해 "가벼운 스키마 변경만 허용"한다는 원칙을 세웠다.

컬럼 추가·삭제 등 재작성 없이 가능한 변경만 허용하고, DDL 작업에는 5초 타임아웃을 강제해, 잘못된 변경이 오래 걸리며 시스템을 잡고 있는 상황을 원천 차단한다.

새로운 기능이 새로운 테이블을 필요로 하면, 애초에 PostgreSQL이 아니라 Cosmos DB 같은 샤딩 시스템에 그 테이블을 만들도록 가이드한다.

대용량 데이터 백필 역시 강력한 레이트 리밋을 걸어, 경우에 따라 일주일 이상에 걸쳐 천천히 수행하면서도 운영 트래픽에는 영향을 주지 않는 방식을 택한다.

인사이트

이 사례의 핵심은 "PostgreSQL을 특별히 튜닝해서 마법처럼 만든 것"이 아니라, 시스템 전체를 PostgreSQL의 강점과 약점에 맞춰 재설계했다는 점이다.

PostgreSQL을 대규모 서비스에서 활용하고 싶다면, 다음과 같은 관점을 참고할 만하다.

첫째, 가능한 한 읽기 위주로 설계하고, 쓰기 많은 도메인은 초기에부터 샤딩 가능한 저장소로 분리하라. 장기적으로 PostgreSQL에 남겨야 할 것은 "쓰기 트랜잭션이 복잡하고, 샤딩이 어렵지만 읽기가 훨씬 많은 데이터"다.

둘째, 복잡한 조인·리포팅·집계는 운영 DB에서 하지 말고, 애플리케이션·캐시·분석용 DB나 데이터 파이프라인으로 옮겨라. PostgreSQL을 OLTP에 집중시키면 훨씬 높은 안정성과 성능을 얻을 수 있다.

셋째, 캐시·커넥션 풀·레이트 리밋·워크로드 분리·HA는 선택이 아니라 필수 인프라로 생각해야 한다. 이 계층들이 함께 작동할 때 비로소 단일 기본 인스턴스를 한계까지 밀어붙일 수 있다.

마지막으로, 스키마 변경과 대량 데이터 작업은 "서버 리소스를 마음대로 써도 되는 작업"이 아니라 "서비스 전체를 멈출 수 있는 잠재적 위험 작업"으로 보고, 절차·도구·제한을 강하게 걸어두는 것이 장기적인 안정성에 큰 도움이 된다.

출처 및 참고 : Scaling PostgreSQL to power 800 million ChatGPT users | OpenAI

#PostgreSQL#대규모 트래픽#읽기 분산#커넥션 풀링#운영 안정성

이 노트는 요약·비평·학습 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.