Масштабирование оперативного хранилища для более 1 млрд пользователей ChatGPT (часть II)
Как мы оптимизировали Azure Cosmos DB в нашей платформе хранения данных приложений Habitat, чтобы справиться с беспрецедентным ростом.
Всем продуктам OpenAI нужен быстрый и надежный доступ к данным. Его обеспечивает наша платформа хранения Habitat. Она должна обрабатывать запросы более миллиарда пользователей из 40 географических регионов за считаные миллисекунды. Перед нашей инфраструктурой стояла исключительная задача: поддерживать взрывной рост и новые сценарии использования, продолжая повышать надежность, производительность и скорость разработки продуктов. Для этого пришлось переосмыслить архитектуру и эксплуатацию нашей платформы оперативного хранения данных. Мы постоянно искали баланс, следуя такой последовательности: сначала добиться надежности, достаточной для критически важного продуктового трафика, затем — скорости для пользователей по всему миру и, наконец, эффективной эксплуатации в огромном масштабе.
В этой серии из двух частей мы рассказываем, как превратили Habitat из простой клиентской библиотеки и базы данных в глобально распределенную платформу, обслуживающую более 500 петабайт данных. Часть I была посвящена превращению Habitat из клиента на Python в сервис. В ней мы также рассмотрели приемы, позволившие масштабировать сервис на Python далеко за пределы того, что многие сочли бы практически возможным.
В этой статье, части II, мы рассказываем, как масштабировали лежащий в основе платформы компонент хранения — Azure Cosmos DB. В тесном сотрудничестве с командой Azure мы разработали жизнеспособный мультитенантный подход на основе совместно управляемых контейнеров, обеспечивающий отличную скорость чтения в огромном глобальном масштабе.
Рисунок 01 · Что такое Habitat?
Платформа оперативного хранения
Habitat — созданная нами платформа оперативного хранения, которая обеспечивает продуктам OpenAI быстрый и надежный доступ к необходимой информации.
- Запрос
- Ответ
- Изменения (CDC)
По мере роста компаний инфраструктурные команды часто переходят от однотенантных систем к мультитенантным. При однотенантном подходе каждая команда, отвечающая за часть продукта, поддерживает собственную систему. Это ограничивает последствия сбоев, четко разграничивает ответственность и изолирует нагрузки. По мере роста числа команд и компонентов продукта эксплуатация становится дорогой. Однотенантность возлагает планирование ресурсов на каждую продуктовую команду и часто приводит к выделению ресурсов с большим избытком.
Мультитенантность, напротив, передает управление централизованной команде баз данных, у которой больше знаний и опыта в выделении ресурсов. Но у нее есть свои проблемы, например шумные соседи: один тенант потребляет необычно большую долю ресурсов в ущерб остальным.
При разработке Habitat нам пришлось взвесить все эти компромиссы, чтобы построить надежную и эффективную систему хранения большого масштаба. Сегодня Habitat — мультитенантный слой работы с базой данных для многих продуктов OpenAI. Но мы прошли полный круг: от одного мультитенантного контейнера Azure Cosmos DB к отдельным однотенантным контейнерам хранения, а затем обратно к мультитенантному контейнеру по умолчанию. Развитие мультитенантности в Habitat еще на раннем этапе. Мы поддерживаем множество сценариев, но пока смогли отложить инвестиции в это направление, везде, где возможно, опираясь на встроенные механизмы Azure Cosmos DB.
Прежде чем перейти к нашей истории работы с Azure Cosmos DB, поясним несколько основных понятий:
Учетная запись Azure Cosmos DB задает набор контейнеров и политику репликации
Каждому контейнеру выделяется определенная пропускная способность в единицах запроса в секунду (RU/s)
Данные в контейнере хранятся по обычной схеме шардирования по заданному ключу логического раздела, с документированными лимитами хранения и пропускной способности на каждый ключ
Azure Cosmos DB распределяет ключи логических разделов по меньшему числу физических разделов. Физические разделы делятся при превышении порогового объема хранения или когда контейнеру нужно больше разделов для обеспечения выделенной пропускной способности
Изначально мы пропускали любую клиентскую нагрузку, рассчитывая, что заявленный лимит 10 тыс. RU/s на ключ логического раздела позволит ограничивать входящую нагрузку. В самом начале мы также использовали лишь один контейнер Azure Cosmos DB по умолчанию в каждой учетной записи. Это работало достаточно хорошо, но выявило ряд проблем:
Из-за ранних решений при проектировании API мы предоставили клиентам возможность напрямую выбирать ключ логического раздела. В результате в разных продуктовых сценариях часто возникают коллизии по распространенным ключам разделов, таким как user_id или workspace_id. Поскольку эти сценарии делили лимиты Azure Cosmos DB для одних и тех же ключей логических разделов, такая архитектура ограничивала общую пропускную способность для отдельного пользователя.
До перехода на Habitat и Azure Cosmos DB при масштабировании Postgres мы часто сталкивались с тем, что множество запросов от одной очень активной организации или к одному горячему ключу могло вывести из строя всю базу данных: один тенант создавал непропорционально высокую нагрузку. Мы совместно потратили много сил на кеширование таких ключей и внедрение механизма временных блокировок (leases) при промахах кеша, чтобы вызванная ими нагрузка не перегружала базу данных.
Figure 02a · Logical partition key limits
Different clients, the same partition key
Хотя в документации лимиты объема данных и пропускной способности указаны для ключей логических разделов, фактически они применяются на уровне физического раздела. На практике это означает, что логические разделы не могут достичь 10 000 RU/s. Вместо этого они делят лимиты физического раздела со всеми остальными ключами логических разделов, размещенными на нем.
Из-за этого шумные соседи влияли на множество несвязанных ключей логических разделов: нагрузки, работавшие с данными маловостребованных функций, нарушали основные процессы входа и регистрации.
Figure 02b · Noisy neighbors
Different users, the same physical partition
Лимит пропускной способности физического раздела тоже непостоянен: он зависит от общей пропускной способности, выделенной контейнеру Azure Cosmos DB. Выделенная контейнеру пропускная способность распределяется примерно поровну между его физическими разделами.
По мере роста объема данных физические разделы делятся. Их число растет, а общая выделенная пропускная способность остается прежней. В результате с ростом объема данных фактический лимит пропускной способности каждого раздела снижается.
Для сценариев с наибольшими объемами хранения, похожих на хранение BLOB-объектов, это создает риск сбоев: один лишь рост объема данных снижает доступную пропускную способность активных нагрузок, даже если частота запросов и интенсивность использования не меняются.
Figure 02C · Weak partitions
The limit falls below demand
A partition splits when it reaches its storage limit. Each child receives half the throughput capacity.
Отдельные контейнеры помогают частично решить проблемы лимитов ключей разделов и шумных соседей. Каждому отдельному контейнеру можно выделить собственные независимые физические разделы. Тогда шумными соседями или источниками пересечений в пространстве ключей разделов могут быть только нагрузки внутри этого контейнера. Но создание новых контейнеров требует затрат: для каждого нужно отдельно планировать ресурсы. Поэтому мы стремимся выделять контейнеры только там, где это оправдано границами сервиса или сценария использования.
Мы начали использовать отдельные контейнеры Azure Cosmos DB для наиболее важных новых сценариев, требующих строгой изоляции. Вместе со скриптом, который решал проблему слабых разделов, приводя общую выделенную пропускную способность каждого контейнера в соответствие с числом его физических разделов, это позволило избежать большинства прежних проблем.
Однако мы начинали с контейнера по умолчанию, и многие старые критически важные сценарии, к сожалению, остались привязаны к нему: для переноса требовалась миграция данных. Чтобы защитить критически важные сервисы, все еще зависевшие от данных в общем контейнере по умолчанию, мы часто советовали командам создавать отдельные контейнеры для новых сервисов или крупных функций продукта. По мере масштабирования нагрузка на контейнер по умолчанию росла, а с ней — и число инцидентов в рабочей среде. Мы стали выделять контейнеры для любого нового сервиса или крупной функции, просто чтобы уберечь контейнер по умолчанию от дополнительной нагрузки.
Это уберегло нас от множества сбоев всей платформы: последствия ограничивались ухудшением работы отдельных функций или сервисов. Но со временем накладные расходы на выделение новых контейнеров дали о себе знать:
В прежнем общем контейнере по умолчанию рост отдельного нового сценария с 0 до 1 млн запросов в секунду (QPS) вряд ли существенно изменил бы совокупную нагрузку. В изолированном контейнере такой же рост с 0 до 1 млн QPS мог увеличить нагрузку в 1000 раз относительно исходного уровня, создаваемого только внутренним тестированием продукта.
С выделенными контейнерами командам теперь нужно было не забывать заранее обеспечивать ожидаемую на момент запуска пропускную способность. Продуктовые команды часто выпускали функции, забыв заранее увеличить ресурсы. В день запуска мы оказывались на экстренных совещаниях по инцидентам и пытались масштабировать контейнеры в последний момент.
Сглаживание нагрузки за счет ее смешивания — ключевое преимущество мультитенантных систем, которым при таком подходе к контейнерам мы жертвуем ради лучшей изоляции.
Figure 03 · Lack of stable aggregate growth
The same workload, different headroom
1K RU/s during load testing → 1M RU/s at launch
Со временем мониторинг использования выделенных контейнеров стал требовать еще и глубокого знания продукта: нужно было понимать, когда трафик изменится или вырастет.
Мы можем предоставить простой скрипт управляющего слоя, который увеличивает выделенную пропускную способность каждого контейнера с учетом тенденций роста. Но без плавного роста совокупной нагрузки для правильного масштабирования приходилось привлекать к планированию ресурсов практически каждую продуктовую команду OpenAI.
Организационно этот подход не масштабировался: команды разных сервисов уделяли очень разное внимание выделению ресурсов Azure Cosmos DB.
По мере роста организации справляться с этими эксплуатационными проблемами становилось все труднее. Без единой центральной команды планирования многие команды планировали ресурсы плохо. В итоге мы вернулись к мультитенантному решению и в тесном сотрудничестве с командой Azure устранили исходные проблемы совместного использования ресурсов:
Если первыми ограничивать запросы к самым горячим ключам, шумные соседи не смогут занять всю доступную пропускную способность физического раздела. Это ослабляет эффект шумного соседа, при котором один горячий ключ может нарушить работу всей платформы.
Аналогично QoS с разделением запросов по приоритетам не дает фоновым нагрузкам, например массовой вставке или выгрузке данных, занять всю пропускную способность физического раздела. Запросам этих нагрузок назначается более низкий приоритет, поэтому они ограничиваются первыми. Это защищает от ограничений более приоритетные интерактивные запросы конечных пользователей.
В Azure Cosmos DB уже был бессерверный режим, который мог решить нашу проблему слабых разделов в режиме с выделенной пропускной способностью. Но у него был серьезный недостаток: масштабирование зависело только от объема хранения, а расход RU ограничивался 5 тыс. RU/s на физический раздел. Это решило бы проблему слабых разделов, но не позволило бы масштабировать сценарии с большим потреблением RU.
Команда Azure Cosmos DB разработала для нас улучшенный бессерверный режим: каждый физический раздел мог масштабироваться до 10 тыс. RU/s, а разделение стало зависеть не только от объема хранения, но и от потребления RU. Это позволило отказаться от ручного мониторинга и масштабирования контейнеров.
Благодаря этим изменениям мы смогли вернуться к одному общему контейнеру. Это значительно упростило для продуктовых команд как предварительное, так и текущее планирование ресурсов, сохранив изоляцию нагрузок. За прошедшие с тех пор месяцы у нас не было ни одного инцидента, связанного с выделением ресурсов или шумными соседями, несмотря на ускорение разработки продуктов.
Когда компании достигают масштаба OpenAI, управляемые сервисы становятся не такими уж «управляемыми». Многие нагрузки выходят за рамки типичных сценариев инфраструктуры облачного провайдера. Когда OpenAI стала одним из крупнейших пользователей Azure Cosmos DB, для успешной эксплуатации потребовался другой подход: общие тесты и панели мониторинга, прямые каналы связи между инженерами и совместная ответственность за сбои, возникающие только в нашем масштабе. Сегодня наша крупнейшая учетная запись Azure Cosmos DB хранит десятки петабайт данных и обслуживает более 20 млн запросов в секунду. Насколько нам известно, мы единственная компания, развернувшая Azure Cosmos DB в таком масштабе.
В конце 2024 года региональный сбой наглядно показал, почему такое сотрудничество необходимо. Тогда в основном регионе Azure одной из наших крупнейших учетных записей Azure Cosmos DB произошло отключение электропитания. При последующем аварийном переключении выяснилось, что перенос крупнейших учетных записей во вторичный регион займет несколько часов. Ни мы, ни команда Azure Cosmos DB не тестировали эту процедуру в нашем масштабе.
Примерно через месяц, возвращая эти учетные записи в исходный регион, мы спровоцировали еще один серьезный сбой. Массовое одновременное подключение к исходному региону перегрузило его шлюз Azure Cosmos DB. Из-за особенности обработки переключений клиентом Azure Cosmos DB в режиме шлюза все клиенты немедленно установили соединения и отправили запросы в исходный регион, хотя туда вернулась лишь часть физических разделов.
При каждом сбое OpenAI и Azure следовали своим процедурам управления инцидентами, выявляя первопричины и внедряя исправления. Но эти сбои были симптомами более глубокой системной проблемы в том, как наша команда и команда Azure Cosmos DB организовывали эксплуатацию и тестирование. Поначалу управляемые сервисы позволяли нам двигаться быстро и сосредоточиться на бизнес-целях. В большом масштабе они становятся менее «управляемыми» и требуют тесной интеграции с провайдером на всех уровнях — от проектирования до эксплуатации.
Сегодня инженерные команды Habitat и Azure совместно отслеживают все уровни Habitat: от наших сервисных слоев до физического хранения Azure Cosmos DB. Мы эксплуатируем тестовую учетную запись Azure Cosmos DB, сопоставимую по размеру с рабочей, и проверяем на ней изменения. Мы вместе тестируем и дорабатываем SDK, вырабатываем лучшие практики масштабирования учетных записей Azure Cosmos DB и выбора их размера. Наши инженеры общаются напрямую через Teams. Мы даже предусмотрели возможность напрямую вызывать дежурных друг друга, минуя уровни техподдержки.
За последний год мы совместно проделали большую работу по улучшению масштабируемости управляющего слоя и шлюза Azure Cosmos DB. Добавили защиту от перегрузок и кеширование метаданных Azure Cosmos DB на шлюзе для защиты раздела метаданных. Снизили нагрузку от наших процессов habitat-service на Python за счет пула соединений и их более частого повторного использования. По возможности разместили связанные рабочие нагрузки рядом, а критические нагрузки изолировали от фоновых с помощью разных VPC.
Такая тесная совместная разработка сыграла ключевую роль в стабильности нашей Azure Cosmos DB за последний год. Любая достаточно крупная или критически важная система требует такой командной работы — без барьеров между зонами ответственности и организационными структурами.
Как мы отмечали в части I, наш основной принцип: сначала добиться, чтобы все работало, затем ускорить, а потом удешевить. Сначала надежность, затем производительность, затем эффективность. Habitat стала критически важной инфраструктурой OpenAI, поэтому первой задачей было обеспечить ее надежность. В 2025 году, по мере стабилизации системы и глобального расширения OpenAI, следующим ограничением для пользователей и продуктовых команд стала задержка.
Вычислительные ресурсы и хранилища часто находились в разных регионах, поэтому межрегиональные запросы были неизбежны.
Figure 04 · Improving performance at a global scale
Global traffic
Compute and storage capacity were often located in different regions, making cross-region requests unavoidable.
Географическое размещение вычислительных ресурсов и хранилищ показано только для иллюстрации.
Задержка сильно различалась в зависимости от того, где хранились данные и обрабатывался запрос. Чтобы ускорить запросы к Habitat, разработчики продуктов добавляли кеширование на стороне клиента, объединяли запросы в пакеты, сокращали число чтений и записей в базу и по возможности выносили работу за пределы критического пути. Это привело к интересным продуктовым решениям, более устойчивым к проблемам с базой данных, но одновременно усложнило продукты и замедлило их развитие. Так мы не достигли цели Habitat: ускорять разработку продуктов без необходимости разбираться в устройстве базы данных.
В 2026 году это стало одним из наших главных приоритетов. Мы разработали многоуровневую стратегию ускорения чтения, на которое приходится более 90% запросов к Habitat:
Для пользователей с требованиями к резидентности данных у нас есть несколько учетных записей Azure Cosmos DB в разных регионах Azure по всему миру. Даже если строгих требований к резидентности данных нет, их размещение ближе к пользователям снижает задержку и улучшает пользовательский опыт.
На практике нам не удается широко применять этот подход за пределами задач резидентности данных: размещение ограничено физическими ресурсами облака. Наши наборы данных настолько велики, что многим регионам не хватает места для хранения объема, соответствующего их доле пользовательского трафика.
В регионах с ограниченными ресурсами ЦОД, электропитанием или охлаждением иногда приходится выбирать: выделить ресурсы под локальное хранилище и снизить задержку Habitat или использовать их для дополнительных вычислений при обслуживании запросов, обращаясь к более удаленному хранилищу.
Поэтому решения о размещении часто требуют продуктового компромисса между: А) снижением задержек для разных продуктов и групп пользователей и Б) доступными ресурсами хранения и вычислений.
Для наиболее чувствительных к задержкам нагрузок, например метаданных аутентификации, нужных при каждом запросе, мы сокращаем время обмена (RTT) между вычислительными ресурсами и Habitat за счет учетных записей Azure Cosmos DB с репликацией во множество вторичных регионов. Некоторые учетные записи реплицируются во все 40 регионов Azure, где мы обслуживаем трафик, другие — как минимум в один регион на каждом континенте.
Нам пришлось раньше запланированного встроить в Habitat незаметный для клиентов механизм переноса данных и маршрутизации запросов, чтобы переносить наборы данных между учетными записями с разными топологиями репликации.
Однако и этот подход в конечном счете ограничен доступными ресурсами, поэтому применяется лишь к нескольким тщательно отобранным наборам данных.
В нагрузках Habitat преобладает чтение: приложения постоянно запрашивают настройки, состояние и другие свойства. Непропорционально большая доля трафика приходится на относительно небольшой рабочий набор часто запрашиваемых типов данных. Поэтому система особенно хорошо подходит для распределенного кеширования. Кеширование ускоряет работу даже в одном регионе, но при работе между регионами эффект усиливается: оно позволяет «скрыть» межрегиональную задержку.
Мы встроили в Habitat слой распределенного кеширования по схеме lookaside. Он хранит копии недавно запрошенных объектов и связей на серверах Valkey, размещенных в нашей инфраструктуре рядом с местом обработки пользовательских запросов. Обычно в таких кешах время жизни (TTL) задает компромисс между долей попаданий и актуальностью данных. Короткий TTL уменьшает устаревание данных, но снижает пользу кеширования: записи быстро истекают, и их приходится чаще запрашивать заново. Длинный TTL повышает долю попаданий, но кеш дольше отдает устаревшие данные, ухудшая пользовательский опыт и делая поведение продукта непредсказуемым.
Другой подход — явно инвалидировать кеш, когда приложение записывает изменения в соответствующие исходные данные. Это существенно повышает актуальность кешированных данных, но корректно реализовать такой механизм бывает сложно. Кроме того, остаются пограничные случаи, например частичные сбои, из-за которых кеш остается устаревшим. В мультирегиональных системах эти проблемы усиливаются: межрегиональные сети менее надежны, а между обновлением кеша и репликацией хранилища возникают гонки. Добавление TTL ограничивает максимальное время устаревания, но не устраняет описанный выше компромисс.
Мы выбрали более продвинутый подход к инвалидации, следуя примеру таких систем, как TAO от Meta. Встроенный кеш Habitat использует захват изменений данных (CDC) через ленту изменений Azure Cosmos DB. Инвалидации распространяются по всему миру за секунды, обеспечивая высокую долю попаданий при низком уровне устаревания данных. Сервис в нашей инфраструктуре читает эту ленту и инвалидирует локальные кеши при обновлении исходных данных. При следующем чтении происходит промах кеша, и он заполняется актуальной версией данных.
Мы также существенно снизили задержку, сократив накладные расходы самого сервиса Habitat: переписали код с Python на более производительный Rust.
Вместе эти изменения снизили глобальную задержку чтения p50 примерно со 120 до 10 мс — в 12 раз с тех пор, как в начале этого года мы сосредоточились на производительности.
Figure 05 · Improving performance at a global scale
p50 latency for reads
Three major design changes improved read latency
Хотя кеширование резко снизило медианную задержку, самые медленные запросы все еще требуют чтения некешированных данных из хранилища, часто в удаленных регионах. По мере ввода новых мощностей и повышения эффективности хранения мы планируем решить более фундаментальную задачу: изначально размещать данные ближе к месту их использования.
Надежный и масштабируемый CDC — передача изменений базы данных зависимым системам — важен не только для стратегии кеширования Habitat. Миллионы изменений в секунду из тысяч контейнеров и миллионов их разделов проходят через более сотни топиков Kafka. Они также обеспечивают работу вторичных индексов с согласованностью в конечном счете, конвейеров перемещения данных, офлайн-аналитики и других функций продукта в реальном времени. С глобальным ростом нашей инфраструктуры отслеживать изменения строк становилось все сложнее. Чтобы зарегистрировать функцию обратного вызова при изменении строк своего типа, клиенту нужно, помимо прочего, определить, какие учетные записи и контейнеры Cosmos хранят его данные, какие экземпляры сервиса CDC отслеживают эти контейнеры в соответствующих регионах, а затем правильно масштабировать воркеры CDC, Kafka и своего консьюмера (обработчика). Наша платформа CDC призвана скрыть все это от разработчиков продуктов и дать им простой интерфейс: клиент регистрирует функцию для обработки измененных строк, а всю инфраструктурную работу и масштабирование мы берем на себя.
Внутренний управляющий слой постоянно проверяет, что изменения всех нужных контейнеров захватываются, выбирает регионы Cosmos для чтения и эффективно распределяет работу между тысячами воркеров в нескольких географических регионах. Чтение ленты из регионов-реплик с пониженным приоритетом защищает чувствительный к задержкам трафик Habitat от фоновой нагрузки захвата изменений. В системе такого размера постоянно происходят сбои воркеров и контейнеров, ошибки хранилища временных блокировок, конфигурации и систем-получателей. Мы повысили надежность управления жизненным циклом воркеров CDC и временными блокировками, изолировали сбои отдельных потоков и добавили защитные механизмы для изменений конфигурации и развертываний. Очереди недоставленных сообщений не дают постоянным ошибкам доставки блокировать обработку следующих сообщений. При региональных сбоях мы переключаем процессы чтения на другие регионы Cosmos. При нашем масштабе пришлось даже изменить SDK Azure для CDC: чтобы эффективнее сохранять контрольные точки, мы накапливаем их в памяти и записываем лишь периодически.
Хотя внутреннее использование Azure Cosmos DB в Habitat устроено сложно, главная ценность платформы всегда заключалась в том, чтобы скрыть инфраструктурные детали и ускорить разработку продуктов. Наша платформа CDC следует тому же принципу.
Небольшая команда смогла поддерживать быстрорастущую платформу хранения, намеренно ограничивая API хранения, продолжая масштабировать сервис на Python и отдавая приоритет самым срочным инженерным задачам. Сначала мы сосредоточились на надежности для критически важного продуктового трафика, затем — на скорости чтения для пользователей по всему миру и, наконец, на эффективности эксплуатации в большом масштабе. Этот подход обеспечил быстрый рост и новые сценарии использования, одновременно повышая надежность, производительность и скорость разработки продуктов.
Чтобы масштабировать Habitat и хранилище переписок для следующего миллиарда пользователей, нам нужно решить задачу репликации данных по всей инфраструктуре в еще большем масштабе. Готовясь к этому, мы разрабатываем собственную систему оперативного хранения эксабайтного масштаба, оптимизированную для объемных типов данных Habitat. Эта система пока в разработке. Она сочетает ядро RocksDB Cloud от Rockset с репликацией на основе Raft. Она реализует ту же документную модель, что и Azure Cosmos DB, но использует отделенное от вычислений хранилище и физическую репликацию. Это сокращает необходимый объем флеш-памяти ценой более низкого пикового QPS.
Если вам интересно решать подобные задачи инфраструктуры данных в масштабе передовых систем, посмотрите наши вакансии.


