Skalering af PostgreSQL til at drive 800 mio. ChatGPT‑brugere
Af Bohan Zhang, medlem af det tekniske personale
I årevis har PostgreSQL været et af de mest kritiske datasystemer bag kulisserne, der driver kerneprodukter som ChatGPT og OpenAI’s API. Efterhånden som vores brugerbase vokser hurtigt, er kravene til vores databaser også steget eksponentielt. I løbet af det seneste år er vores PostgreSQL-belastning vokset mere end tifold, og den fortsætter med at stige hurtigt.
Vores indsats for at forbedre vores produktionsinfrastruktur for at opretholde denne vækst afslørede en ny indsigt: PostgreSQL kan skaleres til pålideligt at understøtte langt større læsetunge arbejdsbelastninger, end mange tidligere troede muligt. Systemet (oprindeligt skabt af et team af forskere ved University of California, Berkeley) har gjort det muligt for os at understøtte massiv global trafik med en enkelt primær Azure PostgreSQL fleksibel serverinstans(åbner i et nyt vindue) og næsten 50 læsereplikaer fordelt på flere regioner globalt. Dette er historien om, hvordan vi har skaleret PostgreSQL hos OpenAI til at understøtte millioner af forespørgsler pr. sekund for 800 millioner brugere gennem grundige optimeringer og solidt ingeniørarbejde; vi vil også dele de vigtigste erfaringer, vi har lært undervejs.
Efter lanceringen af ChatGPT voksede trafikken i et hidtil uset tempo. For at understøtte det implementerede vi hurtigt omfattende optimeringer på både applikations- og PostgreSQL-databaselagene, skalerede op ved at øge instansstørrelsen og skalerede ud ved at tilføje flere læsereplikaer. Denne arkitektur har tjent os godt i lang tid. Med løbende forbedringer fortsætter det med at give rigelig mulighed for fremtidig vækst.
Det kan lyde overraskende, at en single-primary-arkitektur kan opfylde kravene i OpenAI’s skala; dog er det ikke enkelt at få dette til at fungere i praksis. Vi har set flere SEV’er forårsaget af Postgres-overbelastning, og de følger ofte det samme mønster: et upstream-problem forårsager en pludselig stigning i databasebelastningen, såsom udbredte cache-misses fra en fejl i caching-laget, en bølge af dyre multi-way joins, der mætter CPU’en, eller en write storm fra lanceringen af en ny funktion. Når ressourceudnyttelsen stiger, øges forespørgselslatensen, og anmodninger begynder at time out. Gentagne forsøg forstærker belastningen yderligere og udløser en ond cirkel, der kan forringe hele ChatGPT- og API-tjenesterne.
Selvom PostgreSQL skalerer godt til vores læsetunge arbejdsbelastninger, oplever vi stadig udfordringer i perioder med høj skrivetrafik. Dette skyldes i høj grad PostgreSQLs implementering af multiversion concurrency control (MVCC), hvilket gør det mindre effektivt til skriveintensive arbejdsbelastninger. For eksempel, når en forespørgsel opdaterer en tuple eller endda et enkelt felt, kopieres hele rækken for at skabe en ny version. Under store skrivebelastninger resulterer dette i betydelig skriveforstærkning. Det øger også læseamplifikation, da forespørgsler skal scanne gennem flere tuple-versioner (døde tuples) for at hente den seneste. MVCC medfører yderligere udfordringer såsom oppustning af tabeller og indekser, øget vedligeholdelsesoverhead for indekser og kompleks justering af autovacuum. (Du kan finde en dybdegående gennemgang af disse problemer i en blog, jeg skrev sammen med Prof. Andy Pavlo på Carnegie Mellon University kaldet The Part of PostgreSQL We Hate the Most(åbner i et nyt vindue), citeret(åbner i et nyt vindue) på PostgreSQLs Wikipedia-side.)
For at afbøde disse begrænsninger og reducere skrivetryk har vi migreret og fortsætter med at migrere shardbare (dvs. arbejdsbelastninger, der kan partitioneres horisontalt), skriveintensive arbejdsbelastninger til shardede systemer som Azure Cosmos DB, optimering af applikationslogik for at minimere unødvendige skrivninger. Vi tillader heller ikke længere at tilføje nye tabeller til den nuværende PostgreSQL-udrulning. Nye arbejdsbelastninger er som standard indstillet til de sharderede systemer.
Selv om vores infrastruktur har udviklet sig, er PostgreSQL forblevet uden sharding, med en enkelt primær instans, der håndterer alle skrivninger. Den primære begrundelse er, at opdeling af eksisterende applikationsarbejdsbelastninger ville være meget komplekst og tidskrævende, kræve ændringer af hundredvis af applikationsendepunkter og potentielt tage måneder eller endda år. Da vores arbejdsbelastninger primært er læsetunge, og vi har implementeret omfattende optimeringer, giver den nuværende arkitektur stadig rigelig kapacitet til at understøtte fortsat vækst i trafikken. Selvom vi ikke udelukker at opdele PostgreSQL i fremtiden, er det ikke en prioritet på kort sigt, da vi har tilstrækkelig kapacitet til nuværende og fremtidig vækst.
I de følgende afsnit vil vi dykke ned i de udfordringer, vi stod overfor, og de omfattende optimeringer, vi implementerede for at håndtere dem og forhindre fremtidige nedbrud, mens vi pressede PostgreSQL til dets grænser og skalerede det til millioner af forespørgsler pr. sekund (QPS).
Udfordring: Med kun én skribent kan en single-primary-opsætning ikke skalere skriveprocessen. Kraftige skriveaktivitetsspidsbelastninger kan hurtigt overbelaste den primære og påvirke tjenester som ChatGPT og vores API.
Løsning: Vi minimerer belastningen på den primære så meget som muligt—både læsninger og skrivninger—for at sikre, at den har tilstrækkelig kapacitet til at håndtere spidser i skrivebelastningen. Læsetrafik aflastes til replikaer, hvor det er muligt. Dog skal nogle læseforespørgsler forblive på den primære, fordi de er en del af skrivetransaktioner. For dem fokuserer vi på at sikre, at de er effektive og undgår langsomme forespørgsler. For skrivetrafik har vi migreret shardbare, skrivetunge arbejdsbelastninger til shardede systemer såsom Azure CosmosDB. Arbejdsbelastninger, der er sværere at opdele, men stadig genererer en høj skrivevolumen, tager længere tid at migrere, og den proces er stadig i gang. Vi optimerede også aggressivt vores applikationer for at reducere skrivebelastningen; for eksempel har vi rettet applikationsfejl, der forårsagede overflødige skrivninger, og indført forsinkede skrivninger, hvor det er passende, for at udjævne trafikspidser. Derudover håndhæver vi strenge hastighedsgrænser, når vi udfylder tabelfelter bagud, for at forhindre for stort skrivetryk.
Udfordring: Vi identificerede flere ressourcekrævende forespørgsler i PostgreSQL. Tidligere forårsagede pludselige stigninger i volumen i disse forespørgsler et stort CPU-forbrug, hvilket gjorde både ChatGPT og API-anmodninger langsommere.
Løsning: Nogle få dyre forespørgsler, såsom dem der forbinder mange tabeller, kan betydeligt forringe eller endda bringe hele tjenesten ned. Vi skal løbende optimere PostgreSQL-forespørgsler for at sikre, at de er effektive og undgå almindelige anti-mønstre i Online Transaction Processing (OLTP). For eksempel identificerede vi engang en ekstremt omkostningstung forespørgsel, der forbandt 12 tabeller, hvor spidser i denne forespørgsel var ansvarlige for tidligere høj-alvorligheds SEV'er. Vi bør undgå komplekse multi-table joins, når det er muligt. Hvis joins er nødvendige, har vi lært at overveje at opdele forespørgslen og flytte den komplekse join-logik til applikationslaget i stedet. Mange af disse problematiske forespørgsler genereres af Object-Relational Mapping-frameworks (ORM'er), så det er vigtigt at gennemgå de SQL-forespørgsler, de producerer, omhyggeligt og sikre, at de fungerer som forventet. Det er også almindeligt at finde langvarige inaktive forespørgsler i PostgreSQL. Konfigurering af timeouts som idle_in_transaction_session_timeout er afgørende for at forhindre dem i at blokere autovacuum.
Udfordring: Hvis en læsereplika går ned, kan trafikken stadig dirigeres til andre replikaer. Men at være afhængig af en enkelt skribent betyder at have et enkelt fejlpunkt. Hvis det går ned, påvirkes hele tjenesten.
Løsning: De mest kritiske anmodninger involverer kun læseforespørgsler. For at afbøde det enkelte fejlpunkt i primæren, flyttede vi disse læseoperationer fra skriveren til replikaer, hvilket sikrer, at disse forespørgsler kan fortsætte med at blive betjent, selv hvis primæren går ned. Selvom skriveoperationer stadig ville fejle, er påvirkningen reduceret; det er ikke længere en SEV0, da læseoperationer stadig er tilgængelige.
For at afbøde primære fejl kører vi den primære i High-Availability (HA)-tilstand med en hot standby, en kontinuerligt synkroniseret replika, der altid er klar til at overtage trafikhåndteringen. Hvis den primære går ned eller skal tages offline til vedligeholdelse, kan vi hurtigt promovere standbyen for at minimere nedetid. Azure PostgreSQL-teamet har gjort et betydeligt stykke arbejde for at sikre, at disse failovers forbliver sikre og pålidelige, selv under meget høj belastning. For at håndtere fejl i læsereplikaer, implementerer vi flere replikaer i hver region med tilstrækkelig kapacitetsbuffer, hvilket sikrer, at en enkelt replika-fejl ikke fører til et regionalt nedbrud.
Udfordring: Vi støder ofte på situationer, hvor visse forespørgsler forbruger en uforholdsmæssig stor mængde ressourcer på PostgreSQL-instanser. Dette kan føre til forringet ydeevne for andre arbejdsbyrder, der kører på de samme instanser. For eksempel kan lanceringen af en ny funktion introducere ineffektive forespørgsler, der i høj grad belaster PostgreSQL CPU'en, hvilket gør anmodninger langsommere for andre vigtige funktioner.
Solution: To mitigate the “noisy neighbor” problem, we isolate workloads onto dedicated instances to ensure that sudden spikes in resource-intensive requests don’t impact other traffic. Specifically, we split requests into low-priority and high-priority tiers and route them to separate instances. This way, even if a low-priority workload becomes resource-intensive, it won’t degrade the performance of high-priority requests. We apply the same strategy across different products and services as well, so that activity from one product does not affect the performance or reliability of another.
Challenge: Each instance has a maximum connection limit (5,000 in Azure PostgreSQL). It’s easy to run out of connections or accumulate too many idle ones. We’ve previously had incidents caused by connection storms that exhausted all available connections.
Solution: We deployed PgBouncer as a proxy layer to pool database connections. Running it in statement or transaction pooling mode allows us to efficiently reuse connections, greatly reducing the number of active client connections. This also cuts connection setup latency: in our benchmarks, the average connection time dropped from 50 milliseconds (ms) to 5 ms. Inter-region connections and requests can be expensive, so we co-locate the proxy, clients, and replicas in the same region to minimize network overhead and connection use time. Moreover, PgBouncer must be configured carefully. Settings like idle timeouts are critical to prevent connection exhaustion.
Hver læsereplika har sin egen Kubernetes-udrulning, der kører flere PgBouncer-pods. Vi kører flere Kubernetes-implementeringer bag den samme Kubernetes-tjeneste, som fordeler trafikken jævnt på tværs af pods.
Challenge: A sudden spike in cache misses can trigger a surge of reads on the PostgreSQL database, saturating CPU and slowing user requests.
Solution: To reduce read pressure on PostgreSQL, we use a caching layer to serve most of the read traffic. However, when cache hit rates drop unexpectedly, the burst of cache misses can push a large volume of requests directly to PostgreSQL. This sudden increase in database reads consumes significant resources, slowing down the service. To prevent overload during cache-miss storms, we implement a cache locking (and leasing) mechanism so that only a single reader that misses on a particular key fetches the data from PostgreSQL. When multiple requests miss on the same cache key, only one request acquires the lock and proceeds to retrieve the data and repopulate the cache. All other requests wait for the cache to be updated rather than all hitting PostgreSQL at once. This significantly reduces redundant database reads and protects the system from cascading load spikes.
Challenge: The primary streams Write Ahead Log (WAL) data to every read replica. As the number of replicas increases, the primary must ship WAL to more instances, increasing pressure on both network bandwidth and CPU. This causes higher and more unstable replica lag, which makes the system harder to scale reliably.
Solution: We operate nearly 50 read replicas across multiple geographic regions to minimize latency. However, with the current architecture, the primary must stream WAL to every replica. Although it currently scales well with very large instance types and high-network bandwidth, we can’t keep adding replicas indefinitely without eventually overloading the primary. To address this, we’re collaborating with the Azure PostgreSQL team on cascading replication(åbner i et nyt vindue), where intermediate replicas relay WAL to downstream replicas. This approach allows us to scale to potentially over a hundred replicas without overwhelming the primary. However, it also introduces additional operational complexity, particularly around failover management. The feature is still in testing; we’ll ensure it’s robust and can fail over safely before rolling it out to production.
Challenge: A sudden traffic spike on specific endpoints, a surge of expensive queries, or a retry storm can quickly exhaust critical resources such as CPU, I/O, and connections, which causes widespread service degradation.
Solution: We implemented rate-limiting across multiple layers—application, connection pooler, proxy, and query—to prevent sudden traffic spikes from overwhelming database instances and triggering cascading failures. It’s also crucial to avoid overly short retry intervals, which can trigger retry storms. We also enhanced the ORM layer to support rate limiting and when necessary, fully block specific query digests. This targeted form of load shedding enables rapid recovery from sudden surges of expensive queries.
Challenge: Even a small schema change, such as altering a column type, can trigger a full table rewrite(åbner i et nyt vindue). We therefore apply schema changes cautiously—limiting them to lightweight operations and avoiding any that rewrite entire tables.
Solution: Only lightweight schema changes are permitted, such as adding or removing certain columns that do not trigger a full table rewrite. We enforce a strict 5-second timeout on schema changes. Creating and dropping indexes concurrently is allowed. Schema changes are restricted to existing tables. If a new feature requires additional tables, they must be in alternative sharded systems such as Azure CosmosDB rather than PostgreSQL. When backfilling a table field, we apply strict rate limits to prevent write spikes. Although this process can sometimes take over a week, it ensures stability and avoids any production impact.
This effort demonstrates that with the right design and optimizations, Azure PostgreSQL can be scaled to handle the largest production workloads. PostgreSQL handles millions of QPS for read-heavy workloads, powering OpenAI’s most critical products like ChatGPT and the API platform. We added nearly 50 read replicas, while keeping replication lag near zero, maintained low-latency reads across geo-distributed regions, and built sufficient capacity headroom to support future growth.
This scaling works while still minimizing latency and improving reliability. We consistently deliver low double-digit millisecond p99 client-side latency and five-nines availability in production. And over the past 12 months, we’ve had only one SEV-0 PostgreSQL incident (it occurred during the viral launch(åbner i et nyt vindue) of ChatGPT ImageGen, when write traffic suddenly surged by more than 10x as over 100 million new users signed up within a week.)
While we’re happy with how far PostgreSQL has taken us, we continue to push its limits to ensure we have sufficient runway for future growth. We’ve already migrated the shardable write-heavy workloads to our sharded systems like CosmosDB. The remaining write-heavy workloads are more challenging to shard—we’re actively migrating those as well to further offload writes from the PostgreSQL primary. We’re also working with Azure to enable cascading replication so we can safely scale to significantly more read replicas.
Looking ahead, we’ll continue to explore additional approaches to further scale, including sharded PostgreSQL or alternative distributed systems, as our infrastructure demands continue to grow.
Skrevet af
Tak til
En særlig tak til Jon Lee, Sicheng Liu, Chaomin Yu og Chenglong Hao, der bidrog til dette indlæg, og til hele teamet, der hjalp med at skalere PostgreSQL. Vi vil også gerne takke Azure PostgreSQL-teamet for deres stærke partnerskab.


