Прямой ответ
Общая база данных — самая сильная форма связанности между сервисами. Сервисы, обращающиеся к одним таблицам, зависят друг от структуры данных, а не от контракта. Любое изменение схемы становится согласованным, даже если 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. Переносите данные без остановки. Схема переносится двойной записью и последующим переключением чтения, а не остановкой на время копирования.
Рекомендации:
- Не начинайте с переноса данных. Сначала уберите запись из чужих таблиц, затем чтение, и только потом переносите.
- Измеряйте обращения к чужим таблицам. Поиск имён чужих схем в коде даёт список того, что нужно заменить.
- Проверяйте права в CI. Тест, который пытается записать в чужую схему и ожидает ошибку, защищает границу от случайного нарушения.
- Планируйте запросы, объединяющие данные. После разделения JOIN через базы невозможен; нужен API-композитор или локальная копия.
- Не разделяйте базу, пока не разделены границы. Вынос таблиц при неверных границах сервисов создаёт распределённый монолит с общей сетью вместо общей схемы — см. статью про границы сервисов.
Практический сценарий: миграция одного сервиса сломала три других
Контекст: четыре сервиса работали с одной базой. Сервис каталога переименовал колонку в таблице товаров, и три других сервиса начали возвращать ошибки.
- Диагностика. В логах трёх сервисов появились ошибки обращения к несуществующей колонке. Их код не менялся две недели.
- Причина. Миграция сервиса каталога применилась к общей таблице, а потребители читали её напрямую, минуя API.
- Проверка масштаба. Поиск по коду показал 47 мест, где сервисы читали чужие таблицы. Из них 12 выполняли запись.
- Действия. (а) Временно вернули прежнее имя колонки и добавили представление с новым именем, чтобы снять аварию; (б) составили список владельцев таблиц и закрепили его в репозитории; (в) отозвали права на запись у всех сервисов, кроме владельца; (г) заменили прямое чтение на обращения к API владельца и на локальные копии, построенные по событиям; (д) ввели правило обратно совместимых миграций на переходный период; (е) перенесли таблицы в отдельные схемы после того, как обращения исчезли.
- Результат. Через два месяца миграция любого сервиса перестала влиять на остальные; проверка прав в тестах выявляет попытку обращения к чужой схеме до слияния.
Вывод: общая база делает миграции согласованными, даже если API разделены. Отзыв прав на запись и замена прямого чтения на контракт — первый и самый важный шаг выхода.
FAQ
Можно ли оставить общую базу, если сервисов немного? Да, при двух-трёх сервисах и одной команде издержки разделения могут превышать пользу. Важно, чтобы решение было осознанным, а не следствием отсутствия обсуждения.
Что делать с отчётами, которые читают всё? Строить отдельное аналитическое хранилище, наполняемое событиями или CDC, а не давать отчётному слою доступ к операционным таблицам.
Как обеспечить целостность при разделении базы? Внутри сервиса — транзакцией, между сервисами — сагой с компенсирующими действиями; о паттерне — в статье про saga и outbox.
Нужен ли отдельный сервер базы на каждый сервис? Нет. Схема на сервис в одном экземпляре СУБД решает задачу границ; отдельный сервер нужен при независимом масштабировании нагрузки.
Источники: microservices.io (shared database, database per service) — ссылки в шапке.