Масштабирование PostgreSQL для 800 млн пользователей ChatGPT
Бохан Чжан, технический персонал
На протяжении многих лет PostgreSQL была одной из самых критически важных внутренних систем данных, поддерживающих основные продукты, такие как ChatGPT и API OpenAI. По мере стремительного роста нашей базы пользователей нагрузка на наши базы данных также увеличивалась в геометрической прогрессии. За последний год наша нагрузка на PostgreSQL увеличилась более чем в 10 раз и продолжает быстро расти.
Наши усилия по развитию производственной инфраструктуры для поддержания этого роста привели к новому выводу: PostgreSQL можно масштабировать для надёжной поддержки значительно более крупных нагрузок с преобладанием операций чтения, чем многие ранее считали возможным. Система (изначально созданная командой ученых из University of California, Беркли) позволила нам поддерживать огромный глобальный трафик с помощью одного основного экземпляра гибкого сервера Azure PostgreSQL(открывается в новом окне) и почти 50 реплик для чтения, распределенных по нескольким регионам по всему миру. Это рассказ о том, как мы масштабировали PostgreSQL в OpenAI, чтобы поддерживать миллионы запросов в секунду для 800 миллионов пользователей благодаря строгим оптимизациям и надёжной инженерии; мы также поделимся ключевыми выводами, которые извлекли на этом пути.
После запуска ChatGPT трафик рос с беспрецедентной скоростью. Чтобы поддержать это, мы быстро внедрили обширные оптимизации как на уровне приложения, так и на уровне базы данных PostgreSQL, увеличили масштаб, увеличив размер экземпляра, и расширили масштаб, добавив больше реплик для чтения. Эта архитектура хорошо служила нам долгое время. Благодаря постоянным улучшениям, она продолжает предоставлять достаточный потенциал для будущего роста.
Может показаться удивительным, что архитектура с одним основным узлом может удовлетворить требованиям масштаба OpenAI; однако на практике реализовать это непросто. Мы наблюдали несколько SEV, вызванных перегрузкой Postgres, и они часто следуют одной и той же схеме: проблема на более высоком уровне вызывает внезапный всплеск нагрузки на базу данных, например, массовые промахи кэша из-за сбоя кэширующего слоя, всплеск затратных многопутевых соединений, нагружающих процессор, или множество записей из-за запуска новой функции. По мере увеличения использования ресурсов задержка запросов возрастает, и запросы начинают завершаться по тайм-ауту. Повторные попытки затем еще больше увеличивают нагрузку, запуская порочный круг, который может привести к падению всех сервисов ChatGPT и API.
Хотя PostgreSQL хорошо масштабируется для наших нагрузок с преобладанием чтения, мы всё же сталкиваемся с трудностями в периоды высокой нагрузки на запись. Во многом это связано с реализацией multiversion concurrency control (MVCC) в PostgreSQL, из-за чего он менее эффективен для рабочих нагрузок с преобладанием операций записи. Например, когда запрос обновляет кортеж или даже одно поле, вся строка копируется для создания новой версии. При высоких нагрузках на запись это приводит к значительному усилению записи. Это также увеличивает усиление чтения, так как запросы должны просканировать несколько версий кортежей (устаревшие кортежи), чтобы получить последнюю. MVCC создает дополнительные сложности, такие как разрастание таблиц и индексов, увеличение накладных расходов на обслуживание индексов и сложная настройка autovacuum. (Подробный разбор этих проблем можно найти в статье в блоге, которую я написал вместе с проф. Энди Павло из Университета Карнеги-Меллона, под названием The Part of PostgreSQL We Hate the Most(открывается в новом окне); статья цитируется(открывается в новом окне) на странице PostgreSQL в Википедии.)
Чтобы смягчить эти ограничения и снизить нагрузку на запись, мы мигрировали и продолжаем мигрировать шардируемые (т. е. нагрузки, которые можно горизонтально разделить) рабочие нагрузки с преобладанием операций записи, в сегментированные системы, такие как Azure Cosmos DB, оптимизируя логику приложения, чтобы минимизировать ненужные записи. Мы также больше не разрешаем добавлять новые таблицы в текущее развертывание PostgreSQL. Новые рабочие нагрузки по умолчанию распределяются на шардированные системы.
Даже по мере развития нашей инфраструктуры PostgreSQL оставался без шардирования, и все операции записи обслуживались одним основным экземпляром. Основная причина заключается в том, что шардирование существующих рабочих нагрузок приложений будет чрезвычайно сложным и трудоёмким процессом, требующим изменений в сотнях конечных точек приложений и потенциально занимающим месяцы или даже годы. Поскольку наши рабочие нагрузки в основном ориентированы на чтение и мы внедрили обширные оптимизации, текущая архитектура по-прежнему предоставляет достаточный запас для поддержки дальнейшего роста трафика. Хотя мы не исключаем шардирование PostgreSQL в будущем, это не является приоритетом в ближайшее время, учитывая достаточный запас прочности для текущего и будущего роста.
В следующих разделах мы углубимся в проблемы, с которыми мы столкнулись, и обширные оптимизации, которые мы внедрили, чтобы их устранить и предотвратить будущие сбои, доведя PostgreSQL до предела и масштабировав его до миллионов запросов в секунду (QPS).
Проблема: при наличии только одного узла записи, конфигурация с одним основным узлом не может масштабировать операции записи. Резкие всплески записи могут быстро перегрузить основной сервер и повлиять на такие сервисы, как ChatGPT и наши API.
Решение: Мы минимизируем нагрузку на основной сервер — как на чтение, так и на запись, — чтобы обеспечить его достаточной мощностью для обработки пиковых нагрузок записи. Трафик чтения разгружается на реплики, где это возможно. Однако некоторые запросы на чтение должны оставаться на основном сервере, поскольку они являются частью транзакций записи. Для них мы сосредоточены на эффективности и избегаем медленных запросов. Для трафика записи мы перенесли рабочие нагрузки с высокой интенсивностью записи, которые можно разбить на шардированные системы, такие как Azure CosmosDB. Рабочие нагрузки, которые сложнее шардировать, но которые все равно генерируют большой объем операций записи, мигрируют дольше, и этот процесс все еще продолжается. Мы также активно оптимизировали наши приложения, чтобы уменьшить нагрузку на запись; например, мы устранили ошибки в приложениях, которые вызывали избыточные записи, и внедрили отложенные записи там, где это уместно, чтобы сгладить всплески трафика. Кроме того, при заполнении полей таблицы мы применяем строгие ограничения скорости, чтобы предотвратить чрезмерную нагрузку на запись.
Задача: Мы обнаружили несколько затратных запросов в PostgreSQL. В прошлом внезапные всплески объема в этих запросах потребляли большие объемы ресурсов ЦП, замедляя как ChatGPT, так и запросы API.
Решение: Несколько затратных запросов, таких как те, которые объединяют множество таблиц, могут значительно ухудшить работу или даже вывести из строя весь сервис. Нам необходимо постоянно оптимизировать запросы PostgreSQL, чтобы гарантировать их эффективность и избегать распространённых антипаттернов в Online Transaction Processing (OLTP). Например, однажды мы выявили чрезвычайно затратный запрос, который объединял 12 таблиц, и всплески этого запроса были причиной прошлых инцидентов высокой степени серьезности. Следует по возможности избегать сложных многотабличных соединений. Если соединения необходимы, мы научились рассматривать возможность разбить запрос и перенести сложную логику соединений на уровень приложения. Многие из этих проблемных запросов создаются фреймворками объектно-реляционного отображения (ORM), поэтому важно тщательно проверять генерируемый ими SQL и удостовериться, что он работает как ожидается. Также часто можно встретить длительные простаивающие запросы в PostgreSQL. Настройка тайм-аутов, таких как idle_in_transaction_session_timeout, необходима для предотвращения блокировки autovacuum.
Задача: Если реплика для чтения выходит из строя, трафик всё равно может быть перенаправлен на другие реплики. Однако зависимость от одного-единственного автора означает наличие единой точки отказа — если она выйдет из строя, пострадает весь сервис.
Решение: Большинство критически важных запросов включают только запросы на чтение. Чтобы уменьшить риск единой точки отказа в основной системе, мы перенесли операции чтения с основной системы на реплики, обеспечив, что эти запросы смогут продолжать обслуживаться, даже если основная система выйдет из строя. Хотя операции записи по-прежнему будут завершаться сбоем, влияние снижено; это больше не SEV0, поскольку операции чтения остаются доступными.
Чтобы уменьшить вероятность отказов основного узла, мы запускаем его в режиме высокой доступности (HA) с горячим резервом — постоянно синхронизируемой репликой, которая всегда готова взять на себя обслуживание трафика. Если основной узел выйдет из строя или его нужно будет отключить для обслуживания, мы можем быстро повысить резервный, чтобы свести время простоя к минимуму. Команда Azure PostgreSQL проделала значительную работу, чтобы обеспечить безопасность и надёжность этих переключений даже при очень высокой нагрузке. Чтобы справляться со сбоями реплик для чтения, мы развертываем несколько реплик в каждом регионе с достаточным запасом по мощности, чтобы сбой одной реплики не приводил к региональному отключению.
Проблема: Мы часто сталкиваемся с ситуациями, когда определённые запросы потребляют несоразмерно много ресурсов на экземплярах PostgreSQL. Это может привести к снижению производительности других рабочих нагрузок, выполняющихся на тех же экземплярах. Например, запуск новой функции может привести к появлению неэффективных запросов, которые сильно нагружают процессор PostgreSQL, замедляя выполнение запросов для других критически важных функций.
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.
У каждой реплики для чтения есть собственное развертывание Kubernetes, в котором запущено несколько подов PgBouncer. Мы запускаем несколько развертываний Kubernetes за одним и тем же сервисом Kubernetes, который распределяет нагрузку трафика между подами.
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(открывается в новом окне), 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(открывается в новом окне). 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(открывается в новом окне) 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.
Автор
Благодарности
Особая благодарность Джону Ли, Сичэну Лю, Чаомин Ю и Чэнлун Хао, которые внесли вклад в этот пост, а также всей команде, которая помогла масштабировать PostgreSQL. Мы также хотели бы поблагодарить команду Azure PostgreSQL за надежное сотрудничество.


