Як денормалізація даних працює з шаблоном мікросервісу?


77

Я щойно прочитав статтю про мікросервіси та архітектуру PaaS . У цій статті, приблизно на третині шляху вниз, автор стверджує (під Денормалізуй як Шалений ):

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

Хоча це теоретично звучить чудово, на практиці це має кілька серйозних перешкод, які потрібно подолати. Найбільший з них полягає в тому, що часто бази даних тісно пов’язані, і кожна таблиця має певний зв’язок із зовнішнім ключем принаймні з однією іншою таблицею. Через це може бути неможливо розділити базу даних на n підбаз даних, керованих n microservices.

Тож я запитую: з огляду на базу даних, яка повністю складається із пов’язаних таблиць, як можна денормалізувати її на менші фрагменти (групи таблиць), щоб фрагменти могли контролюватися окремими мікросервісами?

Наприклад, враховуючи таку (досить невелику, але прикладну) базу даних:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime
user_id

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
product_id
order_id
quantity_ordered

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

  1. UserService- для користувачів CRUDding в системі; в кінцевому рахунку повинен керувати [users]таблицею; і
  2. ProductService- для продуктів CRUDding в системі; в кінцевому рахунку повинен керувати [products]таблицею; і
  3. OrderService- для CRUDding замовлень у системі; в кінцевому рахунку слід керувати таблицями [orders]та[products_x_orders]

Однак усі ці таблиці мають взаємозв'язок між зовнішніми ключами. Якщо ми денормалізуємо їх і поводимося з ними як з монолітами, вони втрачають все своє семантичне значення:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
quantity_ordered

Тепер неможливо дізнатись, хто що замовляв, у якій кількості або коли.

Тож чи є ця стаття типовим академічним шумом, чи існує реальна практичність цього підходу до денормалізації, і якщо так, то як це виглядає (бонусні бали за використання мого прикладу у відповіді)?


WRT "денормалізувати як божевільний". . . Чому? Я не побачив жодної конкретної аргументації в статті.
Mike Sherrill 'Cat Recall'

21
Чи проходили ви вирішення цієї проблеми? Здається, це одна з проблем, яких найбільше уникають усі, хто натискає на мікросервіси.
код

Привіт, @ ccit-spence - будь ласка, перегляньте мою відповідь і повідомте мені, що ви думаєте. Мені довелося розробляти це рішення самостійно, і воно вже кілька місяців чудово працює, але цікавить, що про нього думають інші розробники.
smeeb

1
Можливо, варто зазначити, що стаття посилається на БД, яка навіть не підтримує обмеження зовнішнього ключа (так що для мене це показник того, що автор не надає значення обмеженням зовнішнього ключа - можливо, навіть не знає, що було втрачено? ).
Роб Байгрейв

Відповіді:


35

Це суб’єктивно, але наступне рішення спрацювало для мене, моєї команди та нашої команди БД.

  • На рівні програми мікросервіси розкладаються на семантичну функцію.
    • наприклад, Contactсервіс може мати CRUD-контакти (метадані про контакти: імена, телефони, контактна інформація тощо)
    • наприклад, Userсервіс може користуватися CRUD-користувачами, які мають облікові дані, ролі авторизації тощо.
    • наприклад, Paymentсервіс може здійснювати CRUD-платежі та працювати під капотом із сторонніми послугами, сумісними з PCI, такими як Stripe тощо.
  • На рівні БД таблиці можуть бути впорядковані, однак розробники / БД / віддані люди хочуть, щоб таблиці були організовані

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

interface PaymentService {
    PaymentInfo makePayment(User user, Payment payment);
}

Змоделюйте це так:

interface PaymentService {
    PaymentInfo makePayment(Long userId, Payment payment);
}

Таким чином, на об’єкти, що належать лише до інших мікросервісів, посилаються у певній службі за допомогою ідентифікатора, а не за посиланням на об’єкт. Це дозволяє таблицям DB мати зовнішні ключі всюди, але на рівні програми "іноземні" сутності (тобто сутності, що живуть в інших службах) доступні через ідентифікатор. Це зупиняє вихід каскаду об’єктів з-під контролю і чітко окреслює межі обслуговування.

