Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Общая база между сервисами: антипаттерн и выход

Общая база данных между сервисами: почему это тормозит и как выйти

Общая база как точка интеграции: почему она связывает сервисы сильнее API, схема на сервис, совместимые изменения и план выхода.

Прямой ответ

Общая база данных — самая сильная форма связанности между сервисами. Сервисы, обращающиеся к одним таблицам, зависят друг от структуры данных, а не от контракта. Любое изменение схемы становится согласованным, даже если API при этом не меняется.

Почему связь через базу хуже связи через API:

Аспект Общий API Общая база
Контракт Явный, версионируемый Неявный, в структуре таблиц
Изменение Обратная совместимость контролируется Ломает всех, кто читает таблицу
Владелец Известен Размыт
Транзакция Не пересекается Пересекается и блокируется
Тестирование По контракту Только на реальной схеме

Типовая ситуация: сервис оплаты добавил колонку status_v2, а сервис доставки продолжает читать status. Через месяц второй сервис начинает отдавать неверные данные, и причина находится не в его коде, а в чужой миграции.

Вторая проблема — блокировки. Два сервиса, пишущие в одну таблицу, конкурируют за строки и за порядок применения миграций. Транзакция одного может блокировать транзакцию другого, и задержка возникает там, где по логике сервисов её быть не должно.

Третья — невозможность независимого релиза. Изменение схемы требует одновременного развёртывания всех потребителей, поэтому сервисы фактически выкатываются вместе.

Целевое состояние — схема на сервис: каждый сервис владеет своими таблицами и предоставляет данные через API или события.

CREATE SCHEMA orders AUTHORIZATION orders_app;
CREATE SCHEMA payments AUTHORIZATION payments_app;

-- сервис заказов не имеет прав на схему оплаты
REVOKE ALL ON ALL TABLES IN SCHEMA payments FROM orders_app;
GRANT USAGE ON SCHEMA orders TO orders_app;

Отзыв прав на уровне базы делает нарушение границы видимым сразу: запрос завершится ошибкой, а не вернёт чужие данные.

Практика: выход из общей базы

1. Определите владельца каждой таблицы. Для каждой таблицы должен быть один сервис, который её создаёт и изменяет. Таблица без владельца — источник проблем.

2. Начинайте с отзыва прав на запись. Чтение через чужие таблицы можно оставить временно; запись должна уйти к владельцу сразу.

3. Делайте изменения схемы обратно совместимыми. Пока несколько сервисов читают одну таблицу, нельзя удалять и переименовывать колонки. Практика: добавить новую колонку, перевести потребителей, затем удалить старую — о миграциях в статье про Alembic.

4. Замените прямое чтение на представление. Промежуточный шаг — VIEW, который скрывает структуру таблицы:

CREATE VIEW orders.public_summary AS
SELECT id, customer_id, total, status FROM orders.orders;

Потребитель читает представление, а внутренняя структура может меняться.

5. Используйте события для передачи данных. Там, где сервису нужна копия чужих данных, он строит собственное представление по событиям владельца. Альтернатива — CDC, который читает журнал изменений; о механизме — в статье про CDC.

6. Примите eventual consistency. После разделения базы запрос, объединявший данные двух сервисов в одной транзакции, становится невозможным. Согласованность в пределах сервиса остаётся строгой, между сервисами — eventual; о моделях — в статье про eventual consistency.

7. Переносите данные без остановки. Схема переносится двойной записью и последующим переключением чтения, а не остановкой на время копирования.

Рекомендации:

  1. Не начинайте с переноса данных. Сначала уберите запись из чужих таблиц, затем чтение, и только потом переносите.
  2. Измеряйте обращения к чужим таблицам. Поиск имён чужих схем в коде даёт список того, что нужно заменить.
  3. Проверяйте права в CI. Тест, который пытается записать в чужую схему и ожидает ошибку, защищает границу от случайного нарушения.
  4. Планируйте запросы, объединяющие данные. После разделения JOIN через базы невозможен; нужен API-композитор или локальная копия.
  5. Не разделяйте базу, пока не разделены границы. Вынос таблиц при неверных границах сервисов создаёт распределённый монолит с общей сетью вместо общей схемы — см. статью про границы сервисов.

Практический сценарий: миграция одного сервиса сломала три других

Контекст: четыре сервиса работали с одной базой. Сервис каталога переименовал колонку в таблице товаров, и три других сервиса начали возвращать ошибки.

  1. Диагностика. В логах трёх сервисов появились ошибки обращения к несуществующей колонке. Их код не менялся две недели.
  2. Причина. Миграция сервиса каталога применилась к общей таблице, а потребители читали её напрямую, минуя API.
  3. Проверка масштаба. Поиск по коду показал 47 мест, где сервисы читали чужие таблицы. Из них 12 выполняли запись.
  4. Действия. (а) Временно вернули прежнее имя колонки и добавили представление с новым именем, чтобы снять аварию; (б) составили список владельцев таблиц и закрепили его в репозитории; (в) отозвали права на запись у всех сервисов, кроме владельца; (г) заменили прямое чтение на обращения к API владельца и на локальные копии, построенные по событиям; (д) ввели правило обратно совместимых миграций на переходный период; (е) перенесли таблицы в отдельные схемы после того, как обращения исчезли.
  5. Результат. Через два месяца миграция любого сервиса перестала влиять на остальные; проверка прав в тестах выявляет попытку обращения к чужой схеме до слияния.

Вывод: общая база делает миграции согласованными, даже если API разделены. Отзыв прав на запись и замена прямого чтения на контракт — первый и самый важный шаг выхода.

FAQ

Можно ли оставить общую базу, если сервисов немного? Да, при двух-трёх сервисах и одной команде издержки разделения могут превышать пользу. Важно, чтобы решение было осознанным, а не следствием отсутствия обсуждения.

Что делать с отчётами, которые читают всё? Строить отдельное аналитическое хранилище, наполняемое событиями или CDC, а не давать отчётному слою доступ к операционным таблицам.

Как обеспечить целостность при разделении базы? Внутри сервиса — транзакцией, между сервисами — сагой с компенсирующими действиями; о паттерне — в статье про saga и outbox.

Нужен ли отдельный сервер базы на каждый сервис? Нет. Схема на сервис в одном экземпляре СУБД решает задачу границ; отдельный сервер нужен при независимом масштабировании нагрузки.

Источники: microservices.io (shared database, database per service) — ссылки в шапке.