Эталонная архитектура: уровень данных микросервисов
В микросервисах самое важное решение — владение данными: каждый сервис должен владеть своими данными (database-per-service) и не обращаться напрямую к таблицам другого сервиса. Это дерево пошагово рассматривает антипаттерн общей БД, достижение согласованности между сервисами без распределённых транзакций (saga + outbox), синхронизацию данных через CDC и межсервисные запросы (API composition vs модель чтения CQRS). Цель — сохранить сервисы действительно независимыми и раздельно развёртываемыми.
Как сервисы владеют данными: общая БД или по одной на сервис?
Изоляция данных — основа микросервисной архитектуры. Сервисы используют одну общую базу данных (скрытая зависимость), у каждого сервиса своя база данных (правильный паттерн), или реальная проблема — согласованность/запросы между сервисами? Сначала определите текущее состояние.
- Все сервисы используют одну общую БД
- У каждого сервиса своя БД, но операциям нужна согласованность между сервисами
- У каждого сервиса своя БД, но нужны объединённые запросы/отчёты
Как обеспечить согласованность межсервисного бизнес-процесса?
Для операции, охватывающей несколько сервисов (создать заказ → принять оплату → списать остаток), классическая распределённая транзакция (2PC) хрупка и не масштабируется. Вместо этого разбейте процесс на шаги «локальная транзакция + событие» (saga) и надёжно публикуйте события (outbox). С какой проблемой вы имеете дело?
- Я пытаюсь сделать многосервисный процесс атомарным (2PC?)
- Не удаётся согласовать локальную запись и публикацию события (dual write)
Как вы будете обслуживать запрос, показывающий данные из нескольких сервисов?
Когда данные разделены между сервисами, общий экран невозможно построить одним join. Есть два основных подхода: вызывать API соответствующих сервисов во время запроса и объединять результаты (API composition) или поддерживать заранее объединённую модель чтения, наполняемую событиями (CQRS). Выбор определяют частота и сложность запросов.
- Простое, редкое объединение; достаточно вызвать несколько сервисов и объединить
- Сложный/частый объединённый запрос; нужна готовая модель чтения
Рекомендация: database-per-service (разделите общую БД)
Совместное использование одной базы данных сервисами — скрытая и опасная зависимость: изменение схемы одного сервиса ломает другие, владение таблицами становится неясным, а независимое развёртывание — невозможным. Каждый сервис должен владеть своими данными.
Рекомендация: паттерн saga (избегайте распределённых транзакций)
Не пытайтесь сделать операцию, охватывающую несколько сервисов, атомарной одной распределённой транзакцией (2PC); 2PC удерживает блокировки и приносит хрупкость единой точки и низкую доступность. Вместо этого разбейте процесс на шаги, каждый из которых — локальная транзакция в своём сервисе, и отменяйте их компенсирующими шагами при сбое.
Рекомендация: Transactional outbox (ловушка двойной записи)
Когда сервису нужно и записать данные в свою БД, и опубликовать событие, выполнение этих действий по отдельности — ловушка «двойной записи» (dual write): если одно удалось, а другое нет, система остаётся в несогласованном состоянии. Решение — записать событие в таблицу outbox в ТОЙ ЖЕ локальной транзакции, что и бизнес-данные, а затем публиковать его отдельным процессом.
Рекомендация: API composition (объединение во время запроса)
Там, где нужны объединённые данные, вызывающая сторона (например, gateway/BFF) обращается к API соответствующих сервисов и объединяет результаты в памяти. Это самое простое решение для несложных соединений, охватывающих немного сервисов; дополнительная копия данных не хранится.
Рекомендация: модель чтения CQRS (наполняется событиями)
Для сложных и частых объединённых запросов держите заранее соединённую, оптимизированную под запросы модель чтения (материализованное представление/денормализованная копия), наполняемую событиями от сервисов. Сторона записи остаётся в сервисах; сторона чтения быстро отдаёт готовые данные из одного места.