Obsługa 800 mln użytkowników ChatGPT dzięki skalowaniu PostgreSQL
Bohan Zhang, członek personelu technicznego
Od lat PostgreSQL jest jednym z najważniejszych, niewidocznych dla użytkownika systemów danych, które obsługują podstawowe produkty, takie jak ChatGPT i platforma API OpenAI. Wraz z szybkim wzrostem liczby użytkowników nasze wymagania dotyczące naszych baz danych również wzrosły wykładniczo. W ciągu ostatniego roku nasze obciążenie PostgreSQL wzrosło ponad dziesięciokrotnie i nadal rośnie w szybkim tempie.
Nasze działania mające na celu rozwój infrastruktury produkcyjnej pozwalającej utrzymać ten wzrost przyniosły nowe spostrzeżenie: PostgreSQL można skalować tak, aby zapewnić niezawodną obsługę znacznie większych obciążeń związanych z odczytem danych, niż wcześniej uważano za możliwe. System (stworzony początkowo przez zespół naukowców z Uniwersytetu Kalifornijskiego w Berkeley) umożliwił nam obsługę ogromnego globalnego ruchu za pomocą jednej podstawowej elastycznej instancji serwera Azure PostgreSQL(otwiera nowe okno) i prawie 50 replik odczytu rozmieszczonych w wielu regionach na całym świecie. Poniżej przedstawiamy historię skalowania PostgreSQL w OpenAI w celu obsługi milionów zapytań na sekundę dla 800 milionów użytkowników poprzez rygorystyczne optymalizacje i zaawansowaną inżynierię. Omówimy również kluczowe wnioski, które wyciągnęliśmy podczas tego procesu.
Po uruchomieniu ChatGPT ruch wzrósł w niespotykanym dotąd tempie. Aby to umożliwić, szybko wdrożyliśmy szeroko zakrojone optymalizacje zarówno na poziomie aplikacji, jak i bazy danych PostgreSQL, zwiększyliśmy skalę poprzez powiększenie rozmiaru instancji oraz rozszerzyliśmy skalę poprzez dodanie większej liczby replik odczytowych. Ta architektura dobrze nam służyła przez długi czas. Dzięki ciągłym ulepszeniom nadal zapewnia duży potencjał dla przyszłego rozwoju.
Może wydawać się zaskakujące, że architektura oparta na jednym procesorze może sprostać wymaganiom skali OpenAI, jednak w praktyce nie jest to proste. Mieliśmy do czynienia z kilkoma poważnymi incydentami spowodowanymi przeciążeniem systemu Postgres, które często przebiegały według tego samego schematu: problem na wyższym poziomie spowodował nagły wzrost obciążenia bazy danych, np. rozległe braki pamięci podręcznej spowodowane awarią warstwy buforującej, gwałtowny wzrost kosztownych połączeń wielokierunkowych obciążających procesor lub masowy napływ operacji zapisu spowodowany uruchomieniem nowej funkcji. Wraz ze wzrostem wykorzystania zasobów wzrasta opóźnienie zapytań, a żądania zaczynają przekraczać limit czasu. Ponowne próby jeszcze bardziej zwiększają obciążenie, wywołując błędne koło, które może spowodować pogorszenie działania wszystkich usług ChatGPT i API.
Chociaż system PostgreSQL dobrze skaluje się w przypadku naszych obciążeń wymagających intensywnego odczytu, nadal napotykamy wyzwania w okresach dużego ruchu zapisu. Wynika to głównie z implementacji wielowersyjnej kontroli współbieżności (MVCC) w systemie PostgreSQL, która sprawia, że jest on mniej wydajny w przypadku obciążeń wymagających intensywnego zapisu. Na przykład: gdy zapytanie aktualizuje krotkę lub nawet pojedyncze pole, cały wiersz jest kopiowany w celu utworzenia nowej wersji. Przy dużym obciążeniu zapisem powoduje to znaczną amplifikację zapisu. Powoduje to również amplifikację odczytu, ponieważ zapytania muszą przeszukiwać wiele wersji krotki (martwe krotki), aby pobrać najnowszą wersję. MVCC stwarza dodatkowe wyzwania, takie jak nadmierny rozrost tabel i indeksów, zwiększone obciążenie związane z utrzymaniem indeksów oraz skomplikowane dostosowywanie autovacuum. (Szczegółowe omówienie tych kwestii można znaleźć w blogu, który napisałem wraz z prof. Andym Pavlo z Carnegie Mellon University, zatytułowanym „The Part of PostgreSQL We Hate the Most”(otwiera nowe okno) (Najbardziej znienawidzona część PostgreSQL), cytowanym(otwiera nowe okno) na stronie PostgreSQL w Wikipedii).
Aby zminimalizować te ograniczenia i zmniejszyć obciążenie związane z zapisem, dokonaliśmy przeniesienia obciążeń oraz kontynuujemy przenoszenie tych obciążeń, które można podzielić na fragmenty (tj. obciążeń, które można podzielić poziomo), oraz obciążeń wymagających intensywnego zapisu do systemów podzielonych na fragmenty, takich jak Azure Cosmos DB, optymalizując logikę aplikacji w celu minimalizacji zbędnego zapisu. Nie zezwalamy również na dodawanie nowych tabel do bieżącej instalacji PostgreSQL. Nowe obciążenia domyślnie trafiają do systemów pofragmentowanych.
Mimo ewolucji naszej infrastruktury system PostgreSQL pozostał niepofragmentowany – jedna główna instancja obsługuje wszystkie zapisy. Głównym powodem takiego stanu rzeczy jest fakt, że fragmentacja istniejących obciążeń aplikacji byłaby bardzo złożona i czasochłonna, a także wymagałaby zmian w setkach punktów końcowych aplikacji i mogłaby potrwać miesiące, a nawet lata. Z uwagi na to, że nasze obciążenia są głównie związane z odczytem danych, a także nasze szeroko zakrojone optymalizacje, obecna architektura nadal zapewnia wystarczającą rezerwę mocy obliczeniowej, aby sprostać rosnącemu natężeniu ruchu. Chociaż nie wykluczamy późniejszej fragmentacji PostgreSQL w, nie jest to priorytetem w najbliższym czasie, biorąc pod uwagę wystarczające możliwości rozwoju, teraz i w przyszłości.
W kolejnych sekcjach omówimy napotkane wyzwania oraz szeroko zakrojone optymalizacje, które wdrożyliśmy, aby im sprostać i zapobiec przyszłym awariom, wykorzystując w pełni możliwości systemu PostgreSQL i skalując go do milionów zapytań na sekundę (QPS).
Wyzwanie: przy tylko jednym module zapisu konfiguracja z jednym serwerem głównym nie pozwala na skalowanie operacji zapisu. Duże skoki zapisu mogą szybko przeciążyć serwer główny i wpłynąć na działanie usług takich jak ChatGPT i nasza platforma API.
Rozwiązanie: minimalizujemy obciążenie serwera głównego w jak największym stopniu – zarówno odczyty, jak i zapisy – aby zapewnić mu wystarczającą wydajność do obsługi skoków zapisu. Ruch odczytu jest w miarę możliwości przenoszony do replik. Jednak niektóre zapytania odczytu muszą pozostać na serwerze głównym, ponieważ są częścią transakcji zapisu. W ich przypadku skupiamy się na zapewnieniu wydajności i unikaniu powolnych zapytań. W przypadku ruchu zapisu przenieśliśmy obciążenia umożliwiające fragmentację, które wymagają intensywnego zapisu, do systemów pofragmentowanych, takich jak Azure CosmosDB. Obciążenia, które są trudniejsze do fragmentacji, ale nadal generują dużą ilość zapisów, wymagają więcej czasu na migrację, a proces ten jest nadal w toku. Ponadto przeprowadziliśmy intensywną optymalizację naszych aplikacji w celu zmniejszenia obciążenia zapisem. Na przykład naprawiliśmy błędy aplikacji, które powodowały zbędne zapisy, oraz wprowadziliśmy zapis opóźniony tam, gdzie było to stosowne, aby wygładzić skoki ruchu. Dodatkowo stosujemy ścisłe limity szybkości podczas uzupełniania pól tabeli, aby zapobiec nadmiernemu obciążeniu zapisem.
Wyzwanie: wykryliśmy kilka kosztownych zapytań w PostgreSQL. W przeszłości nagłe skoki liczby tych zapytań powodowały duże obciążenie procesora, spowalniając zarówno działanie ChatGPT, jak i żądań API.
Rozwiązanie: kilka kosztownych zapytań, takich jak te łączące wiele tabel, może znacznie pogorszyć działanie lub nawet spowodować awarię całej usługi. Musimy stale optymalizować zapytania PostgreSQL, aby zapewnić ich wydajność i uniknąć typowych antywzorców przetwarzania transakcji online (OLTP). Na przykład kiedyś zidentyfikowaliśmy niezwykle kosztowne zapytanie, które łączyło 12 tabel, a skoki obciążeń w tym zapytaniu w przeszłości powodowały błędy o wysokiej krytyczności. W miarę możliwości powinniśmy unikać złożonych połączeń wielu tabel. Jeśli połączenia są konieczne, nauczyliśmy się brać pod uwagę rozbicie zapytania i przeniesienie złożonej logiki połączeń do warstwy aplikacji. Wiele z tych problematycznych zapytań jest generowanych przez struktury mapowania obiektowo-relacyjnego (ORM), dlatego ważne jest, aby dokładnie sprawdzić generowane przez nie zapytania SQL i upewnić się, że działają one zgodnie z oczekiwaniami. W PostgreSQL często spotyka się również zapytania, które przez długi czas pozostają bezczynne. Konfiguracja limitów czasu, takich jak idle_in_transaction_session_timeout, jest niezbędna, aby zapobiec blokowaniu autovacuum.
Wyzwanie: jeśli replika odczytu ulegnie awarii, ruch nadal może być kierowany do innych replik. Jednak poleganie na jednym module zapisu powoduje powstanie pojedynczego punktu awarii, którego awaria wpłynie na całą usługę.
Rozwiązanie: najbardziej krytyczne żądania dotyczą wyłącznie zapytań odczytu. Aby złagodzić skutki pojedynczego punktu awarii w serwerze głównym, przenieśliśmy odczyty z modułu zapisu do replik, co zapewnia ciągłość obsługi tych żądań nawet w przypadku awarii serwera głównego. Mimo iż operacje zapisu nadal będą się kończyć niepowodzeniem, ich wpływ zostanie ograniczony – nie będzie to już błąd SEV0, ponieważ operacje odczytu będą nadal dostępne.
Aby ograniczyć ryzyko awarii serwera głównego, uruchamiamy go w trybie wysokiej dostępności (HA) z aktywnym trybem gotowości, czyli stale synchronizowaną repliką, która jest zawsze gotowa do przejęcia ruchu. Jeśli serwer główny ulegnie awarii lub będzie wymagał wyłączenia w celu przeprowadzenia konserwacji, możemy szybko uruchomić serwer rezerwowy, aby zminimalizować czas przestoju. Zespół Azure PostgreSQL wykonał znaczną pracę, aby zapewnić bezpieczeństwo i niezawodność tych przełączeń awaryjnych nawet przy bardzo dużym obciążeniu. Aby rozwiązać problem awarii replik odczytu, wdrażamy wiele replik w każdym regionie z wystarczającym zapasem mocy obliczeniowej, dzięki czemu awaria jednej repliki nie powoduje przerwy w działaniu całego regionu.
Wyzwanie: często spotykamy się z sytuacjami, w których niektóre żądania zużywają nieproporcjonalnie dużą ilość zasobów na instancjach PostgreSQL. Może to prowadzić do pogorszenia wydajności innych obciążeń uruchomionych na tych samych instancjach. Na przykład wprowadzenie nowej funkcji może spowodować pojawienie się nieefektywnych zapytań, które zużywają znaczną część mocy obliczeniowej procesora PostgreSQL, spowalniając żądania dotyczące innych kluczowych funkcji.
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żda replika odczytu ma własne wdrożenie Kubernetes, w którym działa wiele podów PgBouncer. Obsługujemy wiele wdrożeń Kubernetes w ramach tej samej usługi Kubernetes, która równoważy obciążenie spowodowane ruchem między podami.
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(otwiera nowe okno), 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(otwiera nowe okno). 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(otwiera nowe okno) 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
Podziękowania
W szczególności dziękujemy Jonowi Lee, Sichengowi Liu, Chaominowi Yu i Chenglongowi Hao, którzy przyczynili się do powstania tego wpisu, a także całemu zespołowi, który przyczynił się do rozbudowy PostgreSQL. Chcielibyśmy również podziękować zespołowi Azure PostgreSQL za ścisłą współpracę.


