Когда данных и нагрузки становится столько, что одна база на одном сервере уже не тянет, а пользователи разбросаны по всему миру, встаёт вопрос: как масштабировать базу и держать низкую задержку везде? В Azure ответ на этот случай — Cosmos DB, глобально распределённая база. Разберём, что это и когда её брать вместо обычной управляемой PostgreSQL.

по ключу секции pk = user-42 одна логическая секция около 1 RU по всем секциям фильтр по полю опрос каждой секции десятки RU

Единицы запросов считают по тому, сколько секций пришлось опросить: чтение по ключу секции идёт в одну и стоит около единицы, запрос без ключа обходит все и стоит десятки. Отсюда и выбор ключа от запросов, и счёт за индексы при записи.

Обязательно

Что такое Cosmos DB

Cosmos DB — это управляемая NoSQL-база, которая автоматически распределяет данные и умеет реплицироваться в несколько регионов мира одним переключателем. Её сильные стороны — предсказуемо низкая задержка при любом масштабе и глобальное распространение: пользователь в каждом регионе читает из ближайшей копии.

Ещё одна особенность — много моделей и API. Cosmos DB умеет прикидываться разными базами: основной API — документный (Core/NoSQL), но есть совместимость с MongoDB, Cassandra, Gremlin (графы) и Table. Это позволяет переносить приложения, написанные под эти базы, почти без изменений. Ближайший аналог в AWS — DynamoDB, но Cosmos шире по моделям доступа и изначально заточен под глобальное распространение.

Ключ секционирования

Главное решение при проектировании Cosmos DB — ключ секционирования (partition key). Данные физически раскладываются по секциям на основе этого ключа, и от его выбора зависит всё: и производительность, и стоимость. Хороший ключ равномерно распределяет нагрузку; плохой создаёт «горячую» секцию, в которую бьёт весь трафик.

Как и в DynamoDB, проектирование начинается не со схемы, а с запросов: вы заранее продумываете, как будете читать данные, и под это выбираете ключ. Переделать позже не выйдет: ключ секционирования существующего контейнера не меняется, «переезд» означает новый контейнер и перелив всех данных.

Есть и жёсткий предел, из-за которого плохой ключ однажды просто останавливает запись: одна логическая секция — все документы с одним значением ключа — не может вырасти больше 20 ГБ. Ключ вида «страна» или «клиент» на крупном значении упирается в этот потолок, и запись в него перестаёт проходить.

Как выглядит документ и запрос

«Модель доступа другая» — утверждение, которое проще показать. Документ здесь — это JSON с обязательным полем идентификатора и полем ключа секционирования:

{
  "id": "ord-10432",
  "customerId": "cust-77",
  "status": "PAID",
  "total": { "amount": 8970, "currency": "RUB" },
  "items": [
    { "sku": "HP-201", "qty": 1, "price": 8970 }
  ],
  "createdAt": "2026-09-25T11:04:00Z",
  "_etag": "\"0x8DC...\""
}

Чтение по ключу — самая дешёвая и быстрая операция, и именно её и стоит делать в большинстве случаев:

var response = container.readItem("ord-10432",
        new PartitionKey("cust-77"), Order.class);   // точечное чтение: 1 RU
resp, err := container.ReadItem(ctx, azcosmos.NewPartitionKeyString("cust-77"), "ord-10432", nil) // точечное чтение: 1 RU
const { resource: order } = await container.item('ord-10432', 'cust-77').read(); // точечное чтение: 1 RU
order = container.read_item(item="ord-10432", partition_key="cust-77")  # точечное чтение: 1 RU

Запрос по нескольким документам пишется на подмножестве SQL:

SELECT c.id, c.status, c.total.amount
FROM c
WHERE c.customerId = 'cust-77' AND c.status = 'PAID'
ORDER BY c.createdAt DESC

И здесь главное отличие от реляционной базы, из которого растут все остальные: запрос внутри одного значения ключа секционирования дешёвый, а запрос по всем значениям — дорогой. Во втором случае база опрашивает все физические разделы (это называют межраздельным запросом), и стоимость растёт с их числом. Поэтому ключ секционирования выбирают так, чтобы типичные запросы попадали в один раздел, а не «чтобы данные распределились ровно».

Точечное чтение по идентификатору и ключу стоит около одной единицы запроса; запрос с фильтром — от единиц до десятков в зависимости от того, сколько документов он просмотрел. Сколько именно стоил запрос, видно в ответе (заголовок с потреблённой ёмкостью) — и это первое, чем стоит пользоваться при отладке.

