PostgreSQL-skálázás 800 millió ChatGPT‑felhasználó kiszolgálására
Bohan Zhang, a műszaki csapat tagja
Évek óta a PostgreSQL az egyik legfontosabb, háttérben működő adatrendszer, amely olyan alapvető termékeket működtet, mint a ChatGPT és az OpenAI API. Felhasználói bázisunk gyors növekedésével az adatbázisainkkal szembeni igények is exponenciálisan megnőttek. Az elmúlt évben a PostgreSQL-terhelésünk több mint tízszeresére nőtt, és továbbra is gyorsan emelkedik.
Erőfeszítéseink a termelési infrastruktúránk fejlesztésére, hogy fenntartsuk ezt a növekedést, új felismeréshez vezettek: a PostgreSQL megbízhatóan skálázható, hogy sokkal nagyobb, olvasásintenzív munkaterheléseket támogasson, mint azt korábban sokan gondolták. A rendszer (amelyet kezdetben a Kaliforniai Egyetem, Berkeley tudósainak egy csapata hozott létre) lehetővé tette számunkra, hogy egyetlen elsődleges Azure PostgreSQL rugalmas kiszolgálópéldánnyal(új ablakban nyílik meg) és közel 50, világszerte több régióban elosztott olvasási replikával támogassuk a hatalmas globális forgalmat. Ez annak a története, hogyan skáláztuk a PostgreSQL-t az OpenAI-nál, hogy szigorú optimalizálásokkal és stabil mérnöki munkával másodpercenként több millió lekérdezést tudjunk kiszolgálni 800 millió felhasználó számára; emellett bemutatjuk az útközben levont legfontosabb tanulságokat is.
A ChatGPT elindítása után a forgalom példátlan mértékben növekedett. Ennek kiszolgálására gyorsan rengeteg optimalizálást vezettünk be mind az alkalmazási, mind a PostgreSQL-adatbázis szintjén, felskálázást végeztünk a példány méretének növelésével, és kifelé történő skálázást is megvalósítottunk a további olvasási replikák hozzáadásával. Ez az architektúra hosszú időn keresztül jól szolgált minket. A folyamatos fejlesztéseknek köszönhetően továbbra is bőven ad mozgásteret a jövőbeli növekedéshez.
Meglepően hangozhat, hogy egy egyetlen elsődlegesre épülő architektúra képes kiszolgálni az OpenAI méretét, de a gyakorlatban egyáltalán nem egyszerű ezt működőképessé tenni. Több súlyos üzemzavart (SEV-et) is láttunk már, amelyeket a Postgres túlterhelése okozott, és ezek gyakran ugyanazt a mintát követték: egy upstream hibába következtében hirtelen megugrott az adatbázis-terhelés. Ilyen volt például, amikor egy cache-réteg hibája miatt tömeges cache miss történt, amikor drága, többtáblás join műveletek árasztották el a CPU-t, vagy amikor egy új funkció indulása írási vihart okozott. Ahogy a erőforrás-kihasználtság nő, a lekérdezések késleltetése is megugrik, és a kérések időkorlátja lejár. Az újrapróbálkozások tovább növelik a terhelést, ami egy ördögi kört indít el, és akár a teljes ChatGPT- és API-szolgáltatások teljesítményének leromlásához vezethet.
Bár a PostgreSQL jól skálázódik az olvasásintenzív munkaterheléseinkhez, a nagy írási forgalom időszakaiban továbbra is kihívásokkal szembesülünk. Ez nagyrészt a PostgreSQL többverziós konkurenciakezelésének (MVCC) megvalósításából adódik, ami miatt írásintenzív terheléseknél kevésbé hatékony. Amikor például egy lekérdezés frissít egy rekordot, vagy akár csak egyetlen mezőt, a teljes sort másolja a rendszer egy új verzió létrehozásához. Nagy írási terhelés mellett ez jelentős írási amplifikációhoz vezet. Ez az olvasási amplifikációt is növeli, mivel a lekérdezéseknek a legújabb megtalálásához több rekordverziót (úgynevezett „dead tuple”-öket) kell átvizsgálniuk. Az MVCC további kihívásokat támaszt, mint például a táblák és indexek felduzzadása, a megnövekedett indexkarbantartási terhek, valamint az autovacuum bonyolult hangolása. (Ezekről a problémákról a Carnegie Mellon Egyetem professzorával, Andy Pavlóval részletesen írtam egy blogban, amelynek címe The Part of PostgreSQL We Hate the Most (A PostgreSQL leggyűlöltebb része)(új ablakban nyílik meg), amelyre a PostgreSQL Wikipédia-oldala is hivatkozik(új ablakban nyílik meg).)
Ezeknek a korlátoknak az enyhítésére és az írási nyomás csökkentésére a jól shardingolható (vagyis vízszintesen felosztható), írásintenzív terheléseket átmigráltuk – és folyamatosan migráljuk – sharded rendszerekbe, például az Azure Cosmos DB-be, miközben az alkalmazáslogikát is optimalizáljuk, hogy minél kevesebb felesleges írás történjen. A jelenlegi PostgreSQL-telepítéshez új táblák hozzáadását sem engedélyezzük már. Az új munkaterhelések alapértelmezés szerint a sharded rendszerekre kerülnek.
Még úgy is, hogy az infrastruktúránk sokat fejlődött, a PostgreSQL továbbra is sharding nélkül működik, és minden írást egyetlen elsődleges példány szolgál ki. Ennek az elsődleges oka az, hogy a meglévő alkalmazásterhelések shardingolása rendkívül összetett és időigényes lenne, több száz alkalmazásvégpont átalakítását igényelné, és akár hónapokig vagy évekig is eltarthatna. Mivel a terheléseink túlnyomórészt olvasásintenzívek, és rengeteg optimalizálást vezettünk be, a jelenlegi architektúra továbbra is bőséges tartalékot biztosít a forgalom további növekedésének kiszolgálásához. Bár a jövőben nem zárjuk ki a PostgreSQL shardingolását, a jelenlegi és várható növekedéshez rendelkezésre álló bőséges mozgástér miatt ez rövid távon nem élvez prioritást.
A következő fejezetekben részletesen bemutatjuk azokat a kihívásokat, amelyekkel szembesültünk, valamint a kiterjedt optimalizálásokat, amelyeket bevezettünk a problémák kezelésére és a jövőbeli leállások megelőzésére – egészen a PostgreSQL határainak feszegetéséig, másodpercenkénti több millió lekérdezésre (QPS) skálázva.
Kihívás: mivel csak egy író van, az egyetlen elsődlegesre épülő felállás nem képes az írások skálázására. A nagy írási terhelési csúcsok gyorsan túlterhelhetik az elsődlegest, és hatással lehetnek az olyan szolgáltatásokra, mint a ChatGPT és az API.
Megoldás: A lehető legnagyobb mértékben csökkentjük az elsődleges terhelését – mind olvasási, mind írási oldalon –, hogy mindig maradjon elég kapacitása az írási csúcsok kezelésére. Az olvasási forgalmat, ahol csak lehet, átterheljük a replikákra. Ugyanakkor néhány olvasási lekérdezésnek az elsődleges példányon kell maradnia, mert írási tranzakciók részét képezik. Ezeknél arra összpontosítunk, hogy hatékonyak legyenek, és elkerüljük a lassú lekérdezéseket. Az írási forgalomnál a jól shardingolható, írásintenzív terheléseket sharded rendszerekbe, például az Azure Cosmos DB-be migráltuk. Azok a terheléseket, amelyeket nehezebb shardingolni, de mégis nagy írási forgalmat generálnak, hosszabb ideig tart átköltöztetni, és ez a folyamat még most is zajlik. Az alkalmazásainkat is intenzíven optimalizáltuk az írási terhelés csökkentése érdekben: például kijavítottuk azokat a hibákat, amelyek felesleges írásokat okoztak, és ahol volt rá lehetőség, bevezettük a lazy write írásokat is, hogy elsimítsuk a forgalmi csúcsokat. Ezenkívül a táblamezők visszatöltésekor szigorú korlátokat alkalmazunk, hogy megelőzzük a túlzott írási terhelést.
Kihívás: Több költséges lekérdezést azonosítottunk a PostgreSQL-ben. Korábban az ezekben a lekérdezésekben jelentkező hirtelen forgalomnövekedések jelentős CPU-teljesítményt emésztettek fel, ezzel lelassítva mind a ChatGPT‑t, mind az API-kéréseket.
Megoldás: néhány drága lekérdezés – például azok, amelyek sok táblát joinolnak össze – jelentősen le tudják rontani a teljesítményt, sőt akár az egész szolgáltatást is le tudják állítani. Folyamatosan optimalizálnunk kell a PostgreSQL-lekérdezéseket, hogy hatékonyak legyenek, és elkerüljük a tipikus Online Transaction Processing (OLTP) tipikus rossz gyakorlatokat (anti-patterns). Egyszer például azonosítottunk egy rendkívüli erőforrásokat felemésztő lekérdezést, ami 12 táblát joinolt, és ennek a lekérdezésnek a kiugrásai korábban több igen súlyos SEV okozói voltak. Amikor csak lehet, kerüljük az összetett, több táblát érintő join műveleteket. Ha a join műveletek elkerülhetetlenek, megtanultuk, hogy érdemes lehet feldarabolni a lekérdezést, és a bonyolult join logikát inkább áthelyezni az alkalmazásrétegbe. Ezek közül a problémás lekérdezések közül sokat Object-Relational Mapping (ORM) keretrendszerek generálnak, ezért fontos alaposan átnézni az általuk előállított SQL-t, és megbizonyosodni róla, hogy valóban a kívánt módon viselkedik. A PostgreSQL-ben is gyakran találhatók hosszú ideig tétlenül futó lekérdezések. Az olyan időtúllépések konfigurálása, mint az idle_in_transaction_session_timeout, elengedhetetlen annak érdekében, hogy ne akadályozzák az autovacuum működését.
Kihívás: Ha egy olvasási replika leáll, a forgalom továbbra is átirányítható más replikákhoz. Ugyanakkor, ha egyetlen íróra támaszkodunk, az egyetlen hibapontot jelent – ha az kiesik, az érinti az egész szolgáltatást.
Megoldás: a legkritikusabb kérések többsége csak olvasási lekérdezéseket érint. Az elsődleges rendszer egyetlen hibapontjának elkerülése érdekében az olvasási műveleteket az íróról a replikákra helyeztük át, így biztosítva, hogy ezek a kérések akkor is kiszolgálhatók maradjanak, ha az elsődleges rendszer leáll. Bár az írási műveletek továbbra is sikertelenek lennének, a hatás csökkent; már nem számít SEV0-nak, mivel az olvasások továbbra is elérhetők.
Az elsődleges példány kiesésének kezelésére az elsődlegest magas rendelkezésre állású (HA) módban futtatjuk egy hot standby-jal, vagyis egy folyamatosan szinkronban tartott replikával, amely bármikor készen áll arra, hogy átvegye a forgalom kiszolgálását. Ha az elsődleges rendszer leáll vagy karbantartás miatt offline állapotba kerül, gyorsan feljebb léptethetjük a készenléti rendszert a leállási idő minimalizálása érdekében. Az Azure PostgreSQL csapata rengeteget dolgozott azon, hogy ezek a failoverek még nagyon nagy terhelés alatt is biztonságosan és megbízhatóan menjenek végbe. Az olvasási replika kiesések kezelésére régiónként több replikát telepítünk, elegendő kapacitástartalékkal, így egyetlen replika meghibásodása nem vezet régiós leálláshoz.
Kihívás: gyakran találkozunk olyan helyzetekkel, amikor bizonyos kérések aránytalanul sok erőforrást emésztenek fel a PostgreSQL-példányokon. Ez romló teljesítményhez vezethet más, ugyanazon példányokon futó munkaterhelések esetén. Például egy új funkció bevezetése hozhat magával nem hatékony lekérdezéseket, amelyek erősen terhelik a PostgreSQL CPU-ját, és lelassítják más, kritikus funkciók kéréseit.
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.
Minden olvasási replika saját Kubernetes-telepítéssel rendelkezik, amely több PgBouncer podot futtat. Több Kubernetes-telepítést futtatunk ugyanazon Kubernetes szolgáltatás mögött, amely a podok között terheléselosztással osztja el a forgalmat.
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(új ablakban nyílik meg), 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(új ablakban nyílik meg). 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(új ablakban nyílik meg) 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.
Szerző
Köszönetnyilvánítás
Külön köszönetet érdemel a bejegyzés elkészítésében résztvevő Jon Lee, Sicheng Liu, Chaomin Yu és Chenglong Hao, valamint a PostgreSQL felskálázásához segítséget nyújtó teljes csapat. Az Azure PostgreSQL csapatának is szeretnénk köszönetet mondani a jelentős együttműködésért.


