🏗️ Раздел 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, допустившая эту поломку).

Связанные темы: