NoSQL
NoSQL — це парасолькова назва, яка окреслює чотири різні види баз даних через те, чим вони не є, — і саме тому вибір так часто роблять погано. Спільне в них корисніше за те, чого їм бракує: дані розміщуються на вузлі за ключем, який ви обираєте, тож саме цей ключ вирішує, які запити дешеві, які дорогі, а які неможливі. На цій сторінці — чотири сімейства, до чого насправді зобовʼязує ключ партиціювання, що означає «зрештою узгоджено» для ваших користувачів і коли реляційна база досі є правильною відповіддю.
- Не одне ціле
- Чотири сімейства
- Розміщення
- За обраним вами ключем
- Моделюють під
- Запити, а не сутності
Чотири сімейства й компроміс, на який пішли всі
Ця мітка накриває чотири моделі даних, які майже не мають між собою нічого спільного. Обрати не те сімейство — дорожча помилка, ніж обрати не той продукт усередині сімейства, і трапляється вона тому, що розмова зазвичай починається з «нам треба щось, що масштабується», а не з того, якої форми ваші дані.
Ключ-значення
Хеш-мапа, яку можна експлуатувати. Одна операція: дай значення за цим ключем. Сесії, кеші, лічильники частоти, прапорці функцій.
Документний
Сховище ключ-значення, здатне зазирнути всередину значення. Вкладені записи, які дістають цілими, з вторинними індексами на їхніх полях.
Ширококолонковий
Рядки, згруповані під ключем партиціювання й упорядковані всередині нього. Створений для величезних обсягів запису й діапазонних сканувань в одній партиції.
Графовий
Вузли й ребра як повноцінні сутності. Єдине сімейство, що відповідає на «друзі друзів друзів» без запиту, який росте з глибиною.
Перші три сімейства обʼєднує механізм, що дає їм масштабування, і його варто назвати прямо, бо він же є обмеженням. Кожен запис призначається вузлові через хешування ключа, який ви обрали, проєктуючи таблицю. Запит, що несе цей ключ, іде рівно до одного вузла й лишається швидким незалежно від того, скільки даних є загалом. Запит, що його не несе, мусить питати кожен вузол і зливати відповіді — або, у кількох рушіях, просто відхиляється.
Друге, що визначає ключ, — чи розподіляється навантаження взагалі. Хешування рівномірно розкидає ключі лише тоді, коли самі ключі різноманітні: партиціюйте за країною — і вузол із вашим найбільшим ринком візьме більшість трафіку, поки решта простоює; партиціюйте за днем — і кожен запис у системі падатиме на один вузол до півночі. Гаряча партиція не оголошує себе гарячою партицією: вона виглядає як повільність усього кластера, і додавання вузлів не допомагає, бо роботу ніколи не було поділено.
Чого коштує «зрештою узгоджено»
Ці системи тримають кілька копій кожного запису на різних вузлах — саме це дозволяє їм пережити смерть машини. Питання, на яке кожна з них має відповісти: скільки копій мають підтвердити запис, перш ніж клієнтові скажуть про успіх, і скільки треба прочитати, перш ніж повернути відповідь. Відповідь «усі» — і система перестає працювати, щойно один вузол недосяжний. Відповідь «одна» — і читання може цілком законно повернути вже застаріле значення. Більшість рушіїв дозволяє обирати це на кожну операцію, і цей вибір є продуктовим рішенням, вбраним у конфігураційний прапорець.
Відома формула — узгодженість, доступність, стійкість до розділення, оберіть два — правдива й майже некорисна, бо мережі розділяються рідко, а компроміс, з яким ви живете насправді, щоденний. Практичніша версія додає нормальний випадок: коли з мережею все гаразд, ви все одно обираєте між затримкою й узгодженістю на кожному окремому запиті. Дочекатися більшості реплік повільніше, ніж спитати найближчу, і ця різниця сплачується на кожному читанні, а не лише під час інциденту.
| Гарантія | Що бачить користувач | Чого коштує |
|---|---|---|
| Зрештою | Зміна може бути відсутньою на наступному екрані, а потім зʼявитися. Двоє користувачів можуть на мить розходитися. | Нічого додатково: це найдешевший і найшвидший режим і типовий у більшості рушіїв. |
| Читай свої записи | Ви завжди бачите власні зміни; чужі можуть відставати. | Спрямування читань користувача на репліку, яка взяла його запис, або читання з первинної деякий час. |
| Кворум | Читання відображає останній підтверджений запис, якщо читання й записи перетинаються в більшості. | Затримки на кожній операції й недоступності, коли досяжно замало реплік. |
| Транзакція на один елемент | Один документ чи рядок оновлюється атомарно; двоє — ні, якщо рушій не каже інакше. | Обмеження моделювання: усе, що має змінюватися разом, мусить жити в одному елементі. |
Моделюйте під запит, а не під сутність
Реляційне моделювання зберігає кожен факт один раз і збирає відповіді приєднаннями під час читання. Без приєднань цю інверсію доводиться робити під час проєктування: ви зберігаєте відповідь у тій формі, якої потребує екран, і погоджуєтеся зберігати деякі факти більше ніж один раз. Документ замовлення, що несе імʼя й адресу клієнта поруч із позиціями, — це не провал моделювання, це і є модель. Ціна в тому, що коли клієнт змінює імʼя, змінитися має кілька документів, і єдине, що знає, які саме, — ваш код.
Звідси корисне правило про те, що має лежати разом. Дублюйте значення, коли його читають значно частіше, ніж змінюють, і коли трохи застаріла копія прийнятна: назву товару в позиції замовлення, власне, і має бути заморожено. Тримайте посилання й платіть за друге звертання, коли значення часто змінюється або коли застарілість є дефектом. А все, що має змінюватися атомарно, мусить жити всередині одного елемента, бо це найбільша одиниця, яку більшість цих рушіїв оновить цілком.
Вторинні індекси існують, і саме тут маркетинг розходиться з механікою. Індекс, локальний для партиції, дешевий і узгоджений, бо живе поруч із даними, які індексує. Індекс, що охоплює партиції, доводиться підтримувати між вузлами, тож оновлюють його зазвичай асинхронно — отже, читання з нього може не побачити щойно записаного рядка, — і він повертає ту саму вартість, якої мав уникнути ключ партиціювання. Це справжній інструмент для другорядних шляхів доступу і поганий замінник ключа, що пасує основному.
Це правильна форма
- Патерн доступу відомий і вузький: діставати цю річ за цим ідентифікатором знову й знову.
- Обсяг запису справді більший за одну машину, і ключ розподіляє його рівномірно.
- Записи самодостатні: те, що читають разом, можна зберігати разом.
- Коротке вікно застарілості прийнятне для тих, хто користується продуктом.
Це реляційна задача
- Приходять нові питання, яких ніхто не передбачав, і кожному потрібен інший шлях доступу.
- Кілька записів мають змінитися разом або не змінитися зовсім: гроші, залишки, бронювання.
- Той самий факт потрібен із багатьох боків, тож дублювання множить способи, якими він розійдеться.
- Обсяг великий за відчуттям, а не за виміром, — а так буває здебільшого.
Як це виявляється в реальній поставці
«Безсхемність» — фраза, що коштує командам найдорожче, бо схема є завжди; питання лише в тому, чи вона записана й забезпечена, чи мається на увазі тим кодом, який останнім торкався колекції. Через рік у документному сховищі лежать три покоління форми запису: у якихось поле є, у якихось немає, у якихось воно містить інший тип. Далі кожен шлях читання має обробляти всі три, і ніщо не скаже вам, коли ви один пропустили. Команди, які лишаються при глузді, ставлять номер версії на кожен документ, валідують на записі за схемою, оголошеною в застосунку чи в рушії, і сприймають зміну форми як міграцію з планом.
Самі міграції змінюють характер. Немає виразу, який перепише колонку по всій таблиці, тож зміна форми стає фоновою задачею, що обходить кожен запис, — і вона має бути відновлюваною, обмеженою за швидкістю, щоб не заморити живий трафік, і безпечною для повторного запуску. Тим часом застосунок читає і стару, і нову форму, а отже, міграція не завершена, коли завершилася задача: вона завершена, коли прибрано код сумісності, — і саме цей крок ніхто не планує.
Нарешті — чесне порівняння. Сучасні реляційні рушії роблять більшість того, заради чого брали документне сховище: зберігають та індексують JSON, партиціюють таблиці, реплікуються, а одна первинна база тягне обсяги запису, значно більші за ті, що їх колись вироблятиме більшість продуктів. Експлуатувати одну базу замість двох — це заощаджена історія бекапів, історія моніторингу, режим збою й набір людей, які це розуміють. Беріть другий рушій, коли цього вимагає виміряна потреба — обсяг запису понад одну машину, обхід графа, кеш із бюджетом у мілісекунду, — і вмійте сказати, що саме з цього. «Нам, можливо, знадобиться масштабуватися» до цього переліку не входить.
Де це вироджується
- Вибір сімейства через розповідь про масштаб, а не через форму даних.
- Ключ партиціювання, обраний до того, як відомі запити: перший новий екран його й знецінює.
- Ключ низької потужності — країна, день, статус, — що концентрує трафік на одному вузлі.
- Запити «розкидай і збери», додані по одному, аж поки кожне читання не зачіпає весь кластер.
- Опора на «останній запис перемагає» у продукті, де двоє клієнтів справді можуть редагувати той самий запис.
- Ні версії документа, ні валідації на записі: три форми запису співіснують непоміченими.
- Перебудова приєднань у застосунку: та сама робота, тільки без планувальника й індексу.
- Друга база, взята заради однієї функції й далі експлуатована ніким конкретно.
Коли це застосовувати
Застосовуйте, коли
- Відомий вузький патерн доступу на обсязі, що справді перевищує одну машину: події, телеметрія, стрічки.
- Записи, які читають і пишуть цілими і які ні від чого не залежать: сесії, кошики, стан пристрою.
- Кеші, лічильники й черги, де сховище ключ-значення простіше й швидше за будь-яку альтернативу.
- Обхід звʼязків невідомої глибини — саме для цього й існує графовий рушій.
Уникайте, коли
- Усе, де кілька записів мають змінитися разом, — якщо рушій не дає справжніх транзакцій на кілька елементів.
- Звітність і довільний аналіз, де питання невідомі, а кожному потрібен інший шлях.
- Продукт, що ще відкриває власні патерни доступу: ключ, який треба обрати першим, — саме те, що ще рухається.
- Обсяг, який ви припустили, а не виміряли: реляційна база витримала б його спокійно.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.