Немало баз данных на сегодняшний день стремятся сделать всё, чтобы обеспечить высокую производительность, масштабируемость и доступность, при этом минимизируя сложность и стоимость поддержки. Azure Cosmos DB — отличный пример СУБД, которая легко может обеспечить эти качества. Данная статья описывает её возможности вместе с ограничениями, которые могут быть неочевидными с первого взгляда и при этом стать серьезной проблемой в будущем, если их не учесть при проектировании системы.

Далее в статье некоторые факты будут отмечены специальными символами:

Особенности, которые могут быть очень полезными на практике.

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

Ограничения, которые могут стать большой проблемой, если вы их не учли на этапе проектирования.

Cosmos DB поддерживает несколько API: SQL, Cassandra, MongoDB, Gremlin, Table. Здесь под SQL подразумевается документ-ориентированный API, который раньше назывался DocumentDB, и он значительно отличается от привычных нам реляционных баз данных. Данная статья основана на опыте работы с SQL API. Хочу обратить внимание, что тип API выбирается при создании экземпляра хранилища, работать с данными одного и того же экземпляра через разные API не получится.

Документная модель Cosmos DB хранит данные в контейнерах (containers), состоящих из элементов (items). Ранее в документации они назывались коллекциями и документами соответственно. Все настройки масштабирования, пропускной способности, индексирования указываются на уровне контейнера (за некоторыми исключениями, о которых мы поговорим позже). База данных, по большому счету — именованное объединение контейнеров.

И здесь сразу стоит сказать о первом ограничении:

Максимальный размер элемента в контейнере — 2 Мб. Не должно быть проблемой в большинстве случаев, но следует помнить о том, что размер ограничен и хранение медиа-контента в элементах контейнеров — не очень хорошая идея. Больше информации о ресурсных ограничениях можно найти здесь.

Масштабирование «на лету»


Cosmos DB позволяет управлять метрикой производительности (пропускной способностью) для каждого контейнера индивидуально.

Пропускная способность измеряется в единицах запроса в секунду (Request Units per second или сокращенно RU/sec). Примерным эквивалентом единицы запроса можно считать чтение элемента размером 1 Кб по его идентификатору. Например, необходимая пропускная способность контейнера, позволяющего делать 500 чтений и 100 записей однокилобайтных элементов в секунду будет примерно равна 500 * 1 + 100 * 5 = 1000 RU. Однако, в общем случае, с точностью расчитать требуемую пропускную способность пратически невозможно, так как сложность запросов и размеры элементов могут быть очень разными.

Любая операция, выполняемая базой данных имеет свою «цену» в терминах RU. Указанная на контейнере пропускная способность — это лимит, который Cosmos DB разрешает «потратить» в секунду. База отслеживает суммарную «цену» запросов в пределах секунды и если лимит уже исчерпан, последующие запросы не принимаются к исполнению, пока сумма за секунду не вернется в значение меньше указанного лимита.

Минимально возможная величина пропускной способности для контейнера равна 400 RU и обойдется примерно в 25 долларов в месяц. Стоимость линейно растет с увеличением RU — контейнер с 800 RU будет уже стоить порядка 50 долларов в месяц.

Пропускная способность указывается на уровне контейнера и может быть изменена в любой момент без каких-либо дополнительных процедур.

Есть возможность указывать пропускную способность на уровне базы данных. В этом случае все контейнеры будут использовать одну и ту же «емкость».

Также есть режим «автопилота» (на данный момент в стадии превью), который позволяет автоматически увеличивать или уменьшать заявленную пропускную способность в зависимости от нагрузки на базу данных. Его недостатком является то, что минимальное и максимальное значения, которые вы можете сконфигурировать, находятся в пропорции 1:10, т.е. вы не сможете сделать так, что в пиковые часы метрика устанавливается в 40000 RU, а в покое — 400 RU.

В случае выхода за пределы заявленной пропускной способности, Cosmos DB просто не берет новые запросы на выполнение и в ответ возвращает специальный статус HTTP 429 («Request rate too large»).

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