Проблема, яка вона виникає, полягає в тому, що вона вимагає більше мережевих дзвінків. Наприклад, якби я дав посилання кожному Paymentсуб’єкту User, я міг би отримати користувача за певний платіж одним викликом:

User user = paymentService.getUserForPayment(payment);

Але використовуючи те, що я пропоную тут, вам знадобляться два дзвінки:

Long userId = paymentService.getPayment(payment).getUserId();
User user = userService.getUserById(userId);

Це може порушити угоду. Але якщо ви розумні та впроваджуєте кешування та впроваджуєте добре спроектовані мікросервіси, які відповідають на 50 - 100 мс на кожен дзвінок, я не сумніваюся, що ці додаткові мережеві дзвінки можуть бути створені, щоб не викликати затримки програми.


1
Чи всі служби прив'язані до однієї бази даних? У нашому випадку кожна служба є автономною службою на своєму екземплярі сервера. Кожна служба має спеціальну базу даних для цієї служби.
код

7
Зовнішні ключі не додають продуктивності. Індекси - це те, що додає продуктивності. Але індекси у FK-подібному стовпці можуть бути створені в будь-якій схемі, не обов'язково однаковій. Наприклад: Ordersтаблиця може жити у власній схемі та мати індексований user_idстовпець, який не є "істинним" FK, а просто ідентифікатором користувача, отриманого з Usersмікросервісу, тоді як usersтаблиця живе у власній схемі. Втрати продуктивності майже не спостерігається, але я все ще не можу зрозуміти, як можна досягти певної фільтрації / групування. Наприклад: знайти всіх користувачів, які мають замовлення, у яких товар має ціну> 100.
Руслан Стельмаченко

1
Але що я справді хочу сказати: якщо ви вже використовуєте подібні мікросервіси, вам не потрібно, щоб таблиці були в одній БД з "справжніми" FK. Вони можуть жити у своїй власній БД. Вони просто повинні мати індекси на "підроблених" стовпцях FK. Ви вже не можете використовувати JOIN через мікросервіси, тому нічого не втрачаєте, якщо розділити БД на менші БД.
Руслан Стельмаченко

1
Але що, якби я створив сутність із FK, який не існує, наприклад, замовлення з посиланням на неіснуючого замовника. Якщо я хочу певної послідовності, мені доведеться виконати деякі перевірки з посиланням на інші мікросервіси ні?
cecemel

2
"Ви вже не можете використовувати JOIN з-за мікросервісів ..." ... Я думаю, це схоже на те, що ми йдемо від планувальника запитів до бази даних (оптимізатор на основі витрат). Тобто, розбиття на безліч дрібних БД означає, що ми втрачаємо переваги оптимізатора витрат і тепер впроваджуємо "JOINS" через rest / rpc тощо
Роб

19

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


Щоб переформатувати це для мікросервісів, ми можемо використовувати кілька стратегій:

Api Приєднуйся

У цій стратегії зовнішні ключі між мікросервісами ламаються, і мікросервіс відкриває кінцеву точку, яка імітує цей ключ. Наприклад: Мікросервіс продукту відкриє findProductByIdкінцеву точку. Замовити мікросервіс може використовувати цю кінцеву точку замість приєднання.

введіть тут опис зображення У нього є очевидний мінус. Це повільніше.

Перегляди лише для читання

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

Високопродуктивне читання

Можна досягти високої продуктивності читання шляхом введення таких рішень, як redis / memcached поверх read only viewрозчину. Обидві сторони з'єднання слід скопіювати в плоску структуру, оптимізовану для читання. Ви можете запровадити абсолютно новий мікросервіс без стану, який можна використовувати для читання з цього сховища. Хоча це здається великим клопотом, варто зазначити, що воно буде мати вищу продуктивність, ніж монолітне рішення, поверх реляційної бази даних.


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


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

Так, що певною мірою поєднує читачів з продюсером, з яскравої сторони читачі можуть прочитати лише частину події і не дбати про всю інформацію. На практиці майже в кожному великому додатку вам знадобиться якийсь спільний стан між мікросервісами. Так само, як у прикладі. Замовлення має товар і користувача. Важко переробити цю справу без спільної інформації
Марцін Шимчак,

5

