Cosmos DB: NoSQL с глобальной репликацией
База, которая живёт сразу в нескольких точках планеты, отвечает за миллисекунды — и жестоко наказывает за неверный ключ раздела.
Azure Cosmos DB — распределённая NoSQL-база данных с гарантиями задержки в SLA, эластичной пропускной способностью и репликацией в любое число регионов одним щелчком.
Зачем нужна ещё одна база
Azure SQL прекрасна, пока данные помещаются на один узел, а пользователи сидят в одном полушарии. Как только приложение становится глобальным, начинаются вопросы: как отдавать данные японцу и бразильцу одинаково быстро? Как пережить чёрную пятницу, когда нагрузка вырастет в 50 раз на четыре часа? Как хранить документы, у которых у каждого свой набор полей?
Cosmos DB отвечает на все три. Она:
- реплицируется в любые регионы Azure — добавление региона это одна команда, данные приезжают сами;
- даёт SLA на задержку: чтение точки по ключу — единицы миллисекунд в 99-м перцентиле;
- хранит JSON-документы без жёсткой схемы;
- масштабируется горизонтально практически без потолка.
За это она требует от вас понимания двух вещей: ключа раздела и RU/s. Не разберётесь — получите либо тормозящую базу, либо счёт, от которого испортится настроение. Разберётесь — получите инструмент, у которого мало конкурентов.
Пять API — но берите NoSQL
Cosmos DB умеет притворяться разными базами: API for NoSQL (родной, документный), MongoDB, Cassandra, Gremlin (графы) и Table. Совместимость нужна для миграций: у вас есть приложение на Mongo — переключаете строку подключения и живёте дальше. Для нового проекта всегда берите API for NoSQL: только у него полный набор возможностей и первоклассная поддержка новых фич.
Иерархия: аккаунт → база → контейнер → item
Контейнер (container) внешне похож на таблицу, но это принципиально другая сущность. Таблица живёт на одном узле. Контейнер — распределённая структура, которую Azure автоматически режет на физические разделы и раскладывает по разным машинам. Как именно резать — решаете вы, задавая ключ раздела.
Partition key: решение, которое определяет всё
Partition key (ключ раздела) — поле документа, по значению которого Cosmos DB группирует данные. Все документы с одинаковым значением ключа образуют логический раздел и всегда лежат вместе.
Три факта, из которых следует всё остальное:
- Логический раздел не может превышать 20 ГБ. Уперлись — запись останавливается, и починить это без пересоздания контейнера нельзя.
- Запрос, в котором указано значение ключа раздела, идёт в одну партицию и стоит копейки. Запрос без ключа раздела рассылается во все партиции (fan-out) — цена растёт линейно с их числом.
- Ключ раздела нельзя изменить после создания контейнера. Только сделать новый контейнер и перелить данные.
Хороший ключ обладает тремя свойствами: высокая кардинальность (много разных значений), равномерность (нагрузка размазана, нет одного «толстого» значения) и присутствие в большинстве запросов.
| Сценарий | Плохой ключ | Хороший ключ |
| Заказы интернет-магазина | /status — значений пять, все заказы со статусом new лягут в один раздел | /customerId — миллионы значений, и почти все запросы всё равно про конкретного покупателя |
| Телеметрия устройств | /date — весь сегодняшний поток бьёт в одну партицию (hot partition) | /deviceId — нагрузка равномерна по устройствам |
| Мультитенантный SaaS | /region — тенантов тысячи, регионов три | /tenantId, а для гигантских клиентов — синтетический /tenantId_month |
Hot partition — главный симптом плохого ключа. Пропускная способность делится между физическими разделами поровну: если у контейнера 10 000 RU/s и 10 партиций, каждая получает по 1000 RU/s. Когда весь трафик идёт в одну — вы упираетесь в 1000 и получаете ошибки 429, хотя формально «оплатили десять тысяч». База при этом выглядит «медленной», а виноват дизайн.
Синтетический ключ — приём против переполнения 20 ГБ: вместо /tenantId пишем в документ поле tenantId_month со значением tenant-42_2026-07. Логические разделы становятся мельче, но запросы за месяц по-прежнему бьют в один.
RU/s: валюта Cosmos DB
RU (Request Unit) — универсальная единица стоимости операции, в которой смешаны CPU, память и IO. Одно чтение документа размером 1 КБ по его id и ключу раздела = 1 RU. Всё остальное измеряется относительно этого эталона.
| Операция | Порядок цены |
| Point read (по id + partition key), 1 КБ | 1 RU |
| Запись документа 1 КБ | ≈ 5 RU (и больше, если много индексов) |
| Запрос внутри одного раздела | от единиц до десятков RU |
| Кросс-партишн запрос | десятки–сотни RU и растёт с числом партиций |
Вы покупаете пропускную способность в секунду: 400 RU/s, 4000 RU/s и так далее. Превысили — Cosmos возвращает 429 Too Many Requests и заголовок x-ms-retry-after-ms: «приходи через столько-то миллисекунд». Официальные SDK ретраят такие ответы сами, но если 429 идут потоком, приложение начинает тормозить.
Прикинем нагрузку и счёт. Цены ниже — порядок величин для одного региона записи.
reads_per_sec = 500 # чтений по ключу
writes_per_sec = 50 # записей
queries_per_sec = 10 # запросов без ключа раздела
RU_READ, RU_WRITE, RU_QUERY = 1, 5, 15
need = reads_per_sec * RU_READ + writes_per_sec * RU_WRITE + queries_per_sec * RU_QUERY
print("Нужно RU/s:", need)
provisioned = 1000 # округляем вверх с запасом
price_per_100_ru_hour = 0.008 # доллары за 100 RU/s в час
hours_per_month = 730
cost = provisioned / 100 * price_per_100_ru_hour * hours_per_month
print(f"Provisioned {provisioned} RU/s ≈ ${cost:.2f} в месяц")
Результат:
Нужно RU/s: 900
Provisioned 1000 RU/s ≈ $58.40 в месяц
Обратите внимание: десять кросс-партишн запросов в секунду съедают столько же, сколько 150 обычных чтений. Один неудачный отчёт способен удвоить счёт.
Три режима оплаты
- Provisioned — фиксированные RU/s, платите круглосуточно, даже если запросов ноль. Предсказуемо и дёшево при ровной нагрузке.
- Autoscale — задаёте максимум (скажем, 4000 RU/s), Cosmos сам гуляет между 10% и 100% от него. Ставка за RU примерно в полтора раза выше, зато вы не платите за пиковый потолок в три часа ночи.
- Serverless — платите за фактически потреблённые RU (порядка четверти доллара за миллион). Идеально для dev-стендов и редких всплесков, но есть лимиты и нет мультирегиона.
И приятная мелочь: free tier даёт первые 1000 RU/s и 25 ГБ бесплатно — одному аккаунту на подписку, навсегда. Учебный проект в него укладывается целиком.
Практика: создаём аккаунт и контейнер
az cosmosdb create \
--name ccdemo-cosmos7 \
--resource-group rg-cosmos-demo \
--locations regionName=westeurope failoverPriority=0 \
--default-consistency-level Session \
--enable-free-tier true
# пропускная способность на уровне БАЗЫ — её делят все контейнеры
az cosmosdb sql database create \
--account-name ccdemo-cosmos7 \
--resource-group rg-cosmos-demo \
--name shop \
--throughput 400
az cosmosdb sql container create \
--account-name ccdemo-cosmos7 \
--resource-group rg-cosmos-demo \
--database-name shop \
--name orders \
--partition-key-path "/customerId" \
--ttl 2592000
# добавить второй регион — данные приедут сами
az cosmosdb update \
--name ccdemo-cosmos7 \
--resource-group rg-cosmos-demo \
--locations regionName=westeurope failoverPriority=0 \
--locations regionName=southeastasia failoverPriority=1
Здесь --throughput задан у базы: 400 RU/s делят между собой все её контейнеры. Альтернатива — dedicated throughput у каждого контейнера, и вот тут кроется классическая денежная ловушка (см. «Частые ошибки»). А --ttl 2592000 включает автоудаление документов через 30 дней — бесплатный способ не копить мусор.
Работа из кода
from azure.cosmos import CosmosClient
from azure.identity import DefaultAzureCredential
client = CosmosClient(
url="https://ccdemo-cosmos7.documents.azure.com:443/",
credential=DefaultAzureCredential(),
)
container = client.get_database_client("shop").get_container_client("orders")
# 1 RU: знаем и id, и ключ раздела — идём прямо в нужную партицию
order = container.read_item(item="order-1001", partition_key="cust-42")
print(order["total"])
# дёшево: запрос ограничен одним разделом
items = container.query_items(
query="SELECT * FROM c WHERE c.customerId = @cid AND c.status = 'new'",
parameters=[{"name": "@cid", "value": "cust-42"}],
partition_key="cust-42",
)
for item in items:
print(item["id"], item["total"])
# сколько это стоило на самом деле
charge = container.client_connection.last_response_headers["x-ms-request-charge"]
print("Потрачено RU:", charge)
# ДОРОГО: ключ раздела не указан — fan-out по всем физическим партициям
all_new = container.query_items(
query="SELECT * FROM c WHERE c.status = 'new'",
enable_cross_partition_query=True,
)
Возьмите за правило смотреть на заголовок x-ms-request-charge в разработке. Он превращает абстрактную «оптимизацию запросов» в конкретное число, которое можно уменьшать.
Уровни согласованности
Распределённая база всегда идёт на компромисс между свежестью данных, задержкой и стоимостью. Cosmos DB не выбирает за вас, а даёт пять уровней — это её фирменная черта.
| Уровень | Что гарантирует | Цена |
| Strong | Читатель всегда видит последнюю подтверждённую запись. Как в обычной СУБД. | Чтение стоит вдвое дороже в RU; при мультирегионе растёт задержка записи. |
| Bounded staleness | Отставание не больше K версий или T секунд — вы сами задаёте границу. | Тоже 2× RU на чтение. |
| Session (по умолчанию) | В рамках одной сессии клиент всегда видит свои записи («читай свои записи»). | 1× RU. Разумный компромисс для 90% приложений. |
| Consistent prefix | Данные могут отставать, но порядок никогда не нарушится: не увидите третью версию раньше второй. | 1× RU. |
| Eventual | Никаких гарантий порядка, только обещание «когда-нибудь сойдётся». | Самый дешёвый и быстрый. |
Практическое правило: Session — дефолт, который стоит менять осознанно. Пользователь опубликовал комментарий и сразу увидел его — этого достаточно почти всегда. Eventual берут для счётчиков лайков и лент рекомендаций, где рассинхрон на секунду никому не мешает, а экономия реальна. Strong — для остатков товара и балансов; помните, что он удваивает RU на чтение и плохо дружит с записью в несколько регионов сразу.
Как это работает
Логические разделы (все документы с одним значением ключа) Cosmos DB раскладывает по физическим разделам — это уже реальные машины с диском, примерно по 50 ГБ и 10 000 RU/s каждая. Растут данные или пропускная способность — Azure молча добавляет физические разделы и перераспределяет логические. Вы этого не видите, но именно поэтому важна равномерность ключа.
Каждый физический раздел — это набор из четырёх реплик внутри региона. Запись подтверждается, когда её приняло большинство: отсюда и живучесть, и гарантия, что чтение из кворума видит свежие данные.
Индексируется по умолчанию каждое поле каждого документа. Это удобно (любой запрос будет работать) и дорого (каждая запись обновляет все индексы). На нагрузке «много пишем, редко ищем» первым делом урезают политику индексирования:
{
"indexingMode": "consistent",
"includedPaths": [
{ "path": "/customerId/?" },
{ "path": "/status/?" }
],
"excludedPaths": [
{ "path": "/*" }
]
}
Так индексируются только два поля, по которым вы реально фильтруете, а стоимость записи падает в разы.
Ещё одна встроенная суперсила — change feed: упорядоченный поток всех изменений контейнера. Его читает Azure Functions и превращает в аналитику, поисковый индекс или уведомления, не нагружая основную базу.
Частые ошибки
- Ключ раздела «на потом». Взяли
/id, потому что «уникально». Формально верно — но тогда каждый документ в своём разделе, а любой запрос по любому другому полю становится кросс-партишн. Исправить можно только пересозданием контейнера. - Hot partition. Ключ вроде
/dateили/countryдля России-центричного сервиса: 90% трафика в одну партицию, ошибки 429 при formально огромной пропускной способности. - Dedicated throughput на каждом контейнере. Минимум для контейнера — 400 RU/s. Завели 10 микроконтейнеров — платите за 4000 RU/s, хотя нагрузки почти нет. Лечится общей пропускной способностью на уровне базы.
- «Мы же ничего не запрашивали». Provisioned-режим списывает деньги каждый час независимо от активности. Забытый прототип с 10 000 RU/s тихо съедает сотни долларов в месяц. Для песочниц — Serverless или free tier.
- Мультирегион «на всякий случай». Каждый добавленный регион умножает стоимость: два региона — примерно двойной счёт. Multi-region writes ещё дороже. Включайте, когда есть пользователи, а не когда красиво выглядит на схеме.
- Strong-консистентность по умолчанию. «Пусть будет надёжнее» удваивает RU на каждом чтении и убивает выигрыш от глобальной репликации.
- Кросс-партишн отчёты на боевом контейнере. Один аналитический
SELECTпо всей базе способен выесть всю пропускную способность и уронить пользовательский трафик в 429. Для аналитики есть change feed и Synapse Link.
Итоги
- Cosmos DB — документная NoSQL-база с глобальной репликацией и SLA на задержку; для новых проектов берите API for NoSQL.
- Partition key — самое важное решение: высокая кардинальность, равномерность, участие в большинстве запросов. Изменить его после создания контейнера нельзя.
- Логический раздел ограничен 20 ГБ; неравномерный ключ создаёт hot partition и ошибки 429.
- RU/s — валюта: point read 1 КБ = 1 RU, запись ≈ 5 RU, кросс-партишн запрос — десятки и сотни.
- Режимы оплаты: Provisioned (ровно), Autoscale (пики), Serverless (редко и мало). Free tier — 1000 RU/s и 25 ГБ.
- Пять уровней согласованности; Session — разумный дефолт, Strong удваивает стоимость чтения.
- Индексируется всё подряд — на write-heavy нагрузке урезайте indexing policy.