Партиционирование для достижения бесконечной масштабируемости


У Cosmos DB раньше было две опции для контейнеров: с разделами (partitioned) и без (non-partitioned). Контейнеры без разделов были ограничены максимумом пропускной способности в 10000 RU и размером в 10 гигабайт. На данный момент все контейнеры могут иметь разделы, поэтому при их создании необходимо указывать ключ партиционирования.

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

Вся пропускная способность, выделенная на контейнер, равномерно распределяется среди его физических разделов. Представим, что изначально у контейнера с пропускной способностью 1000 RU один раздел. Когда его размер дойдет до предела в 10 Гб, система разделит его на две части. Это будет значить, что если изначально любой запрос, выполняемый на этом контейнере, имел в своем распоряжении 1000 RU в секунду, то теперь запросы, относящиеся к одному и тому же разделу, уже будут ограничены пропускной способностью в 500 RU.

Хоть количество разделов и не ограничено, максимальный объем данных для одного физического раздела — 10 Гб и максимальная пропускная способность — 10000 RU. Ключ партиционирования должен быть выбран так, чтобы вероятность достижения этих лимитов была минимальной. Следует отметить, что один и тот же логический раздел не может быть разделен между несколькими физическими.

Автоиндексация


Вместо создания индексов для отдельных полей или их сочетаний, Cosmos DB позволяет настроить политику индексирования для путей внутри объекта. Политика представляет собой набор атрибутов: какие пути включить в индексацию, какие исключить, какие типы индексов использовать и т.д.

Интересным моментом является то, что в отличие от большинства других СУБД, Cosmos DB использует инвертированный индекс (inverted index) вместо классического B-дерева (B-tree), что делает его очень эффективным при поиске по нескольким критериям и не требует создания составных индексов для поиска по нескольким полям. Больше деталей на эту тему можно найти в статье по этой ссылке.

Политика индексирования может быть изменена в любой момент.

Доступны два режима индексирования: целостный (consistent) и ленивый (lazy). Ленивый делает запись более быстрой, но вредит согласованности записи и чтения, потому что в этом случае индексирование происходит в фоновом режиме после того, как операция записи завершилась. Пока индекс обновляется, запросы могут возвращать неактуальные данные.

Можно создавать составные индексы для ускорения операции ORDER BY по нескольким полям, в остальных случаях составные индексы бесполезны.

Есть поддержка пространственных индексов (spatial indices).

Политика индексирования, создаваемая на контейнере по умолчанию («индексировать все поля») может вызывать большое потребление RU при записи элементов с большим количеством полей.

Отслеживание изменений


Отслеживание изменений в Cosmos DB возможно благодаря механизму, который называется Change Feed. Он возвращает документы, измененные с определенного момента времени, в том порядке, в котором они были изменены. Этим начальным моментом времени можно гибко управлять: им может быть либо момент инициализации самого потока изменений, либо фиксированная временная метка, либо момент создания контейнера.

Изменения могут быть обработаны асинхронно и инкрементально, а результат может быть распределен между одним и более потребителями для параллельной обработки. Это все дает очень большую гибкость в различных интеграционных сценариях.

Если используете низкоуровневый API для Change Feed, не забудьте учесть разбиение разделов (partition split), которое может происходить при достижении разделом предельного размера.

Этот механизм не отслеживает удаления, поэтому лучше использовать мягкое удаление (soft delete) для того, чтобы иметь возможность реагировать на удаления элементов.

Хранимые процедуры и транзакции


Cosmos DB поддерживает хранимые процедуры и триггеры, написанные на JavaScript.

Операции ввода-вывода в JavaScript полностью асинхронные и так как async/await еще не поддерживается в Cosmos DB, придется писать немалое количество коллбеков, что не сильно способствует читабельности кода.

Нет удобных способов возврата дополнительной информации в ошибках из хранимых процедур. Единственный способ решить проблему — добавить информацию в сообщение об ошибке, а затем на клиенте «вырезать» эти данные из сообщения.