Уровни согласованности

Необычная для новичка вещь: в Cosmos DB согласованность настраивается. Уровней пять, от самого строгого к самому слабому: строгий (strong), ограниченное устаревание (bounded staleness), сессионный (session), согласованный префикс (consistent prefix) и итоговый (eventual). Крайности понятны: на строгом вы всегда читаете самое свежее, но медленнее и дороже; на итоговом быстро и дёшево, но чтение может ненадолго отставать.

Правильный ответ почти всегда посередине, и он же выбран за вас: по умолчанию стоит сессионный уровень. Его гарантия — «я вижу то, что сам только что записал»: внутри одной клиентской сессии свои записи читаются сразу, чужие могут доехать чуть позже. Для пользовательских сценариев этого обычно достаточно.

Два факта, которые и решают выбор. Первый: чтение на строгом уровне и на ограниченном устаревании стоит вдвое больше RU, чем на сессионном, согласованном префиксе и итоговом, — там запрос идёт к двум репликам вместо одной. Второй: строгая согласованность недоступна, если включена запись в несколько регионов. То есть главный сценарий Cosmos DB — глобальное распространение с записью на местах — со строгим уровнем не собирается в принципе; это не «медленнее и дороже», это «нельзя».

Отсюда практика: для баланса счёта берут строгий уровень и запись держат в одном регионе, для ленты новостей хватает итогового, а в остальных случаях оставляют сессионный. Это прямой рычаг «скорость против свежести данных».

Единицы запросов (RU) и режимы

Стоимость и производительность Cosmos DB меряют в единицах запросов (Request Units, RU) — условная «цена» операции: чтение одного документа в 1 КБ по ключу стоит 1 RU, запись такого же — около 5 RU, а запрос по нескольким секциям — десятки и сотни. Режимов пропускной способности три. Выделенный (provisioned) — вы резервируете столько-то RU/s и платите за них независимо от того, шла ли нагрузка: предсказуемо, когда нагрузка ровная. Автомасштабируемый (autoscale) — вы задаёте потолок RU/s, а платите за фактический максимум в каждый час в пределах этого потолка; на практике это самый ходовой вариант, потому что не надо угадывать. Serverless — платите строго за выполненные запросы, удобно для нерегулярной нагрузки и стартующих проектов.

Когда Cosmos DB, а когда PostgreSQL

Берите Azure Database for PostgreSQL, когда: классическое приложение, данные помещаются на один (пусть и мощный) сервер, нужны реляционная модель, сложные запросы и джойны, зрелая экосистема SQL. Разумный выбор по умолчанию для большинства сервисов.

Берите Cosmos DB, когда: нужен глобальный масштаб и низкая задержка в разных регионах, гибкая схема, огромные объёмы с предсказуемой задержкой по ключу. Типичные сценарии — глобальные пользовательские профили, каталоги, телеметрия, игровые данные.

Практический совет новичку: не берите Cosmos DB «на будущее». Если данные спокойно живут на одном сервере и вам нужны джойны — PostgreSQL проще и дешевле. Cosmos оправдывает себя именно там, где нужен глобальный масштаб и вы готовы проектировать под доступ по ключу.

Где это применяется

Cosmos DB закрывает нишу «глобальный масштаб и низкая задержка при гибкой схеме» — то, ради чего раньше приходилось вручную городить репликацию по регионам. В связке с serverless она особенно естественна: функция без сервера + база без сервера дают систему, которая масштабируется и тарифицируется по факту.

Типичные ошибки новичков:

  • берут Cosmos DB для маленького сервиса — там, где PostgreSQL проще и богаче;
  • плохо выбирают ключ секционирования — создают горячую секцию и упираются в неё;
  • оставляют строгую согласованность везде — переплачивают там, где хватило бы итоговой;
  • ждут привычного SQL с джойнами — это другая модель доступа.
Дополнительно: при первом чтении можно пропустить

Глубже: отказ 429: ёмкость кончиласьрасширенное

Первое, с чем сталкивается каждый на Cosmos, — ответ 429 (Too Many Requests). Это не поломка и не ошибка кода: это означает, что запросы в этот момент потребовали больше единиц, чем выделено контейнеру.

Три вещи, которые надо про это знать.

