🏗️ Розділ 17 · Питання #13

Що таке патерн Database per Service

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


🟢 Junior Level

Database per Service (База даних на мікросервіс) — це базовий архітектурний патерн мікросервісної архітектури, при якому кожен мікросервіс володіє своєю власною ізольованою базою даних.

[Order Service] ──────► [(Order DB)]
      ▲                        │
      │ REST / gRPC / Kafka    │ (Прямий доступ до чужої БД СУВОРО ЗАБОРОНЕНО!)
      ▼                        ▼
[User Service]  ──────► [(User DB)]

Головний закон патерну:

Жоден мікросервіс не має права напряму підключатися до бази даних іншого мікросервісу, читати з її таблиць або виконувати SQL-запити. Усі взаємодії між сервісами відбуваються лише через публічний API (REST, gRPC) або події в брокері повідомлень (Kafka, RabbitMQ).

Чому це необхідно:

  1. Слабка зв’язність (Loose Coupling): Команда сервісу User Service може змінити тип колонки phone або провести міграцію Liquibase/Flyway у будь-який момент без узгодження з іншими командами.
  2. Ізоляція збоїв (Fault Tolerance): Якщо база даних каталогу товарів упала через перевантаження, база даних оформлення замовлень та база платежів продовжують стабільно працювати.
  3. Головна плата за зручність: У мікросервісах неможливо зробити SQL JOIN між таблицями різних сервісів, а також втрачаються класичні ACID-транзакції.

🟡 Middle Level

1. Три рівні фізичної ізоляції даних

Рівень ізоляції Архітектура Переваги Недоліки / Ризики
1. Database Server per Service У кожного мікросервісу свій власний кластер (наприклад, окремий AWS RDS / PostgreSQL). 🟢 100% ізоляція CPU, RAM, дискового I/O та дискових збоїв. Висока фінансова вартість; складніше адмініструвати 50 кластерів.
2. Database Schema per Service Один спільний інстанс СУБД, але в кожного сервісу своя схема (orders_schema.*, users_schema.*). 🟢 Дешевше в обслуговуванні; простіше налаштовувати резервне копіювання. 🔴 Ефект Noisy Neighbor: важкий запит одного сервісу з’їдає весь CPU сервера БД.
3. Private Tables / Credentials Спільна схема, але різні користувачі СУБД із жорсткими GRANT лише на свої таблиці. Найнижчий поріг входу при розпилі моноліту. Ризик випадкового порушення ізоляції або витоку прав доступу.

2. Способи отримання даних без SQL JOIN

А. API Composition (Синхронний збір даних через BFF / Gateway)

Клієнт запитує сторінку замовлення. API Gateway або спеціалізований BFF паралельно робить запити:

  • GET /orders/101 $\to$ повертає { id: 101, userId: 42, amount: 500 }
  • GET /users/42 $\to$ повертає { id: 42, name: "Іван", email: "ivan@test.ua" } Шлюз об’єднує дві JSON-відповіді в один екран для фронтенду.

Б. Event-Driven Data Replication (Асинхронна денормалізація)

Сервіс замовлень підписується на топік user-events. При створенні або оновленні профілю користувача Order Service зберігає мінімально необхідний знімок даних користувача (userId, deliveryAddress, fullName) у свою локальну таблицю order_customers. Плюс: Order Service може миттєво відобразити замовлення локальним запитом без зовнішніх мережевих викликів.

В. CQRS (Command Query Responsibility Segregation)

Спеціальний Read-сервіс слухає події від усіх мікросервісів системи та формує денормалізовані вітрини даних у СУБД, оптимізованій під швидкий пошук та аналітику (Elasticsearch, ClickHouse, MongoDB).


🔴 Senior Level

1. Поліглотна персистентність (Polyglot Persistence)

Патерн Database per Service звільняє від «золотого молотка» єдиної реляційної СУБД. Кожен обмежений контекст (Bounded Context) обирає тип бази даних під свій профіль навантаження:

