Skal. PostgreSQL-a kako bi se podržalo 800 mil. kor. ChatGPT‑a
Autor: Bohan Zhang, član tehničkog osoblja
Već godinama, PostgreSQL je jedan od najvažnijih, skrivenih sistema za podatke koji pokreće ključne proizvode kao što su ChatGPT i OpenAI-jev API. Kako naša korisnička baza brzo raste, zahtjevi prema našim bazama podataka također su eksponencijalno porasli. Tokom protekle godine, naše PostgreSQL opterećenje poraslo je više od 10 puta i nastavlja brzo rasti.
Naši napori da unaprijedimo našu proizvodnu infrastrukturu kako bismo podržali ovaj rast otkrili su novo saznanje: PostgreSQL se može skalirati da pouzdano podrži mnogo veća opterećenja sa dominantnim čitanjem nego što su mnogi ranije smatrali mogućim. Sistem (koji je prvobitno kreirao tim naučnika na Univerzitetu Kalifornija, Berkeley) omogućio nam je da podržimo ogroman globalni promet uz jednu primarnu Azure PostgreSQL fleksibilnu instancu servera(otvara se u novom prozoru) i skoro 50 replika za čitanje raspoređenih u više regija širom svijeta. Ovo je priča o tome kako smo skalirali PostgreSQL u OpenAI-ju da podržimo milione upita u sekundi za 800 miliona korisnika kroz rigorozne optimizacije i solidan inženjering; također ćemo podijeliti ključne lekcije koje smo naučili usput.
Nakon lansiranja ChatGPT‑a, promet je rastao neviđenom brzinom. Da bismo to podržali, brzo smo implementirali opsežne optimizacije na nivou aplikacije i PostgreSQL baze podataka, povećali smo veličinu instance i dodali više replika za čitanje. Ova arhitektura nas je dugo dobro služila. Uz stalna poboljšanja, nastavlja pružati dovoljno prostora za budući rast.
Možda zvuči iznenađujuće da arhitektura sa jednim primarnim čvorom može zadovoljiti zahtjeve razmjera OpenAI-a; međutim, ostvariti to u praksi nije jednostavno. Vidjeli smo nekoliko SEV-ova uzrokovanih preopterećenjem Postgresa, i često prate isti obrazac: problem uzvodno uzrokuje nagli skok opterećenja baze podataka, kao što su rašireni promašaji keša zbog kvara sloja za keširanje, porast skupih višestrukih spajanja koja zasićuju CPU, ili oluja upisa zbog lansiranja nove funkcionalnosti. Kako se iskorištenost resursa povećava, latencija upita raste i zahtjevi počinju isteći. Ponovni pokušaji dodatno pojačavaju opterećenje, pokrećući začarani krug s potencijalom da degradira cjelokupne usluge ChatGPT i API.
Iako PostgreSQL dobro skalira za naša opterećenja sa pretežno čitanjem, i dalje se suočavamo s izazovima tokom perioda visokog prometa upisa. Ovo je uglavnom zbog PostgreSQL-ove implementacije viševerzijske kontrole konkurentnosti (MVCC), što ga čini manje efikasnim za radna opterećenja s intenzivnim upisom. Na primjer, kada upit ažurira tuple ili čak jedno polje, cijeli red se kopira kako bi se stvorila nova verzija. Pod velikim opterećenjem pisanja, ovo rezultira značajnim pojačanjem pisanja. Također povećava amplifikaciju čitanja, jer upiti moraju pregledati više verzija torki (mrtve torke) kako bi dohvatili najnoviju verziju. MVCC uvodi dodatne izazove kao što su napuhavanje tabela i indeksa, povećano opterećenje održavanja indeksa i složeno podešavanje autovacuum-a. (Možete pronaći detaljnu analizu o ovim pitanjima u blogu koji sam napisao s prof. Andyjem Pavlom na Univerzitetu Carnegie Mellon pod nazivom Dio PostgreSQL-a koji najviše mrzimo(otvara se u novom prozoru), citiran(otvara se u novom prozoru) na Wikipedia stranici o PostgreSQL-u.)
Kako bismo ublažili ova ograničenja i smanjili pritisak pisanja, migrirali smo i nastavljamo migrirati, dijeljive (tj. radna opterećenja koja se mogu horizontalno particionirati), radna opterećenja s velikim brojem upisa u šardirane sisteme kao što je Azure Cosmos DB, optimizirajući logiku aplikacije kako bi se minimizirali nepotrebni upisi. Također više ne dopuštamo dodavanje novih tabela u trenutnu PostgreSQL implementaciju. Nova radna opterećenja po defaultu idu na šardirane sisteme.
Čak i dok se naša infrastruktura razvijala, PostgreSQL je ostao nešardiran, sa jednom primarnom instancom koja obrađuje sve upise. Glavni razlog je da bi podjela postojećih radnih opterećenja aplikacije bila izuzetno složena i dugotrajna, zahtijevajući promjene na stotinama krajnjih tačaka aplikacije i potencijalno trajući mjesecima ili čak godinama. Budući da su naša radna opterećenja uglavnom usmjerena na čitanje, a implementirali smo opsežne optimizacije, trenutna arhitektura i dalje pruža dovoljno prostora za podršku stalnom rastu prometa. Iako ne isključujemo mogućnost shardinga PostgreSQL-a u budućnosti, to nije prioritet u skorijem vremenu s obzirom na to da imamo dovoljno prostora za trenutni i budući rast.
U sljedećim odjeljcima ćemo se detaljno pozabaviti izazovima sa kojima smo se suočili i opsežnim optimizacijama koje smo implementirali kako bismo ih riješili i spriječili buduće prekide rada, dovodeći PostgreSQL do njegovih granica i skalirajući ga na milione upita u sekundi (QPS).
Izazov: Sa samo jednim piscem, postavka s jednim primarnim čvorom ne može skalirati upise. Veliki skokovi u zapisivanju mogu brzo preopteretiti primarni sistem i utjecati na usluge poput ChatGPT‑a i našeg API-ja.
Rješenje: Smanjujemo opterećenje na primarnom serveru koliko god je moguće—i za čitanje i za pisanje—kako bismo osigurali da ima dovoljno kapaciteta za podnošenje naglih povećanja u pisanju. Promet čitanja se preusmjerava na replike gdje god je to moguće. Međutim, neki upiti za čitanje moraju ostati na primarnom jer su dio transakcija upisivanja. Fokusiramo se na to da budu efikasni i da izbjegnemo spore upite. Za promet upisa, migrirali smo radna opterećenja s intenzivnim upisom koja se mogu dijeliti na sisteme s dijeljenjem kao što je Azure CosmosDB. Opterećenja koja je teže podijeliti, ali i dalje generiraju veliki obim upisa, duže se migriraju, a taj proces je još uvijek u toku. Također smo agresivno optimizirali naše aplikacije kako bismo smanjili opterećenje upisa; na primjer, ispravili smo greške u aplikacijama koje su uzrokovale suvišne upise i uveli odgođene upise, gdje je to prikladno, kako bismo ublažili skokove u prometu. Osim toga, kada popunjavamo polja tabele unazad, primjenjujemo stroga ograničenja brzine kako bismo spriječili prekomjeran pritisak upisa.
Izazov: Identificirali smo nekoliko skupih upita u PostgreSQL-u. U prošlosti su iznenadni skokovi u obimu ovih upita trošili velike količine CPU-a, usporavajući ChatGPT i API zahtjeve.
Rješenje: Nekoliko skupih upita, poput onih koji spajaju mnogo tabela, može značajno narušiti ili čak oboriti cijelu uslugu. Moramo kontinuirano optimizirati PostgreSQL upite kako bismo osigurali njihovu efikasnost i izbjegli uobičajene anti-obrasce za Online Transaction Processing (OLTP). Na primjer, jednom smo identificirali izuzetno skup upit koji je spajao 12 tabela, gdje su skokovi u ovom upitu bili odgovorni za prethodne SEV-ove visokog stepena ozbiljnosti. Trebali bismo izbjegavati složena spajanja više tablica kad god je to moguće. Ako su spajanja neophodna, naučili smo da razmotrimo razlaganje upita i premjestimo složenu logiku spajanja u aplikacijski sloj. Mnogi od ovih problematičnih upita generiraju se pomoću okvira za mapiranje objekata i relacija (ORM), pa je važno pažljivo pregledati SQL koji oni proizvode i osigurati da se ponaša kako se očekuje. Također je uobičajeno pronaći dugotrajne neaktivne upite u PostgreSQL-u. Konfigurisanje vremenskih ograničenja kao što je idle_in_transaction_session_timeout je ključno kako bi se spriječilo njihovo blokiranje autovacuum procesa.
Izazov: Ako replika za čitanje padne, promet se i dalje može preusmjeriti na druge replike. Međutim, oslanjanje na jednog pisca znači imati jednu tačku otkaza—ako dođe do kvara, cijela usluga je pogođena.
Rješenje: Većina najkritičnijih zahtjeva odnosi se samo na upite za čitanje. Kako bismo ublažili jedinstvenu tačku otkaza na primarnom, prebacili smo ta čitanja sa pisca na replike, osiguravajući da ti zahtjevi mogu nastaviti s opsluživanjem čak i ako primarni padne. Iako bi operacije pisanja i dalje neuspjele, utjecaj je smanjen; više nije SEV0 jer su operacije čitanja i dalje dostupne.
Kako bismo ublažili primarne kvarove, primarni pokrećemo u režimu visoke dostupnosti (HA) sa vrućom rezervom, kontinuirano sinhroniziranom replikom koja je uvijek spremna preuzeti opsluživanje prometa. Ako primarni sistem padne ili ga je potrebno isključiti radi održavanja, možemo brzo unaprijediti rezervni kako bismo smanjili vrijeme zastoja. Tim Azure PostgreSQL-a je obavio značajan posao kako bi osigurao da ovi failoveri ostanu sigurni i pouzdani čak i pod vrlo velikim opterećenjem. Da bismo se nosili s kvarovima replika za čitanje, postavljamo više replika u svakoj regiji s dovoljnom rezervom kapaciteta, osiguravajući da kvar jedne replike ne dovede do regionalnog prekida.
Izazov: Često se susrećemo sa situacijama u kojima određeni zahtjevi troše nesrazmjerno veliku količinu resursa na PostgreSQL instancama. Ovo može dovesti do smanjenja performansi za druga radna opterećenja koja se izvode na istim instancama. Na primjer, lansiranje nove funkcije može uvesti neefikasne upite koji značajno opterećuju CPU PostgreSQL-a, usporavajući zahtjeve za ostale ključne funkcionalnosti.
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.
Svaka replika za čitanje ima vlastitu Kubernetes implementaciju koja pokreće više PgBouncer podova. Pokrećemo više Kubernetes deploymenta iza istog Kubernetes servisa, koji balansira saobraćaj preko podova.
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(otvara se u novom prozoru), 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(otvara se u novom prozoru). 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(otvara se u novom prozoru) 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.
Autor
Priznanja
Posebna zahvala Jonu Leeju, Sichengu Liuu, Chaominu Yuu i Chenglongu Haou, koji su doprinijeli ovom postu, kao i cijelom timu koji je pomogao u skaliranju PostgreSQL-a. Također bismo željeli zahvaliti timu Azure PostgreSQL na njihovom snažnom partnerstvu.