Транзакции работают только в пределах хранимых процедур и функций или транзакционных пакетов, которые доступны начиная с .NET SDK версии 3.4 и выше.

Только документы из одного и того же логического раздела могут быть включены в одну транзакцию. Соответственно, операции записи в разные контейнеры не могут быть выполнены транзакционно.

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

Выполнение запросов


Хоть Microsoft и называет документный API “SQL”, его язык всего лишь похож на SQL и имеет много отличий от того, что мы привыкли видеть в реляционных базах.

В API есть параметры, которые помогают предотвратить выполнение «дорогих» запросов: EnableCrossPartitionQuery, EnableScanInQuery (смотрите класс FeedOptions в документации). Запросы, затрагивающие несколько разделов (cross-partition queries) получаются, когда условие не содержит фиксированного значения для ключа партиционирования. Сканирование набора данных может происходить, когда условие запроса содержит не-индексированные поля. Установка обоих параметров в false — хороший способ борьбы с черезмерным потреблением RU. Тем не менее, в некоторых случаях, выполнение запроса на нескольких разделах может оказаться полезным.

Оператор GROUP BY уже поддерживается (был добавлен в ноябре 2019 года).

Агрегатные функции MIN и MAX судя по всему не используют индексы.

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

Так как Cosmos DB не подразумевает строгой схемы данных, указанного в условии поля может и не существовать в некоторых элементах. Поэтому любой запрос, накладывающий условия на опциональные поля, должен также содержать проверку их существования через функцию IS_DEFINED. Проверки на NULL может быть недостаточно.

Больше подсказок на тему запросов можно найти в шпаргалках, опубликованных Microsoft.

Другие полезные возможности


  • Время жизни элементов — может быть указано либо по умолчанию на уровне контейнера, либо приложение может установить время жизни для каждого элемента индивидуально.
  • Пять уровней целостности: bounded staleness, session (по умолчанию), consistent prefix, eventual.
  • Уникальные ключи (обратите внимание, что для изменения структуры уникального ключа придется пересоздать контейнер).
  • Оптимистическая блокировка — каждый элемент имеет специальное поле "_etag", обновляемое самой СУБД. Код приложения во время обновления элемента может указать условие — разрешить запись только в случае если значение поле "_etag" в объекте, переданном приложением, равно значению, сохраненному для этого элемента в базе.
  • Гео-репликация — очень легко настраивается на любое количество регионов. В конфигурации с одним «мастером» в любой момент можно переключить основной регион, или это переключение будет автоматическим в случае сбоя на уровне датацентра. Клиент из SDK автоматически реагирует на эти переключения, не требуя от разработчиков никаких дополнительных действий для обработки подобных ситуаций.
  • Шифрование данных
  • Запись в нескольких регионах — позволяет масштабировать операции записи. Следует помнить, что наличие нескольких копий данных, разрешающих запись, практически всегда подразумевает возможные конфликты: один и тот же элемент изменён в двух регионах одновременно, несколько элементов с одинаковым первичным ключом добавлены в разных регионах и т.д. К счастью, Cosmos DB предоставляет два способа разрешения конфликтов: автоматический (последняя операция записи побеждает) или «свой вариант», в котором можно реализовать алгоритм, подходящий под ваши условия.

Как можно увидеть из описанного выше, Azure Cosmos DB имеет большое количество преимуществ, которые делают её хорошим выбором для различных проектов. Но нет ничего идеального, и некоторые ограничения могут быть серьезным препятствием для применения этой технологии: если вам нужны транзакции, состоящие из действий над несколькими контейнерами, или нужны длинные транзакции, включающие в себя сотни и тысячи объектов, если данные не могут быть эффективно партиционированы и при этом могут выйти за пределы 10 Гб и т.д. Если ни одно из ограничений, упомянутых в этой статье, не кажется большой проблемой для вашего проекта — имеет смысл рассмотреть Azure Cosmos DB.

Выражаю благодарность l0ndra за помощь в подготовке статьи.

P.S. Эту статью я уже публиковал ранее в английском варианте, но на другом ресурсе.