[User Service]      ──► PostgreSQL    (ACID-транзакції, сувора консистентність)
[Product Catalog]   ──► MongoDB       (Гнучка документна схема для категорій товарів)
[Shopping Cart]     ──► Redis         (Кешування в оперативній пам'яті з TTL)
[Product Search]    ──► Elasticsearch (Повнотекстовий пошук, фасети, Typo-Tolerance)
[Fraud Detection]   ──► Neo4j         (Графова база даних для виявлення мереж дропів)
[Analytics / BI]    ──► ClickHouse    (Колонкова СУБД для аналітики мільярдів подій)

2. Проблема узгодженості та дрейфу даних (Data Drift)

При асинхронній денормалізації даних (коли Order Service кешує копію адреси користувача) неминуче виникає Data Drift (рассинхронізація даних):

  • Подія оновлення адреси могла загубитися, застрягти в Dead Letter Queue або бути обробленою з помилкою.
  • Застосунок повинен бути готовий до Eventual Consistency (дані стануть консистентними через частки секунди).
  • Методи боротьби:
    1. Audit / Reconciliation Jobs: Періодичні нічні batch-процеси (або стрімінгове порівняння через CDC/Flink), що звіряють контрольні суми (hashes) даних між сервісами та публікують коригувальні події.
    2. Data Ownership: Чітке розмежування відповідальності: сервіс користувачів є єдиним джерелом істини (Single Source of Truth) для особистих даних; решта зберігають лише історичні зліпки (Immutable Snapshot на момент оформлення замовлення).

4 Tricky Questions

1. Як реалізувати видалення персональних даних користувача за законом (GDPR / “Право на забуття”), якщо його ID та дані репліковані у 20 незалежних мікросервісів?

Відповідь:

  1. Choreographed / Orchestrated Deletion Saga: User Service публікує подію UserDeletedEvent(userId). Усі 20 мікросервісів зобов’язані підписатися на цей топік і виконати очищення.
  2. Анонімізація замість фізичного DELETE: Сервіс замовлень і сервіс бухгалтерії за законом зобов’язані зберігати фінансову звітність 5 років. Вони не можуть видалити рядок замовлення. Тому персональні дані замінюються на псевдонім: name = "GDPR_REMOVED", address = "ANONYMIZED", phone = null.
  3. Crypto-Shredding (Криптографічне знищення): Персональні дані зберігаються зашифрованими індивідуальним ключем користувача. При видаленні користувача його ключ знищується в централізованому сховищі ключів (KMS). Після цього дані в усіх 20 базах даних стають нечитабельним шумом без необхідності їх фізичного вичищення.

2. Як уникнути проблеми $N+1$ мережевих запитів при синхронному API Composition (наприклад, при відображенні списку з 50 замовлень із даними покупців)?

Відповідь: Якщо для списку з 50 замовлень API Gateway зробить 1 запит в Order Service і потім у циклі 50 запитів в User Service (GET /users/{id}), час відповіді зросте катастрофічно. Рішення:

  • Batch API: Реалізація пакетного ендпоінта в User Service: POST /api/v1/users/batch-get з тілом {"userIds": [1, 2, ... 50]}, що повертає мапу користувачів за один мережевий виклик.
  • Локальна денормалізація: При створенні замовлення зберігати ім’я та телефон покупця прямо в документі замовлення, повністю усуваючи необхідність запиту до User Service при відображенні списку.
  • GraphQL DataLoader: Використання механізму батчингу та дедуплікації запитів на рівні GraphQL Gateway.

3. Що робити стартапу, якщо бізнес вимагає мікросервісну архітектуру, але бюджет не дозволяє оплачувати 30 незалежних серверів баз даних?

Відповідь: Використовувати патерн Logical Database per Service (Schema per Service):

  1. Розгорнути один високонадійний кластер PostgreSQL.
  2. Для кожного мікросервісу створити окрему логічну базу (CREATE DATABASE service_db) або схему (CREATE SCHEMA service_schema) з ізольованим користувачем і паролем.
  3. Категорично заборонити міжсхемні запити та JOIN на рівні прав доступу (REVOKE ALL ON ALL TABLES IN SCHEMA...).
  4. Впровадити ArchUnit тести або SQL-лінтери в CI/CD, що блокують звернення до чужих схем. Коли компанія виросте, будь-яку логічну схему можна винести на окремий фізичний сервер за допомогою pg_dump без переписування Java-коду.

4. Чому використання спільних представлень (Shared Views) або процедур між базами сервісів є прихованим антипатерном?

Відповідь: Shared Views створюють невидиму семантичну зв’язаність на рівні схеми СУБД. При зміні внутрішньої структури однієї таблиці падає представлення, про яке команда-розробник могла навіть не знати. Це ламає незалежність релізних циклів і CI/CD пайплайнів, перетворюючи мікросервіси на найгірший різновид архітектури — розподілений моноліт (Distributed Monolith).


🎯 Шпаргалка для інтерв’ю

Головне:

  • Database per Service — золоте правило мікросервісів: один сервіс $\to$ одна власна БД.
  • Жодного прямого доступу до чужої БД через SQL/JDBC. Доступ лише через публічні контракти (API або брокер повідомлень).
  • Плюси: Незалежність розробки, незалежні міграції схем, ізоляція відмов, можливість поліглотного зберігання даних (Polyglot Persistence).
  • Мінуси: Відсутність міжсервісних JOIN та ACID-транзакцій, необхідність переходу на Eventual Consistency та патерн Saga.
  • Отримання чужих даних: API Composition (синхронно через Gateway/BFF) або Event-Driven денормалізація (асинхронно через Kafka).
  • На ранніх етапах допустимий варіант Schema per Service на одному сервері СУБД із жорстким розділенням прав.

Червоні прапорці (Чого говорити НЕ МОЖНА):

  • ❌ «Сервіси можуть ходити в бази один одного в режимі Read-Only для прискорення вибірок» (Повністю ламає ізоляцію і робить незалежний деплой неможливим).
  • ❌ «Для об’єднання даних із різних сервісів ми використовуємо розподілені SQL JOIN у коді» (Призведе до перекачування терабайтів даних в оперативну пам’ять застосунку).
  • ❌ «Патерн Database per Service вимагає купівлі 50 окремих фізичних серверів із першого дня» (Логічний поділ схем на одному сервері повністю зберігає архітектурні межі при мінімальному бюджеті).

Пов’язані теми: