Пропустити навігацію EPAM

Переваги та підводні камені Azure Cosmos DB

Юрій Івон

Senior Solution Architect
Думка експерта

Чимало сучасних баз даних прагнуть забезпечити високу продуктивність, масштабованість і доступність, при цьому мінімізуючи складність і вартість підтримки. 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»).

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

Поділ на розділи (partitioning) задля досягнення нескінченої масштабованості

В 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. Цю статтю я вже публікував раніше в англомовному варіанті, але на іншому ресурсі.

Підписатися на новини

Чудово! Ми вже готуємо добірку актуальних новин для вас :)

Вибачте, щось пішло не так. Будь ласка, спробуйте ще раз.

*Будь ласка, заповніть обов’язкові поля