Клиентская библиотека повторяет сама. В ответе приходит заголовок x-ms-retry-after-ms с числом миллисекунд, и библиотека ждёт указанное время и повторяет запрос — по умолчанию до девяти раз. То есть в приложении отказ чаще всего не виден вовсе: он превращается в увеличенную задержку. Отсюда узнаваемый симптом «всё работает, но иногда медленно» — и это повод посмотреть на метрику потребления, а не на код.

Настройки повторов стоит знать. Число попыток и предельное время ожидания настраиваются у клиента; увеличивать их бесконечно бессмысленно — если ёмкости не хватает постоянно, повторы только растягивают очередь.

Когда отказ доходит до приложения, его надо различать. Ошибка с кодом 429 после исчерпания повторов означает «нужно больше ёмкости или меньше запросов», а не «повторить ещё раз». Правильная реакция — сигнал и разбор, а не бесконечный повтор в коде.

Что делают, когда 429 стало много: смотрят, какие запросы потребляют больше всего (это видно в метриках и в ответе каждого запроса), режут стоимость через политику индексирования и через точечные чтения вместо запросов, поднимают выделенную ёмкость или переходят в режим оплаты по факту (serverless или автоматическое масштабирование), и проверяют распределение по разделам: часто ёмкости хватает в сумме, но один «горячий» раздел упирается в свою долю — и это уже вопрос выбора ключа секционирования.

Глубже: лента измененийрасширенное

Механизм, на котором в Cosmos строят половину интеграций и который обычно не упоминают: лента изменений (change feed). Это упорядоченный поток изменений документов внутри раздела, который можно читать с начала или с произвольного момента.

Для чего её берут:

  • Проекции и модели для чтения. Основной контейнер хранит документы как удобно для записи, а лента наполняет второй контейнер или поисковый индекс, удобный для чтения. Это тот же приём, что разделение команд и запросов в архитектуре, только встроенный в базу.
  • Интеграции. Изменение статуса заказа — и лента запускает функцию, которая отправляет уведомление или сообщение в шину.
  • Перенос и переливка. Новая модель данных, новый ключ секционирования — лента наполняет новый контейнер, пока старый продолжает работать.
  • Архив. Все изменения складываются в хранилище объектов как история.

Как читают: проще всего триггером функции (платформа сама держит положение чтения и распределяет разделы между экземплярами), или библиотекой в приложении. Три свойства, которые важно знать: лента отдаёт только вставки и обновления, удаления в неё не попадают (для них применяют приём «мягкого удаления» — пометку в документе плюс срок жизни); порядок гарантирован внутри раздела, а не глобально; и доставка «хотя бы один раз», то есть обработчик обязан быть идемпотентным.

Глубже: индексирование: за него платят при записирасширенное

Свойство, которое объясняет неожиданно большой счёт: по умолчанию Cosmos индексирует все поля документа. Удобно (любой запрос работает сразу), и платится это единицами при каждой записи: чем больше индексируемых путей, тем дороже вставка и обновление.

Поэтому политику индексирования почти всегда сужают: оставляют пути, по которым реально фильтруют и сортируют, а остальное исключают.

{
  "indexingMode": "consistent",
  "includedPaths": [
    { "path": "/customerId/?" },
    { "path": "/status/?" },
    { "path": "/createdAt/?" }
  ],
  "excludedPaths": [
    { "path": "/*" }
  ]
}

Здесь исключено всё, а включены три поля — то есть запись стоит минимум. Порядок работы такой: сначала пишут запросы, потом включают в индекс ровно те поля, которые в них участвуют. Обратный порядок («включим всё, потом посмотрим») означает, что вы платите за индексы, которыми никто не пользуется.

Две детали. Изменение политики применяется в фоне и не требует остановки. И большие вложенные массивы — самый дорогой случай: индексирование каждого элемента массива умножает стоимость записи, поэтому пути внутрь массивов исключают в первую очередь, если по ним не ищут.

Глубже: много регионов: что происходит с записьюрасширенное

«Глобальное распространение одним переключателем» верно для чтения и требует уточнения для записи.

Один регион записи (по умолчанию). Читать можно из любого региона (быстро и рядом с пользователем), а писать — только в один. Запись из другого региона означает сетевой переход до региона записи, то есть задержку. Конфликтов при этом не бывает по определению — есть единственный источник правды.

