Škálovanie PostgreSQL – obsluha 800 miliónov používateľov ChatGPT
Bohan Zhang, člen technického personálu
Už roky je PostgreSQL jedným z najkritickejších dátových systémov v pozadí, ktoré poháňajú kľúčové produkty ako ChatGPT a OpenAI API. Ako naša používateľská základňa rýchlo rastie, nároky na naše databázy sa tiež exponenciálne zvýšili. Za posledný rok sa naše zaťaženie PostgreSQL zvýšilo viac ako 10-násobne a naďalej rýchlo rastie.
Naše úsilie o modernizáciu našej produkčnej infraštruktúry s cieľom udržať tento rast odhalilo nový poznatok: PostgreSQL možno škálovať tak, aby spoľahlivo podporoval oveľa väčšie pracovné záťaže zamerané na čítanie, než si mnohí predtým mysleli, že je možné. Systém (pôvodne vytvorený tímom vedcov na Kalifornskej univerzite v Berkeley) nám umožnil podporovať masívnu globálnu návštevnosť pomocou jednej primárnej inštancie flexibilného servera Azure PostgreSQL(otvorí sa v novom okne) a takmer 50 replík na čítanie rozmiestnených vo viacerých regiónoch po celom svete. Toto je príbeh o tom, ako sme v OpenAI škálovali PostgreSQL, aby sme vďaka dôsledným optimalizáciám a spoľahlivému inžinierstvu podporili milióny dopytov za sekundu pre 800 miliónov používateľov. Zároveň sa budeme venovať kľúčovým poznatkom, ktoré sme sa na tejto ceste naučili.
Po spustení ChatGPT rástla návštevnosť nevídaným tempom. Aby sme to podporili, rýchlo sme zaviedli rozsiahle optimalizácie na úrovni aplikácie aj databázy PostgreSQL, škálovali sme nahor zväčšením veľkosti inštancie a škálovali sme horizontálne pridaním ďalších replík na čítanie. Táto architektúra nám dobre slúžila už dlhý čas. Vďaka neustálym zlepšovaniam naďalej poskytuje dostatočný priestor na budúci rast.
Môže byť prekvapujúce, že architektúra s jedným primárnym uzlom dokáže splniť požiadavky na škálovanie OpenAI. Avšak, uviesť to do praxe nie je jednoduché. Zaznamenali sme niekoľko SEVov spôsobených preťažením Postgresu a často sa riadia rovnakým vzorom: problém v upstream vrstve spôsobí náhly nárast záťaže databázy, ako sú rozsiahle „cache missy“ v dôsledku zlyhania cache vrstvy, nárast nákladných viacnásobných joinov, ktoré nasýtia CPU, alebo „write storm“ pri spustení novej funkcie. Ako sa zvyšuje využitie zdrojov, latencia dopytov rastie a požiadavky začnú časovo exspirovať. Opakované pokusy ešte viac zvyšujú záťaž, čím spúšťajú začarovaný kruh, ktorý môže zhoršiť celé služby ChatGPT a API.
Hoci sa PostgreSQL dobre škáluje pre naše pracovné záťaže s prevahou čítania, stále čelíme výzvam počas období intenzívneho zápisu. Je to do veľkej miery spôsobené implementáciou multiverzionálneho riadenia súbežnosti (MVCC) v PostgreSQL, ktorá ho robí menej efektívnym pri pracovných záťažiach s vysokým podielom zápisov. Napríklad, keď dotaz aktualizuje n-ticu alebo dokonca jedno pole, celý riadok sa skopíruje, aby sa vytvorila nová verzia. Pri vysokom zaťažení zápisov to vedie k výraznému zosilneniu zápisov. Zvyšuje to aj čítaciu amplifikáciu, keďže dopyty musia prechádzať cez viaceré verzie n-tíc (mŕtve n-tice), aby získali tú najnovšiu. MVCC prináša ďalšie výzvy, ako je nafukovanie tabuliek a indexov, zvýšená réžia na údržbu indexov a zložité ladenie autovacuumu. (Podrobný rozbor týchto problémov nájdete v blogu, ktorý som napísal s prof. Andym Pavlom z Carnegie Mellon University s názvom The Part of PostgreSQL We Hate the Most(otvorí sa v novom okne), citovaný(otvorí sa v novom okne) na stránke PostgreSQL na Wikipédii.)
Na zmiernenie týchto obmedzení a zníženie tlaku na zápis sme migrovali a naďalej migrujeme „shardovateľné„ (t. j. pracovné záťaže, ktoré možno horizontálne rozdeliť), zápisovo náročné pracovné záťaže do shardovaných systémov, ako je Azure Cosmos DB, optimalizácia aplikačnej logiky na minimalizáciu zbytočných zápisov. Taktiež už neumožňujeme pridávať nové tabuľky do aktuálneho nasadenia PostgreSQL. Nové pracovné záťaže majú predvolene nastavené shardované systémy.
Aj keď sa naša infraštruktúra vyvíjala, PostgreSQL zostal nešardovaný, pričom všetky zápisy obsluhuje jedna primárna inštancia. Hlavným dôvodom je, že rozdelenie existujúcich aplikačných pracovných záťaží by bolo veľmi zložité a časovo náročné, vyžadovalo by si zmeny v stovkách aplikačných koncových bodov a mohlo by trvať mesiace alebo dokonca roky. Keďže naše pracovné záťaže sú primárne zamerané na čítanie a implementovali sme rozsiahle optimalizácie, súčasná architektúra stále poskytuje dostatočnú rezervu na podporu pokračujúceho rastu návštevnosti. Hoci nevylučujeme možnosť shardingu PostgreSQL v budúcnosti, momentálne to nie je prioritou, pretože máme dostatočnú rezervu pre súčasný a budúci rast.
V nasledujúcich sekciách sa ponoríme do výziev, ktorým sme čelili, a rozsiahlych optimalizácií, ktoré sme implementovali na ich riešenie a prevenciu budúcich výpadkov, pričom sme PostgreSQL posunuli na hranice jeho možností a škálovali ho na milióny dopytov za sekundu (QPS).
Výzva: Pri iba jednom autorovi sa nastavenie s jedným primárnym uzlom nedá škálovať na zápisy. Silné špičky zápisov môžu rýchlo preťažiť primárny systém a ovplyvniť služby ako ChatGPT a naše API.
Riešenie: Minimalizujeme zaťaženie primárneho uzla čo najviac, pri čítaní aj zápise, aby sme zabezpečili, že má dostatočnú kapacitu na zvládnutie špičiek v zápise. Čítanie dát je presmerované na repliky, kdekoľvek je to možné. Niektoré dopyty na čítanie však musia zostať na primárnom uzle, pretože sú súčasťou transakcií zápisu. Zameriavame sa na to, aby boli efektívne a vyhli sa pomalým dotazom. Pre zápisovú prevádzku sme presunuli škálovateľné, na zápis náročné pracovné záťaže do shardovaných systémov, ako je Azure CosmosDB. Pracovné záťaže, ktoré sa ťažšie rozdeľujú, ale stále generujú vysoký objem zápisov, sa migrujú dlhšie a tento proces stále prebieha. Taktiež sme agresívne optimalizovali naše aplikácie, aby sme znížili záťaž zápisov. Napríklad sme opravili chyby aplikácií, ktoré spôsobovali redundantné zápisy, a tam, kde to bolo vhodné, sme zaviedli oneskorené zápisy, aby sme vyhladili špičky v prevádzke. Okrem toho pri spätnom dopĺňaní polí tabuľky uplatňujeme prísne limity rýchlosti, aby sme zabránili nadmernému tlaku na zápisy.
Výzva: Identifikovali sme niekoľko nákladných dotazov v PostgreSQL. V minulosti náhle nárasty objemu týchto dopytov spotrebúvali veľké množstvo CPU, čo spomaľovalo ChatGPT aj API požiadavky.
Riešenie: Niekoľko nákladných dopytov, ako napríklad tie, ktoré spájajú mnoho tabuliek, môže výrazne zhoršiť alebo dokonca znefunkčniť celú službu. Je potrebné neustále optimalizovať dopyty PostgreSQL, aby boli efektívne a aby sme sa vyhli bežným anti-vzorom Online Transaction Processing (OLTP). Napríklad sme raz identifikovali mimoriadne nákladný dotaz, ktorý spájal 12 tabuliek, pričom špičky v tomto dotaze boli zodpovedné za minulé SEV s vysokou závažnosťou. Mali by sme sa vyhýbať zložitým spájaniam viacerých tabuliek, keď je to možné. Ak sú spájania nevyhnutné, naučili sme sa zvážiť rozdelenie dopytu a presunúť zložitú logiku spájania do aplikačnej vrstvy. Mnohé z týchto problematických dopytov generujú frameworky Object-Relational Mapping (ORM), preto je dôležité starostlivo skontrolovať SQL, ktoré vytvárajú, a uistiť sa, že sa správa podľa očakávaní. Je tiež bežné nájsť dlhodobo nečinné dotazy v PostgreSQL. Nastavenie časových limitov, ako je idle_in_transaction_session_timeout, je nevyhnutné na zabránenie blokovaniu autovacuum.
Výzva: Ak dôjde k výpadku repliky na čítanie, prevádzka môže byť stále presmerovaná na iné repliky. Avšak spoliehať sa na jediného zapisovateľa znamená mať jediný bod zlyhania. Ak zlyhá, ovplyvní to celú službu.
Riešenie: Väčšina najkritickejších požiadaviek zahŕňa iba čítacie dopyty. Aby sme zmiernili riziko jediného bodu zlyhania v primárnom systéme, presunuli sme tieto čítania zo zapisovača na repliky, čím sme zabezpečili, že tieto požiadavky môžu byť naďalej obsluhované aj v prípade, že primárny systém vypadne. Hoci by operácie zápisu stále zlyhávali, dopad je znížený. Už to nie je SEV0, keďže čítania zostávajú dostupné.
Aby sme zmiernili primárne zlyhania, prevádzkujeme primárny systém v režime vysokej dostupnosti (HA) s horúcou zálohou, nepretržite synchronizovanou replikou, ktorá je vždy pripravená prevziať obsluhu prevádzky. Ak primárny systém zlyhá alebo ho bude potrebné odstaviť kvôli údržbe, môžeme rýchlo povýšiť záložný systém, aby sme minimalizovali prestoje. Tím Azure PostgreSQL vykonal významnú prácu na zabezpečení, že tieto prepnutia pri zlyhaní zostanú bezpečné a spoľahlivé aj pri veľmi vysokom zaťažení. Na riešenie zlyhaní replík na čítanie nasadzujeme v každom regióne viacero replík s dostatočnou kapacitnou rezervou, čím zabezpečujeme, že zlyhanie jednej repliky nepovedie k výpadku v regióne.
Výzva: Často sa stretávame so situáciami, keď určité požiadavky spotrebúvajú neprimerané množstvo zdrojov na inštanciách PostgreSQL. To môže viesť k zhoršeniu výkonu pre iné úlohy bežiace na rovnakých inštanciách. Napríklad spustenie novej funkcie môže zaviesť neefektívne dopyty, ktoré výrazne zaťažujú CPU PostgreSQL, čo spomaľuje požiadavky na iné kritické funkcie.
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.
Každá replika na čítanie má vlastné nasadenie Kubernetes, ktoré spúšťa viacero podov PgBouncer. Prevádzkujeme viacero nasadení Kubernetes za tou istou službou Kubernetes, ktorá vyvažuje záťaž a rozdeľuje prevádzku medzi pody.
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(otvorí sa v novom okne), 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(otvorí sa v novom okne). 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(otvorí sa v novom okne) 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
Poďakovania
Osobitné poďakovanie patrí Jonovi Lee, Sichengovi Liu, Chaominovi Yu a Chenglongovi Hao, ktorí prispeli k tomuto príspevku, a celému tímu, ktorý pomohol škálovať PostgreSQL. Radi by sme sa tiež poďakovali tímu Azure PostgreSQL za silné partnerstvo.


