РозробкаSenior

NoSQL

NoSQL — це парасолькова назва, яка окреслює чотири різні види баз даних через те, чим вони не є, — і саме тому вибір так часто роблять погано. Спільне в них корисніше за те, чого їм бракує: дані розміщуються на вузлі за ключем, який ви обираєте, тож саме цей ключ вирішує, які запити дешеві, які дорогі, а які неможливі. На цій сторінці — чотири сімейства, до чого насправді зобовʼязує ключ партиціювання, що означає «зрештою узгоджено» для ваших користувачів і коли реляційна база досі є правильною відповіддю.

Не одне ціле
Чотири сімейства
Розміщення
За обраним вами ключем
Моделюють під
Запити, а не сутності

Чотири сімейства й компроміс, на який пішли всі

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

  • Ключ-значення

    Хеш-мапа, яку можна експлуатувати. Одна операція: дай значення за цим ключем. Сесії, кеші, лічильники частоти, прапорці функцій.

  • Документний

    Сховище ключ-значення, здатне зазирнути всередину значення. Вкладені записи, які дістають цілими, з вторинними індексами на їхніх полях.

  • Ширококолонковий

    Рядки, згруповані під ключем партиціювання й упорядковані всередині нього. Створений для величезних обсягів запису й діапазонних сканувань в одній партиції.

  • Графовий

    Вузли й ребра як повноцінні сутності. Єдине сімейство, що відповідає на «друзі друзів друзів» без запиту, який росте з глибиною.

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

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

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

Чого коштує «зрештою узгоджено»

Ці системи тримають кілька копій кожного запису на різних вузлах — саме це дозволяє їм пережити смерть машини. Питання, на яке кожна з них має відповісти: скільки копій мають підтвердити запис, перш ніж клієнтові скажуть про успіх, і скільки треба прочитати, перш ніж повернути відповідь. Відповідь «усі» — і система перестає працювати, щойно один вузол недосяжний. Відповідь «одна» — і читання може цілком законно повернути вже застаріле значення. Більшість рушіїв дозволяє обирати це на кожну операцію, і цей вибір є продуктовим рішенням, вбраним у конфігураційний прапорець.

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

ГарантіяЩо бачить користувачЧого коштує
ЗрештоюЗміна може бути відсутньою на наступному екрані, а потім зʼявитися. Двоє користувачів можуть на мить розходитися.Нічого додатково: це найдешевший і найшвидший режим і типовий у більшості рушіїв.
Читай свої записиВи завжди бачите власні зміни; чужі можуть відставати.Спрямування читань користувача на репліку, яка взяла його запис, або читання з первинної деякий час.
КворумЧитання відображає останній підтверджений запис, якщо читання й записи перетинаються в більшості.Затримки на кожній операції й недоступності, коли досяжно замало реплік.
Транзакція на один елементОдин документ чи рядок оновлюється атомарно; двоє — ні, якщо рушій не каже інакше.Обмеження моделювання: усе, що має змінюватися разом, мусить жити в одному елементі.

Моделюйте під запит, а не під сутність

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

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

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

Це правильна форма

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

Це реляційна задача

  • Приходять нові питання, яких ніхто не передбачав, і кожному потрібен інший шлях доступу.
  • Кілька записів мають змінитися разом або не змінитися зовсім: гроші, залишки, бронювання.
  • Той самий факт потрібен із багатьох боків, тож дублювання множить способи, якими він розійдеться.
  • Обсяг великий за відчуттям, а не за виміром, — а так буває здебільшого.

Як це виявляється в реальній поставці

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

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

Нарешті — чесне порівняння. Сучасні реляційні рушії роблять більшість того, заради чого брали документне сховище: зберігають та індексують JSON, партиціюють таблиці, реплікуються, а одна первинна база тягне обсяги запису, значно більші за ті, що їх колись вироблятиме більшість продуктів. Експлуатувати одну базу замість двох — це заощаджена історія бекапів, історія моніторингу, режим збою й набір людей, які це розуміють. Беріть другий рушій, коли цього вимагає виміряна потреба — обсяг запису понад одну машину, обхід графа, кеш із бюджетом у мілісекунду, — і вмійте сказати, що саме з цього. «Нам, можливо, знадобиться масштабуватися» до цього переліку не входить.

Де це вироджується

  • Вибір сімейства через розповідь про масштаб, а не через форму даних.
  • Ключ партиціювання, обраний до того, як відомі запити: перший новий екран його й знецінює.
  • Ключ низької потужності — країна, день, статус, — що концентрує трафік на одному вузлі.
  • Запити «розкидай і збери», додані по одному, аж поки кожне читання не зачіпає весь кластер.
  • Опора на «останній запис перемагає» у продукті, де двоє клієнтів справді можуть редагувати той самий запис.
  • Ні версії документа, ні валідації на записі: три форми запису співіснують непоміченими.
  • Перебудова приєднань у застосунку: та сама робота, тільки без планувальника й індексу.
  • Друга база, взята заради однієї функції й далі експлуатована ніким конкретно.

Коли це застосовувати

Застосовуйте, коли

  • Відомий вузький патерн доступу на обсязі, що справді перевищує одну машину: події, телеметрія, стрічки.
  • Записи, які читають і пишуть цілими і які ні від чого не залежать: сесії, кошики, стан пристрою.
  • Кеші, лічильники й черги, де сховище ключ-значення простіше й швидше за будь-яку альтернативу.
  • Обхід звʼязків невідомої глибини — саме для цього й існує графовий рушій.

Уникайте, коли

  • Усе, де кілька записів мають змінитися разом, — якщо рушій не дає справжніх транзакцій на кілька елементів.
  • Звітність і довільний аналіз, де питання невідомі, а кожному потрібен інший шлях.
  • Продукт, що ще відкриває власні патерни доступу: ключ, який треба обрати першим, — саме те, що ще рухається.
  • Обсяг, який ви припустили, а не виміряли: реляційна база витримала б його спокійно.

Було корисно?

Поділіться з тим, хто працює над тією ж задачею.