Несколько регионов записи. Включается настройкой, и после этого писать можно в любом регионе — задержка записи становится локальной. Цена: появляются конфликты. Два пользователя изменили один документ в разных регионах, и база должна решить, какая версия победит. По умолчанию действует правило «побеждает последняя запись» по системной отметке времени; можно указать своё поле для сравнения или обрабатывать конфликты самому через специальную ленту конфликтов.

Отсюда практический выбор. Профили, настройки, каталог — данные, где конфликты редки и потеря одного изменения терпима: несколько регионов записи работают хорошо. Счётчики, балансы, статусы заказов — данные, где «побеждает последняя запись» означает потерю операции: либо один регион записи, либо своя логика разрешения конфликтов, что дорого и сложно.

И отдельная вещь, которую стоит знать до включения: несколько регионов умножают стоимость — выделенная ёмкость оплачивается в каждом регионе, и трафик между регионами тоже платный.

Глубже: сколько это стоит на входерасширенное

Про цену Cosmos важно знать одно число: минимальная выделенная пропускная способность контейнера — 400 единиц в секунду. Это значит, что даже пустая база с одним контейнером стоит денег круглосуточно (порядка двадцати с небольшим долларов в месяц за контейнер в одном регионе), и десять контейнеров «на всякий случай» превращаются в заметную сумму.

Что с этим делают:

  • Общая ёмкость на уровне базы. Ёмкость можно выделить не контейнеру, а базе целиком — и тогда контейнеры её делят. Это правильный ответ, когда контейнеров много и нагрузка на них неровная.
  • Автоматическое масштабирование. Ёмкость меняется сама в пределах десятикратного диапазона от заданного максимума; минимум при этом — десятая часть максимума, то есть от 400 единиц всё равно не уйти, но платите по факту в пределах диапазона.
  • Режим оплаты по факту (serverless). Нет постоянной платы за ёмкость — платите за потреблённые единицы. Правильный выбор для стендов, для нечастой нагрузки и для начала проекта; ограничения — меньший предел пропускной способности и не все возможности доступны.
  • Меньше контейнеров. Разложить несколько видов документов в один контейнер с признаком типа (и с общим ключом секционирования) — обычная практика в Cosmos, и она же экономит на минимальной ёмкости.

Глубже: ещё один Cosmos, который путает при поискерасширенное

При поиске по названию обязательно попадётся «Cosmos DB для PostgreSQL», и это не то, о чём вся статья. Речь о другом продукте: распределённом PostgreSQL на основе расширения Citus (раньше он так и назывался). Там обычный PostgreSQL с SQL, транзакциями и схемой, только данные распределены по узлам по ключу распределения — то есть это реляционная база для больших объёмов, а не документная.

Общего у них только имя семейства продуктов. Если вам нужен распределённый PostgreSQL — это он; если нужна документная база с единицами запросов, ключом секционирования и лентой изменений — это тот Cosmos, о котором шла речь выше. Путаница обычная, и стоит она нескольких часов чтения не той документации.

Коротко

  • Cosmos DB это управляемая NoSQL-база с несколькими API, которая распределяет данные и реплицируется по регионам одним переключателем.
  • Ключ секционирования выбирают от запросов и не меняют потом: логическая секция не больше 20 ГБ, запрос внутри одного значения ключа дешёвый, по всем значениям дорогой.
  • Согласованность настраивается из пяти уровней, по умолчанию сессионная «вижу свои записи»; строгий уровень и ограниченное устаревание стоят вдвое больше единиц.
  • Всё меряют в единицах запросов: точечное чтение около одной, запись около пяти, фильтр от единиц до десятков; стоимость каждого запроса видна в ответе.
  • 429 это не поломка, а исчерпание выделенных единиц: библиотека повторяет сама по заголовку с задержкой, а постоянные 429 лечат ёмкостью и стоимостью запросов, а не бесконечными повторами.
  • Лента изменений отдаёт упорядоченный поток изменений по разделу и строит проекции, интеграции, переливки и архив; читают её триггером функции или библиотекой.
  • По умолчанию индексируется каждое поле, и платят за это при каждой записи: политику сужают до путей, по которым фильтруют и сортируют, а вложенные массивы исключают.
  • Чтение из любого региона бесплатно по задержке, запись по умолчанию в один регион; несколько регионов записи дают локальную запись ценой конфликтов и умножения счёта.
  • Минимум 400 единиц в секунду на контейнер стоит круглосуточно: общая ёмкость на базу, автомасштабирование, режим по факту для стендов и меньше контейнеров; Cosmos DB для PostgreSQL это другой продукт.

Что почитать дальше