Я усвідомлюю, що це, можливо, не гарна відповідь, але яка біса. Ваше запитання було:

Враховуючи базу даних, яка повністю складається із пов’язаних таблиць, як денормалізувати це на менші фрагменти (групи таблиць)

WRT дизайн бази даних, я б сказав: "Ви не можете без видалення зовнішніх ключів" .

Тобто люди, які натискають Microservices з суворим правилом забороненої спільної використання даних, просять дизайнерів баз даних відмовитися від зовнішніх ключів (і вони роблять це неявно або явно). Коли вони явно не заявляють про втрату FK, це змушує вас задуматися, чи вони насправді знають і розпізнають цінність зовнішніх ключів (оскільки це часто взагалі не згадується).

Я бачив великі системи, розбиті на групи таблиць. У цих випадках між групами може не бути A) заборонених FK, або B) одна спеціальна група, яка містить "основні" таблиці, які FK можуть посилатись на таблиці в інших групах.

... але в цих системах "групи таблиць" часто складають 50+ таблиць, тому недостатньо малих для суворого дотримання мікропослуг.

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

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

РЕДАКТУВАТИ: (вартість ПРИЄДНАЙТЕСЯ через дзвінки REST)

Коли ми розділяємо БД, як пропонують мікросервіси, і видаляємо ФК, ми не лише втрачаємо примусове декларативне ділове правило (ФК), але ми також втрачаємо можливість для БД виконувати об’єднання (и) за цими межами.

У OLTP значення FK, як правило, не "UX Friendly", і ми часто хочемо приєднатися до них.

У прикладі, якщо ми отримуємо останні 100 замовлень, ми, мабуть, не хочемо показувати значення ідентифікатора клієнта в UX. Натомість нам потрібно зробити другий дзвінок клієнту, щоб отримати його ім’я. Однак, якщо ми також хотіли рядки замовлення, нам також потрібно зробити ще один дзвінок до служби товарів, щоб показати назву товару, номер товару тощо, а не ідентифікатор товару.

Загалом ми можемо виявити, що коли ми розбиваємо дизайн БД таким чином, нам потрібно зробити багато дзвінків "ПРИЄДНАЙТЕСЬ через REST". То яка відносна вартість цього?

Фактична історія: Приклад витрат на "ПРИЄДНАЙТЕСЬ через REST" та DB Joins

Є 4 мікросервіси, і вони включають багато "ПРИЄДНАЙТЕСЬ через REST". Орієнтовне навантаження для цих 4 послуг становить ~ 15 хвилин . Ці 4 мікросервіси, перетворені в 1 службу з 4 модулями проти спільної БД (що дозволяє об'єднання), виконують одне і те ж навантаження за ~ 20 секунд .

На жаль, це не є прямим порівнянням яблук із яблуками для об’єднань БД та “ПРИЄДНАННЯ через REST”, оскільки в цьому випадку ми також змінили базу даних NoSQL на Postgres.

Чи не дивно, що "ПРИЄДНАЙТЕСЬ через REST" працює порівняно погано у порівнянні з БД, яка має оптимізатор витрат тощо.

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


0

Я бачив би кожну мікросервіс як Об'єкт, і, як і будь-який ORM, Ви використовуєте ці об'єкти для витягування даних, а потім створюєте об'єднання в межах Вашого коду та колекцій запитів. Мікросервіси повинні оброблятися подібним чином. Різниця лише в цьому полягає в тому, що кожна мікрослужба представлятиме один об’єкт за раз, ніж повне дерево об’єктів. Рівень API повинен споживати ці послуги та моделювати дані таким чином, щоб вони були представлені або збережені.

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

@ ccit-spence, мені сподобався підхід до перехресних служб, але як його можна розробити та використовувати іншими службами? Я вірю, що це створить своєрідну залежність для інших служб.

Будь-які коментарі, будь ласка?


1
@ user1294787 Ви маєте рацію, можливість зчеплення існує. Зрештою повністю розв’язана система нічого не дасть. Послуги, які агрегуються, насправді не знають про службу, яка їх агрегує. Насправді у вас може бути багато служб, що пропонують агрегування для різних цілей. Якщо служба, що агрегується, більше не потрібна, тоді самі служби агрегування також більше не будуть потрібні.
код
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.