Які проблеми виникають при використанні спільної бази даних
Використання спільної бази даних перетворює систему на найгіршу архітектурну форму:
🟢 Junior Level
Shared Database (Спільна база даних) — це класичний архітектурний антипатерн у мікросервісній архітектурі, при якому кілька незалежних мікросервісів звертаються безпосередньо до однієї й тієї самої фізичної або логічної бази даних, читаючи та змінюючи спільні таблиці.
┌───────────────────┐ ┌────────────────────┐
│ Order Service │ │ User Service │
└─────────┬─────────┘ └─────────┬──────────┘
│ │
└─────────────┬─────────────┘
▼
┌─────────────────────┐
│ SHARED DATABASE │ ◄── ❌ АНТИПАТЕРН!
│ (users, orders, ...)│
└─────────────────────┘
Основні небезпеки для системи:
- Жорстка зв’язаність за структурою (Schema Coupling): Якщо команда сервісу користувачів вирішує перейменувати або видалити колонку
phone, сервіс замовлень упаде при спробі прочитати цю колонку. - Втрата незалежності релізів: Не можна просто так задеплоїти нову версію мікросервісу. Потрібен довгий процес синхронізації релізів між усіма командами.
- Єдина точка відмови (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] ───┘
- Визначення єдиного власника (Data Ownership): Оголошується, що таблицею
usersволодіє виключноUser Service. - Інкапсуляція доступу (API Wrapping): Усі інші сервіси позбавляються прямого SQL-доступу до
users. Їхній код переписується на звернення до REST/gRPC API сервісуUser Service. - Розгортання нової ізольованої БД: Піднімається незалежний інстанс
user_db. - Асинхронна реплікація через CDC (Change Data Capture): Інструмент Debezium вичитує WAL старої БД і в реальному часі стрімить зміни через Kafka в нову
user_db. - Тіньове читання (Dark Launch / Shadow Reads):
User Serviceчитає з нової БД, логуючи розбіжності зі старою базою. - Фінальне перемикання (Cut-over): Запис і читання повністю перемикаються на нову БД. Таблиця в старій спільній базі видаляється.
4 Tricky Questions
1. Як виникає «прихований розподілений дедлок» (Distributed Deadlock) при використанні спільної бази даних кількома мікросервісами?
Відповідь: Сценарій взаємного блокування через мережу та базу даних:
- Сервіс $A$ відкриває локальну транзакцію в БД і блокує рядок у таблиці
accounts(SELECT ... FOR UPDATE). - Не завершуючи транзакцію, Сервіс $A$ робить синхронний HTTP-запит у Сервіс $B$.
- Сервіс $B$ для обробки запиту намагається виконати
UPDATE accountsтого самого рядка в спільній БД і зависає, очікуючи звільнення блокування Сервісом $A$. - У результаті потік Сервіса $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), якщо команду змусили тимчасово жити зі спільною базою даних?
Відповідь:
- Окремі таблиці історії версій: Кожен сервіс зобов’язаний використовувати персональну таблицю версій міграцій:
spring: flyway: table: flyway_schema_history_orders # Персональна таблиця замість дефолтної - Принцип Expand-Contract (Розширення-Стиснення): Зміни схеми робляться строго зворотно сумісними у два етапи:
- Етап 1 (Expand): Додається нова колонка
full_name. Стара колонкаnameзберігається. Застосунок починає писати в обидві. - Етап 2 (Contract): Усі сервіси переведені на роботу з
full_name. Лише наступним релізом стара колонкаnameвидаляється.
- Етап 1 (Expand): Додається нова колонка
4. Що робити, якщо двом сервісам потрібні миттєві транзакційні перевірки даних один одного при відмові від спільної бази?
Відповідь: Якщо два фрагменти даних настільки сильно пов’язані, що вимагають постійних міжбазових транзакційних перевірок у реальному часі, це головна ознака неправильно нарізаних Bounded Contexts. Рішення:
- Провести рефакторинг доменних меж та об’єднати ці два мікросервіси в один сервіс (Merge Services). У мікросервісах немає нічого ганебного у злитті надмірно гранульованих компонентів.
- Або застосувати асинхронний патерн 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, що допустила цю поломку).