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

Які проблеми виникають при використанні спільної бази даних

Використання спільної бази даних перетворює систему на найгіршу архітектурну форму:


🟢 Junior Level

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

       ┌───────────────────┐       ┌────────────────────┐
       │   Order Service   │       │    User Service    │
       └─────────┬─────────┘       └─────────┬──────────┘
                 │                           │
                 └─────────────┬─────────────┘
                               ▼
                    ┌─────────────────────┐
                    │   SHARED DATABASE   │ ◄── ❌ АНТИПАТЕРН!
                    │ (users, orders, ...)│
                    └─────────────────────┘

Основні небезпеки для системи:

  1. Жорстка зв’язаність за структурою (Schema Coupling): Якщо команда сервісу користувачів вирішує перейменувати або видалити колонку phone, сервіс замовлень упаде при спробі прочитати цю колонку.
  2. Втрата незалежності релізів: Не можна просто так задеплоїти нову версію мікросервісу. Потрібен довгий процес синхронізації релізів між усіма командами.
  3. Єдина точка відмови (Single Point of Failure / Blast Radius): Якщо база даних падає через перевантаження або важкий SQL-запит одного сервісу, вся екосистема компанії зупиняється повністю.

🟡 Middle Level

1. Системні та технічні наслідки використання Shared DB

Проблема Фізичний механізм у СУБД Наслідки для продакшену
Connection Pool Starvation Аналітичний сервіс у піку відкриває 150 з’єднань для формування звіту. Сервіс авторизації не може отримати вільне з’єднання з пулу $\to$ масові помилки HTTP 503 Service Unavailable.
Lock Contention & Escalation Сервіс замовлень оновлює 50 000 рядків у межах довгої транзакції $\to$ СУБД ескалує блокування до рівня всієї таблиці (Table Lock). Сервіс користувачів зависає на читанні профілю SELECT ... WHERE id = 1.
Конфлікти міграцій (Flyway / Liquibase) Два сервіси одночасно запускають міграції при старті контейнерів у Kubernetes. Блокування таблиці flyway_schema_history, падіння деплою через Checksum mismatch або Deadlock detected.
Розмиття відповідальності (Lack of Data Ownership) П’ять сервісів пишуть у таблицю customers. Неможливо визначити, який саме баг змінив статус клієнта на FRAUD.

2. Приклад прихованої аварії в коді

// Сервіс А (Order Service) читає колонку 'discount':
@Entity
@Table(name = "users")
public class UserEntity {
    @Id
    private Long id;
    
    @Column(name = "discount")
    private BigDecimal discount;
}

// Сервіс Б (User Service) у новому спринті замінив знижку на систему лояльності:
// V5__drop_discount.sql:
// ALTER TABLE users DROP COLUMN discount;

// ПІДСУМОК: Сервіс Б успішно задеплоївся, а Сервіс А через 5 хвилин падає з фатальним
// org.postgresql.util.PSQLException: ERROR: column "discount" does not exist!

🔴 Senior Level

1. Архітектурний діагноз: «Розподілений моноліт» (Distributed Monolith)

Використання спільної бази даних перетворює систему на найгіршу архітектурну форму:

  • Усі мінуси розподілених систем: Мережеві затримки, падіння мережі, серіалізація JSON/gRPC, складний distributed tracing, труднощі моніторингу.
  • Жодного плюса мікросервісів: Неможливо незалежно масштабуватися, неможливо використовувати поліглотне зберігання (Polyglot Persistence), немає незалежності деплою, нульова ізоляція збоїв.

2. Покроковий план розпилу Shared DB: Strangler Fig & CDC

Розрізання спільної бази даних на живій 24/7 системі вимагає суворої послідовності кроків:

[Стара спільна БД] ──(1. CDC: Debezium / Kafka Connect)──► [Нова User DB]
       ▲                                                          │
       │                                                          ▼
[Order Service] ──(2. API Call: GET /users)──► [User Service] ───┘
  1. Визначення єдиного власника (Data Ownership): Оголошується, що таблицею users володіє виключно User Service.
  2. Інкапсуляція доступу (API Wrapping): Усі інші сервіси позбавляються прямого SQL-доступу до users. Їхній код переписується на звернення до REST/gRPC API сервісу User Service.
  3. Розгортання нової ізольованої БД: Піднімається незалежний інстанс user_db.
  4. Асинхронна реплікація через CDC (Change Data Capture): Інструмент Debezium вичитує WAL старої БД і в реальному часі стрімить зміни через Kafka в нову user_db.
  5. Тіньове читання (Dark Launch / Shadow Reads): User Service читає з нової БД, логуючи розбіжності зі старою базою.
  6. Фінальне перемикання (Cut-over): Запис і читання повністю перемикаються на нову БД. Таблиця в старій спільній базі видаляється.

