Переведено с помощью Struxel
Автор: Бохан Чжан, технический специалист
На протяжении многих лет PostgreSQL была одной из важнейших, невидимых для пользователя систем обработки данных, обеспечивающих работу основных продуктов, таких как ChatGPT и API OpenAI. По мере того как наша пользовательская база быстро растет, требования к нашим базам данных также экспоненциально возрастают. За последний год нагрузка на нашу систему PostgreSQL увеличилась более чем в 10 раз, и она продолжает быстро расти.
Наши усилия по совершенствованию нашей производственной инфраструктуры для обеспечения этого роста выявили новое понимание: PostgreSQL можно масштабировать для надежной поддержки гораздо больших рабочих нагрузок, ориентированных на чтение, чем многие считали возможным ранее. Система (первоначально созданная командой ученых из Калифорнийского университета в Беркли) позволила нам поддерживать огромный глобальный трафик с помощью одного основного сервера Azure PostgreSQL и почти 50 реплик для чтения, распределенных по нескольким регионам мира. Это история о том, как мы масштабировали PostgreSQL в OpenAI для поддержки миллионов запросов в секунду для 800 миллионов пользователей посредством строгих оптимизаций и надежной разработки; мы также рассмотрим основные выводы, которые мы получили в процессе.
Проблемы в нашей первоначальной архитектуре
После запуска ChatGPT трафик рос беспрецедентными темпами. Чтобы поддержать его, мы быстро внедрили обширные оптимизации как на уровне приложений, так и на уровне базы данных PostgreSQL, увеличили размер экземпляра и расширили его, добавив больше реплик для чтения. Эта архитектура хорошо служила нам в течение долгого времени. Благодаря постоянным улучшениям она продолжает обеспечивать достаточный запас прочности для будущего роста.
Возможно, удивительно, что архитектура с одной главной нодой может удовлетворить потребности OpenAI в масштабе; однако, чтобы это работало на практике, это не так просто. Мы столкнулись с несколькими серьезными сбоями, вызванными перегрузкой Postgres, и они часто следуют одной и той же схеме: проблема на более высоком уровне вызывает внезапный скачок нагрузки на базу данных, например, широко распространенные промахи кэша из-за сбоя уровня кэширования, всплеск дорогостоящих многосторонних соединений, перегружающих ЦП, или шквал записей из-за запуска новой функции. По мере увеличения использования ресурсов время отклика запросов увеличивается, и запросы начинают истекать. Повторные попытки затем еще больше усиливают нагрузку, вызывая порочный круг, который может привести к ухудшению работы всей системы ChatGPT и API.
Хотя PostgreSQL хорошо масштабируется для наших рабочих нагрузок, ориентированных на чтение, мы все еще сталкиваемся с проблемами в периоды высокой нагрузки на запись. Это в основном связано с реализацией многоверсионного управления параллелизмом (MVCC) в PostgreSQL, которая делает ее менее эффективной для рабочих нагрузок, ориентированных на запись. Например, когда запрос обновляет кортеж или даже одно поле, вся строка копируется для создания новой версии. При высокой нагрузке на запись это приводит к значительному увеличению объема записи. Это также увеличивает объем чтения, поскольку запросам необходимо просматривать несколько версий кортежей (мертвых кортежей), чтобы получить последнюю. MVCC создает дополнительные проблемы, такие как разрастание таблиц и индексов, увеличение накладных расходов на обслуживание индексов и сложная настройка автоматической очистки. (Вы можете найти подробное описание этих проблем в блоге, который я написал вместе с профессором Энди Павло из Университета Карнеги-Меллона под названием Часть PostgreSQL, которая нам больше всего не нравится)
Масштабирование PostgreSQL до миллионов QPS
Чтобы смягчить эти ограничения и снизить нагрузку на запись, мы перенесли и продолжаем переносить рабочие нагрузки, которые можно разделить (т. е. рабочие нагрузки, которые можно горизонтально разделить), в системы с разделением на разделы, такие как Azure Cosmos DB, оптимизируя логику приложений, чтобы свести к минимуму ненужные записи. Мы также больше не разрешаем добавлять новые таблицы в текущую установку PostgreSQL. Новые рабочие нагрузки по умолчанию используют системы с разделением на шарды.
Даже по мере развития нашей инфраструктуры PostgreSQL остается неразделенной, при этом один основной экземпляр обслуживает все записи. Основная причина заключается в том, что разделение существующих рабочих нагрузок приложений было бы очень сложным и трудоемким, требующим изменений в сотнях конечных точек приложений и, возможно, занимало бы месяцы или даже годы. Поскольку наши рабочие нагрузки в основном ориентированы на чтение, и мы внедрили обширные оптимизации, текущая архитектура по-прежнему обеспечивает достаточный запас прочности для поддержки дальнейшего роста трафика. Хотя мы не исключаем возможность разделения PostgreSQL в будущем, это не является приоритетной задачей в ближайшее время, учитывая достаточный запас прочности, которым мы располагаем для текущего и будущего роста.
В следующих разделах мы рассмотрим проблемы, с которыми мы столкнулись, и обширные оптимизации, которые мы внедрили для их решения и предотвращения будущих сбоев, доводя PostgreSQL до предела и масштабируя ее до миллионов запросов в секунду (QPS).
Снижение нагрузки на основной сервер
Проблема: при наличии только одного записывающего сервера одноосновная настройка не может масштабировать записи. Сильные всплески записи могут быстро перегрузить основной сервер и повлиять на такие сервисы, как ChatGPT и наш API.
Решение: мы максимально снижаем нагрузку на основной сервер — как чтение, так и запись — чтобы обеспечить достаточную мощность для обработки всплесков записи. Трафик чтения перенаправляется на реплики, где это возможно. Однако некоторые запросы на чтение должны оставаться на основном сервере, потому что они являются частью транзакций записи. Для них мы сосредотачиваемся на обеспечении их эффективности и избегаем медленных запросов. Для трафика записи мы перенесли рабочие нагрузки, которые можно разделить и которые интенсивно записывают данные, в системы с разделением на разделы, такие как Azure CosmosDB. Рабочие нагрузки, которые сложнее разделить, но которые все еще генерируют большой объем записи, требуют больше времени для переноса, и этот процесс все еще продолжается. Мы также активно оптимизировали наши приложения, чтобы уменьшить нагрузку на запись; например, мы исправили ошибки приложений, которые вызывали избыточные записи, и, где это уместно, внедрили отложенные записи, чтобы сгладить всплески трафика. Кроме того, при массовой загрузке данных в поля таблиц мы применяем строгие ограничения скорости, чтобы предотвратить чрезмерную нагрузку на запись.
Оптимизация запросов
Проблема: мы выявили несколько дорогостоящих запросов в PostgreSQL. В прошлом внезапные всплески объема этих запросов потребляли большое количество ЦП, замедляя запросы ChatGPT и API.
Решение: Несколько ресурсоемких запросов, таких как запросы, объединяющие множество таблиц, могут значительно ухудшить работу или даже привести к сбою всей службы. Нам необходимо постоянно оптимизировать запросы PostgreSQL, чтобы обеспечить их эффективность и избежать распространенных антипаттернов обработки транзакций (OLTP). Например, мы однажды выявили чрезвычайно дорогостоящий запрос, который объединял 12 таблиц, и всплески этого запроса были причиной серьезных инцидентов в прошлом. Мы должны избегать сложных многотабличных соединений, когда это возможно. Если соединения необходимы, мы научились рассматривать возможность разбиения запроса и переноса сложной логики соединения на уровень приложения. Многие из этих проблемных запросов генерируются объектно-реляционными отображающими (ORM) фреймворками, поэтому важно тщательно проверять создаваемый ими SQL-код и убеждаться, что он работает должным образом. Также часто можно обнаружить длительные неактивные запросы в PostgreSQL. Настройка тайм-аутов, таких как idle_in_transaction_session_timeout, имеет важное значение для предотвращения их блокировки автоочистки.
Снижение риска единой точки отказа
Проблема: Если резервная копия для чтения выходит из строя, трафик все равно можно перенаправлять на другие реплики. Однако зависимость от единственного сервера записи означает наличие единой точки отказа — если он выходит из строя, это влияет на всю службу.
Решение: Большинство критически важных запросов включают только запросы на чтение. Чтобы снизить риск единой точки отказа в основном сервере, мы перенесли эти запросы на чтение с основного сервера на реплики, гарантируя, что эти запросы могут продолжать обслуживаться, даже если основной сервер выходит из строя. Хотя операции записи по-прежнему будут завершаться с ошибкой, влияние уменьшается; это больше не является SEV0, поскольку запросы на чтение остаются доступными.
Для снижения риска сбоев основного сервера мы запускаем основной сервер в режиме высокой доступности (HA) с горячей резервной копией, репликой, которая постоянно синхронизируется и всегда готова к обработке трафика. Если основной сервер выходит из строя или его необходимо отключить для обслуживания, мы можем быстро активировать резервную копию, чтобы свести к минимуму время простоя. Команда Azure PostgreSQL проделала значительную работу, чтобы обеспечить безопасность и надежность этих переключений даже при очень высокой нагрузке. Для обработки сбоев реплик для чтения мы развертываем несколько реплик в каждом регионе с достаточным запасом пропускной способности, гарантируя, что сбой одной реплики не приведет к региональному сбою.
Изоляция рабочей нагрузки
Проблема: Мы часто сталкиваемся с ситуациями, когда определенные запросы потребляют непропорционально большое количество ресурсов на экземплярах PostgreSQL. Это может привести к снижению производительности других рабочих нагрузок, работающих на тех же экземплярах. Например, запуск новой функции может привести к неэффективным запросам, которые сильно нагружают ЦП PostgreSQL, замедляя запросы для других критически важных функций.
Решение: Чтобы снизить проблему «шумного соседа», мы изолируем рабочие нагрузки на выделенных экземплярах, чтобы гарантировать, что внезапные всплески ресурсоемких запросов не повлияют на другой трафик. В частности, мы разделяем запросы на низкоприоритетные и высокоприоритетные уровни и направляем их на отдельные экземпляры. Таким образом, даже если низкоприоритетная рабочая нагрузка станет ресурсоемкой, это не повлияет на производительность высокоприоритетных запросов. Мы применяем ту же стратегию для различных продуктов и служб, чтобы активность одного продукта не влияла на производительность или надежность другого.
Пулинг соединений
Проблема: Каждый экземпляр имеет максимальное количество соединений (5000 в Azure PostgreSQL). Легко исчерпать количество соединений или накопить слишком много неактивных. У нас уже были инциденты, вызванные шквалами соединений, которые исчерпали все доступные соединения.
Решение: Мы развернули PgBouncer в качестве прокси-уровня для пулинга баз данных. Запуск в режиме пулинга операторов или транзакций позволяет нам эффективно повторно использовать соединения, значительно сокращая количество активных клиентских соединений. Это также сокращает задержку установки соединения: в наших тестах среднее время соединения сократилось с 50 миллисекунд (мс) до 5 мс. Межрегиональные соединения и запросы могут быть дорогостоящими, поэтому мы размещаем прокси, клиенты и реплики в одном регионе, чтобы минимизировать сетевые накладные расходы и время использования соединения. Кроме того, PgBouncer необходимо тщательно настроить. Такие параметры, как тайм-ауты простоя, имеют решающее значение для предотвращения исчерпания соединений.
Каждая реплика для чтения имеет собственное развертывание Kubernetes, на котором работает несколько подов PgBouncer. Мы запускаем несколько развертываний Kubernetes за одной и той же службой Kubernetes, которая балансирует трафик между подами.
Кэширование
Проблема: Внезапный всплеск промахов кэша может вызвать всплеск запросов на чтение к базе данных PostgreSQL, насыщая ЦП и замедляя запросы пользователей.
Решение: Чтобы снизить нагрузку на чтение в PostgreSQL, мы используем уровень кэширования для обслуживания большей части трафика чтения. Однако, когда частота попаданий в кэш неожиданно падает, всплеск промахов кэша может привести к большому количеству запросов непосредственно в PostgreSQL. Это внезапное увеличение количества запросов к базе данных потребляет значительные ресурсы, замедляя работу службы. Чтобы предотвратить перегрузку во время шквалов промахов кэша, мы реализуем механизм блокировки (и аренды) кэша, чтобы только один читатель, который не попал в кэш для определенного ключа, извлекал данные из PostgreSQL. Когда несколько запросов не попадают в один и тот же ключ кэша, только один запрос получает блокировку и переходит к извлечению данных и повторному заполнению кэша. Все остальные запросы ждут, пока кэш не будет обновлен, вместо того, чтобы все одновременно обращаться к PostgreSQL. Это значительно снижает избыточные запросы к базе данных и защищает систему от каскадных скачков нагрузки.
Масштабирование реплик для чтения
Проблема: Основной сервер передает данные журнала предварительной записи (WAL) каждой реплике для чтения. По мере увеличения количества реплик основной сервер должен передавать WAL большему количеству экземпляров, что увеличивает нагрузку как на пропускную способность сети, так и на ЦП. Это приводит к увеличению и большей нестабильности задержки репликации, что затрудняет масштабирование системы.
Решение: Мы используем почти 50 реплик для чтения в нескольких географических регионах, чтобы минимизировать задержку. Однако при текущей архитектуре основной сервер должен передавать WAL каждой реплике. Хотя в настоящее время он хорошо масштабируется с использованием очень больших типов экземпляров и высокой пропускной способности сети, мы не можем бесконечно добавлять реплики, не перегружая основной сервер. Для решения этой проблемы мы сотрудничаем с командой Azure PostgreSQL над каскадной репликацией, где промежуточные реплики передают WAL последующим репликам. Этот подход позволяет нам масштабироваться до потенциально более чем ста реплик без перегрузки основного сервера. Однако это также вносит дополнительную операционную сложность, особенно в отношении управления отказом. Эта функция все еще находится в стадии тестирования; мы убедимся, что она надежна и может безопасно переключаться, прежде чем развертывать ее в рабочей среде.
Ограничение скорости
Проблема: Внезапный всплеск трафика на определенных конечных точках, резкое увеличение количества ресурсоемких запросов или шквал повторных попыток могут быстро истощить критические ресурсы, такие как ЦП, ввод-вывод и подключения, что приведет к широкомасштабной деградации сервиса.
Решение: Мы внедрили ограничение скорости на нескольких уровнях — приложении, пуле соединений, прокси и уровне запросов — чтобы предотвратить перегрузку экземпляров баз данных внезапными всплесками трафика и возникновение каскадных сбоев. Также важно избегать слишком коротких интервалов повторных попыток, которые могут вызвать шквал повторных попыток. Мы также расширили ORM-уровень для поддержки ограничения скорости и, при необходимости, полного блокирования определенных дайджестов запросов. Эта целенаправленная форма снижения нагрузки позволяет быстро восстановиться после внезапных всплесков ресурсоемких запросов.
Управление схемами
Проблема: Даже небольшое изменение схемы, такое как изменение типа столбца, может вызвать полную перезапись таблицы . Поэтому мы применяем изменения схемы осторожно, ограничивая их легкими операциями и избегая любых, которые приводят к перезаписи всей таблицы.
Решение: Разрешены только легкие изменения схемы, такие как добавление или удаление определенных столбцов, которые не вызывают полной перезаписи таблицы. Мы применяем строгий тайм-аут в 5 секунд для изменений схемы. Создание и удаление индексов одновременно разрешено. Изменения схемы ограничены существующими таблицами. Если для новой функции требуются дополнительные таблицы, они должны находиться в альтернативных шардированных системах, таких как Azure CosmosDB, а не в PostgreSQL. При заполнении поля таблицы мы применяем строгие ограничения скорости, чтобы предотвратить всплески записи. Хотя этот процесс иногда может занимать более недели, он обеспечивает стабильность и предотвращает любое влияние на производственную среду.
Результаты и дальнейшие планы
Эти усилия демонстрируют, что при правильном проектировании и оптимизации Azure PostgreSQL можно масштабировать для обработки самых больших производственных нагрузок. PostgreSQL обрабатывает миллионы запросов в секунду для рабочих нагрузок, ориентированных на чтение, обеспечивая работу наиболее важных продуктов OpenAI, таких как ChatGPT и платформа API. Мы добавили почти 50 реплик для чтения, при этом сохраняя задержку репликации близкой к нулю, поддерживая низкую задержку чтения в географически распределенных регионах и создавая достаточный запас прочности для поддержки будущего роста.
Это масштабирование работает, минимизируя при этом задержку и повышая надежность. Мы постоянно обеспечиваем низкую задержку на стороне клиента, в пределах двухзначных чисел миллисекунд, и пятизначную доступность в производственной среде. И за последние 12 месяцев у нас был только один инцидент SEV-0 в PostgreSQL (это произошло во время вирусного запуска ChatGPT ImageGen, когда объем записи внезапно увеличился более чем в 10 раз, поскольку за неделю зарегистрировалось более 100 миллионов новых пользователей).
Хотя мы довольны тем, чего достигли с помощью PostgreSQL, мы продолжаем расширять его возможности, чтобы обеспечить достаточный запас прочности для будущего роста. Мы уже перенесли рабочие нагрузки, интенсивно использующие запись и подлежащие шардированию, в наши шардированные системы, такие как CosmosDB. Оставшиеся рабочие нагрузки, интенсивно использующие запись, сложнее шардировать — мы активно переносим их, чтобы еще больше снизить нагрузку на основную базу данных PostgreSQL. Мы также работаем с Azure над включением каскадной репликации, чтобы мы могли безопасно масштабировать до значительно большего количества реплик для чтения.
В будущем мы продолжим изучать дополнительные подходы для дальнейшего масштабирования, включая шардированный PostgreSQL или альтернативные распределенные системы, поскольку наши инфраструктурные требования продолжают расти.