Какие проблемы возникают при использовании общей базы данных
Использование общей базы данных превращает систему в худшую архитектурную форму:
🟢 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, допустившая эту поломку).