4 Tricky Questions

1. Як виникає «прихований розподілений дедлок» (Distributed Deadlock) при використанні спільної бази даних кількома мікросервісами?

Відповідь: Сценарій взаємного блокування через мережу та базу даних:

  1. Сервіс $A$ відкриває локальну транзакцію в БД і блокує рядок у таблиці accounts (SELECT ... FOR UPDATE).
  2. Не завершуючи транзакцію, Сервіс $A$ робить синхронний HTTP-запит у Сервіс $B$.
  3. Сервіс $B$ для обробки запиту намагається виконати UPDATE accounts того самого рядка в спільній БД і зависає, очікуючи звільнення блокування Сервісом $A$.
  4. У результаті потік Сервіса $A$ чекає відповіді через HTTP від $B$, а Сервіс $B$ чекає коміту від $A$ в БД. Оскільки блокування розподілене між мережевим HTTP-сокетом і блокуванням рядків СУБД, детектор дедлоків реляційної бази безсилий. Система висить до закінчення HTTP-таймауту, вичерпуючи всі потоки обох сервісів.

2. Чому ізоляція на рівні схем (Schema per Service) на одному інстансі PostgreSQL все одно не захищає від проблем продуктивності?

Відповідь: Хоча поділ на схеми (orders_schema, users_schema) вирішує проблему незалежності DDL-міграцій, він не вирішує проблему розділення апаратних ресурсів:

  • Спільні CPU та RAM: Збирання сміття MVCC (VACUUM) або неоптимальний Seq Scan в одній схемі завантажує 100% ядер процесора СУБД.
  • Спільний дисковий пул та буферний кеш (Shared Buffers): Запит до терабайтної таблиці аналітики вимиває з кешу гарячі сторінки оперативних таблиць авторизації.
  • Спільний WAL (Write-Ahead Logging): Рясний INSERT в одній схемі забиває дисковий канал запису логу транзакцій, викликаючи затримки комітів у всіх інших схемах.

3. Як організувати деплой міграцій (Flyway / Liquibase), якщо команду змусили тимчасово жити зі спільною базою даних?

Відповідь:

  1. Окремі таблиці історії версій: Кожен сервіс зобов’язаний використовувати персональну таблицю версій міграцій:
    spring:
      flyway:
        table: flyway_schema_history_orders # Персональна таблиця замість дефолтної
    
  2. Принцип Expand-Contract (Розширення-Стиснення): Зміни схеми робляться строго зворотно сумісними у два етапи:
    • Етап 1 (Expand): Додається нова колонка full_name. Стара колонка name зберігається. Застосунок починає писати в обидві.
    • Етап 2 (Contract): Усі сервіси переведені на роботу з full_name. Лише наступним релізом стара колонка name видаляється.

4. Що робити, якщо двом сервісам потрібні миттєві транзакційні перевірки даних один одного при відмові від спільної бази?

Відповідь: Якщо два фрагменти даних настільки сильно пов’язані, що вимагають постійних міжбазових транзакційних перевірок у реальному часі, це головна ознака неправильно нарізаних Bounded Contexts. Рішення:

  1. Провести рефакторинг доменних меж та об’єднати ці два мікросервіси в один сервіс (Merge Services). У мікросервісах немає нічого ганебного у злитті надмірно гранульованих компонентів.
  2. Або застосувати асинхронний патерн Saga з компенсувальними транзакціями та семантичними блокуваннями (Pending State).

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

Головне:

  • Shared DB — це антипатерн, що створює «розподілений моноліт».
  • Головні проблеми: Schema Coupling, виснаження пулів конектів (Connection Starvation), ескалація блокувань (Lock Contention), відсутність власника даних.
  • Падіння спільної СУБД кладе 100% сервісів компанії (максимальний Blast Radius).
  • Рішення: перехід до Database per Service.
  • Міжсервісний доступ до даних дозволений лише через публічний API або брокер повідомлень (Kafka).
  • Міграція старої спільної БД виконується за шаблоном Strangler Fig + Debezium CDC.

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

  • ❌ «Спільна база — це нормально, якщо сервіси просто не чіпають чужі таблиці» (Домовленості на словах завжди порушуються в першому ж палаючому релізі).
  • ❌ «Для об’єднання даних між сервісами найпростіше зробити View у спільній базі» (View маскує залежність і ламається при зміні таблиці).
  • ❌ «Якщо міграція зламала чужий сервіс, винна команда, яка не протестувала чужий код» (Винна архітектура Shared DB, що допустила цю поломку).

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