Škálování PostgreSQL, aby zvládlo 800 milionů uživatelů ChatGPT
Bohan Zhang, člen technického týmu
PostgreSQL je již řadu let jedním z nejdůležitějších datových systémů, na kterých stojí klíčové produkty, jako je ChatGPT a API OpenAI. Tím, jak rychle roste naše uživatelská základna, zvýšily se exponenciálně také nároky na naše databáze. Za poslední rok se naše zatížení PostgreSQL zvýšilo více než desetinásobně a stále rychle roste.
Naše snahy o vylepšení produkční infrastruktury, aby bylo možné tento růst udržet, přinesly nový poznatek: systém PostgreSQL lze škálovat tak, aby spolehlivě zvládl mnohem větší zatížení s převažujícími operacemi čtení, než si mnozí dříve mysleli. Systém (původně vytvořený týmem vědců na University of California v Berkeley) nám umožnil zvládnout masivní globální provoz s jedinou primární instancí flexibilního serveru Azure PostgreSQL(otevře se v novém okně) a téměř 50 replikami pro čtení rozmístěnými ve více regionech po celém světě. Toto je příběh o tom, jak jsme v OpenAI škálovali systém PostgreSQL, abychom díky důsledným optimalizacím a kvalitní vývojářské práci zvládli miliony dotazů za sekundu pro 800 milionů uživatelů; probereme také klíčové poznatky, která jsme se během tohoto procesu naučili.
Po spuštění služby ChatGPT vzrostla návštěvnost nebývalým tempem. Abychom to zvládli, rychle jsme zavedli rozsáhlé optimalizace na úrovni aplikace i databáze PostgreSQL, škálovali jsme vertikálně zvětšením velikosti instance a horizontálně přidáním dalších replik pro čtení. Tahle architektura nám už dlouho dobře slouží. Díky neustálým vylepšením stále nabízí dostatek prostoru pro budoucí růst.
Může znít překvapivě, že architektura s jedním primárním prvkem může splnit rozsáhlé požadavky OpenAI, nicméně uvedení do praxe není snadné. Zaznamenali jsme několik případů vážných incidentů způsobených přetížením systému Postgres, které často mají stejný průběh: problém na upstreamu způsobí náhlý nárůst zatížení databáze, mimo jiné rozsáhlé výpadky mezipaměti v důsledku selhání vrstvy cache, nárůst drahých vícecestných spojení, které zahltí procesor, nebo nával zápisů v důsledku spuštění nové funkce. S rostoucím využitím zdrojů roste latence dotazů a požadavky častěji vyprší s časovým limitem. Opakované pokusy pak dále zvyšují zátěž, čímž spouštějí začarovaný kruh, který může zhoršit celé fungování služeb ChatGPT a API.
Ačkoli PostgreSQL dobře škáluje naše pracovní zátěže náročné na čtení, stále se setkáváme s problémy v obdobích s vysokým objemem zápisu. To je z velké části způsobeno implementací multigenerační architektury (MVCC) v PostgreSQL, díky které je méně efektivní pro pracovní zátěže s vysokým podílem zápisů. Například když dotaz aktualizuje záznam nebo třeba jen jedno pole, celý řádek se zkopíruje a vytvoří se jeho nová verze Při vysoké zátěži zápisu to vede k výraznému nárůstu zápisů. Zvyšuje to množství dat, která se musí při čtení projít, protože dotazy musí prohledávat více verzí záznamů (neaktivní záznamy), aby načetly tu nejnovější. Multigenerační architektura přináší další výzvy, jako je narůst počtu tabulek a indexů, zvýšená režie údržby indexů a složité ladění autovakua. (Těmto problémům se podrobně věnuji v blogu, který jsem napsal s prof. Andym Pavlem z Carnegie Mellon University s názvem The Part of PostgreSQL We Hate the Most (Nejnenáviděnější část PostgreSQL)(otevře se v novém okně), citovaný(otevře se v novém okně) na stránce o PostgreSQL na Wikipedii.)
Abychom zmírnili tato omezení a snížili tlak na zápis, migrovali jsme a nadále migrujeme pracovní zátěže s vysokým podílem zápisů (tj. pracovní zátěže, které lze horizontálně rozdělit), které lze shardovat do systémů, jako je Azure Cosmos DB, a optimalizujeme aplikační logiku, aby se minimalizovaly zbytečné zápisy. Do stávajícího nasazení PostgreSQL již také není možné přidávat nové tabulky. Nové zátěže se ve výchozím nastavení směrují do shardovaných systémů.
I když se naše infrastruktura vyvíjela, systém PostgreSQL zůstal neshardovaný, s jedinou primární instancí, která obsluhuje všechny zápisy. Hlavním důvodem je, že shardování stávajících aplikačních úloh by bylo velmi složité a časově náročné, vyžadovalo by změny stovek koncových bodů aplikací a mohlo by trvat měsíce nebo dokonce roky. Vzhledem k tomu, že naše pracovní zátěže jsou náročné především na čtení a že jsme provedli rozsáhlé optimalizace, poskytuje současná architektura stále dostatek prostoru na to, aby zvládla další růst provozu. Ačkoli nevylučujeme, že v budoucnu budeme PostgreSQL shardovat, není to naše krátkodobá priorita vzhledem k tomu, že máme dostatek prostoru pro současný i budoucí růst.
V následujících kapitolách se budeme podrobně věnovat problémům, kterým jsme čelili, a rozsáhlým optimalizacím, které jsme implementovali, abychom tyto problémy vyřešili, zabránili budoucím výpadkům, posunuli PostgreSQL na hranice jeho možností a škálovali tento systém na miliony dotazů za sekundu (QPS).
Výzva: Pokud je jediná writer instance, nelze v uspořádání s jedním primárním systémem škálovat zápisy. Silné špičky zápisů mohou rychle přetížit primární systém a ovlivnit služby jako ChatGPT a naše API.
Řešení: Minimalizujeme zatížení primárního systému na minimum – jak při čtení, tak při zápisu – abychom zajistili dostatečnou kapacitu pro zvládnutí nárazových zápisů. Čtení je pokud možno přenášeno na repliky. Některé dotazy pro čtení však musí zůstat na primárním systému, protože jsou součástí transakcí se zápisem. U těch se zaměřujeme na to, aby byly efektivní a vyhýbaly se pomalým dotazům. Pokud jde o zápisy, převedli jsme pracovní úkoly náročné na zápis do shardovaných systémů, jako je Azure CosmosDB. Migrace pracovních úkolů, které se hůře shardují, ale stále generují vysoký objem zápisů, trvá déle a tento proces stále probíhá. Také jsme výrazně optimalizovali naše aplikace, abychom snížili zátěž zápisy; například jsme opravili chyby v aplikacích, které způsobovaly redundantní zápisy, a tam, kde to bylo vhodné, jsme zavedli odložené zápisy, aby se vyhladily špičky v provozu. Při zpětném plnění polí tabulek navíc uplatňujeme přísné limity rychlosti, abychom zabránili nadměrnému tlaku na zápis.
Výzva: Identifikovali jsme několik nákladných dotazů v PostgreSQL. Náhlé špičky v objemu těchto dotazů spotřebovávaly v minulosti velké množství procesoru, což zpomalovalo jak ChatGPT, tak i požadavky na API.
Řešení: Několik nákladných dotazů, jako jsou ty, které spojují mnoho tabulek, může výrazně zhoršit výkon nebo dokonce shodit celou službu. Dotazy PostgreSQL musíme neustále optimalizovat, abychom zajistili jejich efektivitu a zabránili těmto špatným návykům při zpracování online transakcí (OLTP). Jednou jsme například identifikovali extrémně nákladný dotaz, který spojoval 12 tabulek, přičemž špičky v tomto dotazu byly zodpovědné za dřívější vysoce závažné incidenty. Pokud je to možné, měli bychom se vyhnout složitému spojování více tabulek. Pokud je spojování nutné, naučili jsme se zvážit rozdělení dotazu a přesunutí složité logiky spojování do aplikační vrstvy. Mnoho těchto problematických dotazů generují frameworky objektově-relačního mapování (ORM), proto je důležité pečlivě zkontrolovat jimi vytvářený SQL a ujistit se, že se chová podle očekávání. V PostgreSQL se také často setkáváme s dlouho běžícími nečinnými dotazy. Konfigurace časových limitů, například hodnoty idle_in_transaction_session_timeout, je nezbytná k tomu, aby se zabránilo zablokování autovakua.
Výzva: Pokud dojde k výpadku repliky pro čtení, provoz lze stále směrovat na jiné repliky. Spoléhat se však na jediné writer instanci znamená mít jediný bod selhání. V případě selhání to ovlivní celou službu.
Řešení: Většina kritických požadavků zahrnuje pouze dotazy na čtení. Abychom omezili jediné místo selhání v primárním systému, přesunuli jsme tato čtení z writer instance na repliky a zajistili tak, že tyto požadavky mohou být vyřizovány i v případě, že primární systém vypadne. Zapisování by sice stále selhávalo, ale dopad je menší; již se nejedná o závažnosti 0, protože čtení zůstává k dispozici.
Pro zmírnění výpadků primárního systému provozujeme primární systém v režimu vysoké dostupnosti (HA) s pohotovostním záložním serverem, což je neustále synchronizovaná replika, která je vždy připravena převzít obsluhu provozu. Pokud primární systém vypadne nebo je potřeba ho kvůli údržbě odstavit, můžeme ho rychle nahradit záložním systémem, abychom minimalizovali dobu výpadku. Tým Azure PostgreSQL odvedl značnou práci na tom, aby zajistil, že tyto toto převzetí služeb při selhání bude bezpečné a spolehlivé i při velmi vysokém zatížení. Abychom zvládli selhání replik pro čtení, nasazujeme v každém regionu více replik s dostatečnou kapacitní rezervou. Tím zajistíme to, že selhání jedné repliky nepovede k výpadku celého regionu.
Výzva: Často se setkáváme se situacemi, kdy určité požadavky spotřebovávají na instancích PostgreSQL nepřiměřené množství prostředků. To může vést ke snížení výkonu u jiných úloh spuštěných na stejných instancích. Například spuštění nové funkce s sebou může přinést neefektivní dotazy, které výrazně zatěžují procesor PostgreSQL a zpomalují požadavky na jiné kritické funkce.
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 pro čtení má své vlastní nasazení Kubernetes s několika pody PgBouncer. Za stejnou službou Kubernetes Service provozujeme více nasazení Kubernetes, která vyrovnávají zátěž mezi 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(otevře se v novém okně), 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(otevře se v novém okně). 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(otevře se v novém okně) 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
Poděkování
Speciální poděkování patří Jonu Leemu, Sichengu Liuovi, Chaominu Yuovi a Chenglongu Haovi, kteří se podíleli na tomto příspěvku, a celému týmu, který pomohl škálovat PostgreSQL. Rádi bychom také poděkovali týmu Azure PostgreSQL za jejich účinnou spolupráci.


