Порівнюємо продуктивність MongoDB та Cosmos DB в Azure
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті
Мене звати Юрій Івон, я співпрацюю з компанією EPAM як Senior Solution Architect. Багато проєктів зараз обирають Cosmos DB як базу даних, але не завжди цей вибір добре вмотивований. Мені багато доводилося працювати з цією технологією і я хотів би поділитись деяким досвідом, а саме показати, яку продуктивність можна очікувати від різних варіантів Cosmos DB і чи завжди вона виграє у звичайної MongoDB. Окрім цього, поділюся своєю утилітою для проведення тестів продуктивності баз даних — Database Benchmark, яка може допомогти у виборі технології для ваших проєктів.
Коли треба зберігати інформацію в документо-орієнтованій базі даних, перша технологія, що спадає на думку — це MongoDB. Зараз уже є багато альтернатив, але коли ми працюємо в хмарному середовищі, вибір зазвичай обмежений platform-as-a-service рішеннями, тому що DevOps підтримка баз, що задеплоєні на віртуальних машинах, видається значно дорожчою. Azure пропонує дуже цікаве рішення для баз даних — Cosmos DB, яке, окрім свого SQL API, підтримує MongoDB API та ще декілька інших, таких як Cassandra або Gremlin.
Крім цього, Cosmos DB має ще декілька потужних можливостей: висока доступність, дуже просте керування георозподілом, можливість запису в декількох регіонах одночасно, автоматичне індексування тощо. З усіма цими фактами, Cosmos DB API for MongoDB виглядає дуже привабливо: з одного боку, ви отримуєте всі переваги Cosmos DB, з іншого — ваше рішення залишається незалежним від конкретних хмарних провайдерів. Чи можуть бути якісь непередбачувані проблеми з таким підходом?
Одне з найважливіших питань, яке тут треба розглянути — це продуктивність. Неможливо без реальних тестів зрозуміти, наскільки швидка Cosmos DB API for MongoDB у порівнянні зі «справжньою» MongoDB чи Cosmos DB SQL API. І щоб краще зрозуміти цей аспект, зробимо декілька тестів на основі моєї утиліти Database Benchmark для наступних варіантів баз даних:
- Cosmos DB API for MongoDB 4.0 — цю версію Azure пропонує за замовчуванням, коли ви створюєте новий аккаунт Cosmos DB API for MongoDB.
- Cosmos DB API for MongoDB 3.6 — багато проєктів усе ще використовують цю версію, і я хочу зрозуміти, чи вплине на швидкість виконання запитів перехід на наступну.
- Cosmos DB SQL API
- MongoDB 4.2 на віртуальній машині під Linux — старіша версія обрана навмисно, щоб уникнути скарг про нечесне порівняння. Водночас ви маєте розуміти, що Cosmos DB і звичайний MongoDB усередині дуже різні, і версія тут просто вказує рівень сумісності API.
В усіх тестах будемо використовувати набір даних, який містить п’ять мільйонів рядків.
Запити:
- Запит сторінки з діапазоном дат в умові — повертає перші 100 документів, відсортовані за датою замовлення та відфільтровані за випадковою країною, випадковим діапазоном дати замовлення та фіксованими критеріями для деяких інших полів.
- Запит «прочитати все» з діапазоном дат в умові — такий самий запит, як і попередній, але без сортування і обмеження кількості документів.
- Запит сторінки без діапазону дат в умові — повертає перші 100 документів, відсортовані за датою замовлення та відфільтровані за випадковою країною, випадковим типом товару та фіксованими критеріями для деяких інших полів.
- Запит «прочитати все» без діапазону дат в умові — такий самий запит, як і попередній, але без сортування і обмеження кількості документів.
- Запит з агрегацією — фільтрує набір даних за випадковою країною, випадковим діапазоном дати замовлення і групує його за типом товару для того, щоб розрахувати кількість документів та загальну кількість проданих одиниць для кожної з груп.
Умови тесту
- Всі ресурси, які є в тесті, належать до одного регіону Azure (East US).
- В тесті «справжньої» MongoDB, сервер бази даних і тестова утиліта належать до окремих віртуальних машин.
- Обидві віртуальні машини мають форму D2s v3 (2 ядра, 8 GB RAM).
- В усіх сценаріях з Cosmos DB, контейнер даних має тільки один розділ з максимально можливою пропускною здатністю в 10000 RU. Я поділюсь деякими думками на тему розділів наприкінці статті.
- Сценарії для кожного запиту складаються з трьох запусків: 1, 10 і 20 паралельних потоків. Кожен запит виконується 500 разів на кожному з потоків.
- Тестова утиліта використовує найновіші версії клієнтських .NET бібліотек для баз даних, що беруть участь у цьому дослідженні. Жодних змін до їхніх налаштувань за замовченням не зроблено.
Індекси
Cosmos DB і звичайна MongoDB побудовані на фундаментально різних механізмах індексації, тому є сенс пояснити деякі речі перед тим, як перейти до результатів.
Припустимо, що в нас є три поля, які ми використовуємо для фільтрації: Country, ItemType та OrderDate. Щоб покращити швидкість наших запитів ми створили три індекси — по індексу на кожне поле.
Звичайна MongoDB в основному використовує традиційні B-tree індекси, тобто якщо ми маємо запит з усіма трьома полями в умові, насправді тільки один індекс може бути задіяний, а всі інші предикати буде застосовано під час сканування індексу. Наприклад, колекція може містити мільйон документів для Великобританії, три мільйони з типом продукту «Одяг» і два мільйони — для вказаного діапазону дат, але тільки десять тисяч — для такої комбінації предикатів. З традиційним B-tree індексом база буде сканувати щонайменше мільйон документів щоб знайти всі відповідні результати. Або може навіть вирішити, що дешевше буде не використовувати індекси, а профільтрувати всю колекцію документів безпосередньо.
Ви завжди можете створити складений індекс для найчастіших комбінацій предикатів (наприклад, Country та ItemType), який значно покращить продуктивність у випадку, коли всі його компоненти є в умові запиту. Однак, створення складених індексів для усіх можливих комбінацій є непрактичним. Зазвичай додають тільки декілька, щоб покрити найтиповіші сценарії.
Cosmos DB, незалежно від зовнішнього API, використовує інвертовані індекси. На високому рівні, інвертований індекс можна уявити як словник, який містить список посилань на відповідні документи для кожної комбінації шляху до поля та його значення (мається на увазі саме шлях, а не просто ім’я, тому що документи можуть містити вкладені об’єкти). Якщо ми проіндексуємо всі три поля, і потім зробимо запит з усіма ними в умові, база зможе знайти списки посилань на документи для всіх вказаних пар шлях-значення і отримати їх перетин. На низькому рівні є багато оптимізацій цього механізму, які роблять інвертовані індекси дуже ефективними в обробці різноманітних комбінацій предикатів в запитах.
Як ви бачите, такий підхід не потребує складених індексів для прискорення пошуку відповідних документів, і будь-яка комбінація наших предикатів може бути покрита включенням всіх трьох до індексу. Тим не менш, Cosmos DB має складені індекси, але для інших цілей — для підтримки сортування по декількох полях. Ми не будемо експериментувати з ними в цих тестах.
Cosmos DB також підтримує «шаблонні» індекси, які дозволяють вказувати не конкретні поля, а шаблони шляхів до полів. Особливим випадком тут є шаблон, якому відповідає будь-яке поле в документі — я надалі буду називати його «повним індексом». З одного боку, він значно дорожчий для змін в даних, ніж «вибіркові» індекси, але, з іншого боку, може прискорити запити незалежно від їх критеріїв.
Для усіх тестів в цій статті я буду дотримуватись простого підходу до індексування:
- Для будь-якого варіанту Cosmos DB, тестуватись будуть і повний і вибірковий індекси.
- Для кожного запиту в звичайній MongoDB я створю складений індекс, який, на мою думку, найкраще підходить до сценарію і водночас не є надмірним.
Результати
Запит сторінки з діапазоном дат в умові
Цей запит має такий SQL-еквівалент:
SELECT * FROM Sales WHERE Sales.Country = @Country AND Sales.OrderDate >= @OrderDateFrom AND Sales.OrderDate < @OrderDateTo AND Sales.SalesChannel = @SalesChannel AND Sales.UnitsSold > @UnitsSoldFrom ORDER BY Sales.OrderDate DESC OFFSET 0 LIMIT 100
Параметри SalesChannel та UnitsSoldFrom фіксовані для всіх запусків, а Country, OrderDateFrom, та OrderDateTo — рандомізовані.
Зверніть увагу, що метрика «request charge» актуальна тільки для Cosmos DB, тому відповідні графіки не включають MongoDB встановлений на віртуальній машині. Більше інформації про те, що таке Request Unit (RU) можна знайти в моїй попередній статті.
На діаграмі вище можна побачити досить цікаві результати:
- MongoDB встановлена на дешевій віртуальній машині швидша за будь-який з варіантів Cosmos DB.
- Індекси на окремих полях дають кращу продуктивність, ніж повний індекс.
- Cosmos DB SQL API швидший за MongoDB API, особливо з індексами на окремих полях.
- Cosmos DB API for MongoDB 4.0 значно швидший, ніж 3.6 коли обидва використовують повний індекс, але з індексами на окремих полях обидві версії мають схожу продуктивність.
Запит «прочитати все» з діапазоном дат в умові
Основне призначення цього запиту — перевірити як різні бази обробляють великі обсяги даних, отриманих в одному запиті — він повертає близько 2200 документів в середньому. Його SQL еквівалент такий:
SELECT * FROM Sales WHERE Sales.Country = @Country AND Sales.SalesChannel = @SalesChannel AND Sales.UnitsSold > @UnitsSoldFrom AND Sales.OrderDate >= @OrderDateFrom AND Sales.OrderDate < @OrderDateTo
Аналогічно попередньому запиту, параметри SalesChannel та UnitsSoldFrom фіксовані для всіх запусків, а Country, OrderDateFrom, та OrderDateTo — рандомізовані.
Тут ми маємо трохи іншу ситуацію:
- MongoDB встановлена на віртуальній машині все ще найшвидша. Я додав до результатів ще один запуск з більш потужною машиною для бази (D4s v3 що помножує ресурси вдвоє), щоб побачити наскільки добре вона піддається вертикальному масштабуванню. Таке невелике покращення може казати про те, що машина, яка виконує тести, теж вичерпує свої ресурси. Подальші перевірки показали, що це так, і збільшення потужності тестової машини покращує результати. Однак я вирішив не включати жодних результатів для інших конфігурацій тестової машини, щоб мати «спільний знаменник».
- Cosmos DB SQL API набагато швидший в однопоточному сценарії, але відстає від MongoDB API під більшим навантаженням, не зважаючи на менші витрати одиниць запитів (Request Units). Це можна пояснити більшою ефективністю MongoDB протоколу та його .NET клієнту у порівнянні з Cosmos DB. Чесно кажучи, порівняння продуктивності клієнтів в цьому тесті не зовсім справедливе, тому що MongoDB плагін десеріалізує в BsonDocument, у той час як Cosmos DB — в стандартний .NET Dictionary. Тим не менш, подальше профілювання показує, що навіть з серіалізацією в один і той же POCO клас, клієнт Cosmos DB витрачає на 28% більше часу процесора на десеріалізацію одного мільйона документів і при цьому виділяє в 6.5 разів більше пам’яті загалом. Зверніть увагу, що остання цифра — про всі виділення пам’яті, які відбуваються під час операції читання даних, а не про фінальний об’єм.
- У Cosmos DB API for MongoDB, повні індекси працюють ефективніше, ніж індекси на окремих полях. Cosmos DB SQL API витрачає менше одиниць запитів у випадку індексів на окремих полях, але реальні результати для обох варіантів індексу дуже схожі під великим навантаженням. Це може мати таке саме пояснення, як і попередній пункт.
- Cosmos DB API for MongoDB 4.0 трохи швидший за 3.6.
Запит сторінки без діапазону дат в умові
Цей запит має такий SQL-еквівалент:
SELECT * FROM Sales WHERE Sales.Country = @Country AND Sales.ItemType = @ItemType AND Sales.SalesChannel = @SalesChannel AND Sales.UnitsSold > @UnitsSoldFrom ORDER BY Sales.OrderDate DESC OFFSET 0 LIMIT 100
Параметри SalesChannel та UnitsSoldFrom фіксовані для всіх запусків, а Country та ItemType — рандомізовані.
Найбільша різниця у порівнянні з першим запитом полягає в поведінці Cosmos DB SQL API — він витрачає трохи більше одиниць запитів (Request Units) і є трохи повільнішим за будь-яку з версій Mongo DB API.
В іншому, результати відповідають тому, що ми бачили для першого запиту:
- MongoDB встановлена на дешевій віртуальній машині набагато швидша за будь-який варіант Cosmos DB при умові, що відповідні індекси були створені.
- Медіанна тривалість запитів для Cosmos DB SQL API майже не змінюється зі збільшенням кількості паралельних потоків, в той час як середня тривалість зростає.
- Індекси на окремих полях дають більшу продуктивність, ніж повний індекс.
- Cosmos DB API for MongoDB 4.0 трохи швидший за 3.6.
Запит «прочитати все» без діапазону дат в умові
Цей запит має такий SQL-еквівалент:
SELECT * FROM Sales WHERE Sales.Country = @Country AND Sales.ItemType = @ItemType AND Sales.SalesChannel = @SalesChannel AND Sales.UnitsSold > @UnitsSoldFrom
Параметри SalesChannel та UnitsSoldFrom фіксовані для всіх запусків, а Country та ItemType — рандомізовані.
Оскільки запит повертає близько 1000 документів у середньому, результати дуже схожі на попередній «прочитати все»:
- MongoDB встановлений на віртуальній машині — найшвидший, але з цим запитом тільки «подвійна» віртуальна машина виграє в усіх запусках.
- Cosmos DB SQL API значно швидший в однопоточному сценарії, але відстає від Mongo DB API під навантаженням, не дивлячись на менші витрати одиниць запитів (Request Units). Як я вже писав, це можна пояснити більшою ефективністю .NET клієнта MongoDB.
- Повні індекси дають вищу продуктивність, ніж індекси на окремих полях.
- Хоча Cosmos DB API for MongoDB 4.0 витрачає трохи менше одиниць запитів (Request Units), ніж 3.6, відносна продуктивність цих двох версій нестабільна протягом тесту.
Запит з агрегацією
Цей запит має такий SQL-еквівалент:
SELECT Sales.ItemType, COUNT(1) OrderCount, SUM(Sales.UnitsSold) TotalUnitsSold FROM Sales WHERE Sales.Country = @Country AND Sales.OrderDate >= @OrderDateFrom AND Sales.OrderDate < @OrderDateTo) GROUP BY Sales.ItemType
Зверніть увагу, що він не має ORDER BY, оскільки Cosmos DB SQL API не дозволяє мати ORDER BY та GROUP BY одночасно.
Параметри Country, OrderDateFrom, та OrderDateTo — рандомізовані.
Першим помітним результатом є падіння тесту Cosmos DB SQL API під час запусків в 20 потоків через перевищення відведеної пропускної здатності. Це може виглядати дивним, тому що метрика одиниць запиту (Request Units) нижча, ніж у декількох інших, які я вже тестував.
Коли загальна кількість одиниць запиту, витрачена паралельними запитами, перевищує відведену пропускну здатність (10000 RU на секунду в нашому випадку), база даних відповідає помилкою «Request rate too large». За замовченням, клієнтська бібліотека не кидає виняток одразу і повторює запит з деякою затримкою до 9 разів. І якщо дев’ята спроба дає таку саму помилку, клієнт кидає виняток. Кількість спроб, звісно, можна змінити, але ми використовуємо налаштування за замовченням.
Уявімо два запити, які витрачають однакову кількість RU, але мають різну тривалість. З них двох той, що займає менше часу, швидше вичерпає спроби і більш ймовірно викликає цю помилку. У порівнянні з усіма іншими запитами, раніше описаними в статті, варіант з агрегацією має найбільше співвідношення RU до секунд, тому ми і бачимо цю проблему.
Іншою важливою знахідкою є те, що Cosmos DB погано справляється з агрегаціями на великих наборах даних. Наприклад, виконання цієї агрегації без фільтру в запиті потребує близько 100000 RU з SQL API і близько 260000 — з Mongo DB API. Я помітив тільки одну оптимізацію під час перевірки сценаріїв з агрегацією — простий запит с «distinct» для поля Country по всій колекції даних бере тільки 140 RU в SQL API, в той час як Mongo DB API з’їдає близько 240000 RU (!).
Інші спостереження для цього випадка такі:
- MongoDB встановлена на дешевій віртуальній машині — швидша за будь-який з варіантів Cosmos DB.
- Cosmos DB SQL API швидший за MongoDB API.
- Медіанна тривалість запитів для Cosmos DB SQL API майже не змінюється зі збільшенням кількості паралельних потоків, в той час як середня тривалість зростає.
- Майже немає різниці між індексами на окремих полях і повним індексом.
- Cosmos DB API for MongoDB 4.0 трохи швидший за 3.6.
Висновки
Наступна таблиця підсумовує найкращі результати часу виконання запитів в усіх тестах (в мілісекундах):
А ця таблиця — найкращі показники пропускної здатності (в запитах на секунду):
🔹MongoDB, встановлена на віртуальній машині, набагато швидша за будь-який варіант Cosmos DB, але це не означає, що її треба завжди використовувати. Є велика різниця в складності DevOps-підтримки між базою даних на віртуальних машинах і platform-as-a-service сервісом, таким як Cosmos DB. Якщо ви підете шляхом віртуальних машин, вашій DevOps-команді треба буде реалізовувати високу доступність, резервне копіювання, та масштабування на низькому рівні, а ще піклуватись про патчі безпеки. Бізнес може вимагати, щоб масштабування та встановлення патчів проходили без відключення всієї системи, що робить ситуацію ще складнішою.
Cosmos DB робить все це «під капотом» і дає адміністраторам просте керування цими функціями. MongoDB Atlas є альтернативним варіантом використання повноцінної MongoDB в Azure, але в мене немає з ним досвіду. Окрім набагато простішої реалізації та підтримки, Cosmos DB дає можливості, яких просто немає в звичайній MongoDB — запис в декількох регіонах одночасно, шаблонні індекси (повний індекс, який брав участь у тестах, є прикладом шаблонного). Немає універсального рецепту, тобто треба обирати технології з урахуванням потреб та обмежень бізнесу, беручі до уваги можливості, характеристики та обмеження технологій.
🔹Cosmos DB SQL API швидший за Cosmos DB API for MongoDB, але з деякими застереженнями. Якщо ваші запити повертають мегабайти даних за один раз, SQL API може працювати під навантаженням гірше за MongoDB API. Швидке дослідження показує, що .NET клієнт Cosmos DB витрачає більше процесорного часу і пам’яті для обробки такого самого набору документів. Запит сторінки без діапазону дат в умові працював трохи гірше з SQL API в паралельних сценаріях, але різниця не була настільки великою, щоб турбуватись.
🔹Cosmos DB погано справляється з агрегаціями на великих наборах даних. Не сподівайтесь реалізувати фасетний пошук на базі Cosmos DB, якщо ваша колекція буде містити більше, ніж декілька тисяч документів. Простий запит унікальних значень (distinct) схоже, що оптимізований в SQL API, в той час як MongoDB API показує дуже погані результати в цьому випадку. Існує спосіб вирішувати аналітичні задачі з даними в Cosmos DB, але це зовсім інша історія — Cosmos DB Analytical Store.
🔹Якщо продуктивність важлива для нового проєкту, я би не розглядав Cosmos DB API for MongoDB. Якщо є необхідність уникнути залежності від конкретного постачальника хмарних сервісів, я би абстрагував доступ до даних таким чином, щоб перехід на іншу технологію бази даних потребував змін тільки в невеликій частині коду. Буде помилкою думати, що міграція з Cosmos DB API for MongoDB на звичайну MongoDB буде безшовною: оскільки ці дві технології використовують абсолютно різні механізми індексації, міграція індексів «як є» може бути неможливою.
Щоб бути впевненим, що все працює добре, вам в будь-якому випадку треба буде робити тести продуктивності й коригувати налаштування індексів в залежності від результатів. Якщо все добре спроєктовано, зміна централізованого коду «стратегії доступу до даних» буде невеликою частиною зусиль витрачених на міграцію. В той же час, Cosmos DB API for MongoDB може бути корисною у випадку «lift-and-shift» — коли рішення переноситься з on-premise в Azure. Але майте на увазі, що звичайна MongoDB має набагато більше можливостей, тому не всі рішення можуть бути переключені на Cosmos DB API без змін в коді.
🔹Cosmos DB API for MongoDB 4.0 швидша за 3.6. Метрика одиниць запитів (Request Units) завжди краща у 4.0, і хоча є випадки коли 3.6 дає кращий час або кращу пропускну здатність, вони не є стабільно відтворюваними. Таким чином, має сенс запланувати перехід на нову версію, якщо ви все ще використовуєте 3.6. Досить важко передбачити ступінь покращення, адже він залежить від конкретних запитів, але в цьому тесті різниця коливалася від 0 до 50 відсотків.
🔹 Коли запит містить ORDER BY, індекси на окремих полях працюють краще, ніж повний індекс. А коли сортування не вказано, ми бачимо протилежне. Не занурюючись у можливі пояснення, хочу сказати, що рекомендовано мінімізувати розмір індексу, якщо ваша система не залежить безпосередньо від шаблонних індексів. Типова ситуація, в якій шаблонні індекси можуть допомогти — коли документи в базі мають динамічну структуру, і система дає можливість фільтрувати по будь-яких полях.
🔹Стаття не покриває сценарії з багатьма розділами (partitioning), але я можу поділитись деякими думками на цю тему. Звичайна MongoDB може бути масштабована для читання або збільшенням кількості процесорів і оперативної пам’яті, або додаванням нових шардів і реплік, в той час як подальше масштабування Cosmos DB може бути досягнуто тільки додаванням нових розділів. Якщо б я розбив колекцію на логічні розділи за країнами, результати б не змінились, тому що фізичний розділ все одно був би тільки один. Нові фізичні розділи створюються автоматично, коли або існуючий досягає ліміту в 20 Гб, або коли нове значення пропускної здатності перетинає наступні 10000 RU. Наприклад, якщо в контейнері в 5 Гб та 10000 RU, буде тільки один фізичний розділ. Якщо ми дамо йому 10400 RU, то в ньому буде вже два фізичних розділи по 5200 RU кожен. Якщо ми дамо 30000 RU, то вже буде три фізичних розділи по 10000 RU. Кожні 10000 RU коштують близько 600 USD на місяць, в той час як базова віртуальна машина задіяна в цих тестах — тільки 70 USD на місяць.
Іншими словами — Cosmos DB легко масштабується, але це масштабування досить дороге. При великих обсягах, варіант з MongoDB на віртуальних машинах може виявитись дешевшим, навіть враховуючи додаткові витрати на DevOps. MongoDB Atlas теж може дати можливість знизити витрати при збереженні можливостей легкого масштабування.
🔹Хоча будь-які тести аналогічні цьому дають ідеї на тему того, що очікувати і на що звертати увагу, я рекомендую виконувати тести запитів, специфічних до вашого проєкту, перед тим як робити технологічне рішення. Моя утиліта Database Benchmark робить такі тести набагато простішими у порівнянні з написанням тестових програм з нуля.
Це переклад моєї статті з Medium.
28 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів