Database per service: критерии, компромиссы и где это правда работает
«У каждого сервиса своя база» — одно из тех правил, которые звучат настолько разумно, что хочется применить везде сразу. На бумаге всё аккуратно: сервисы независимы, релизятся отдельно, схему меняет одна команда. На практике через полгода ты обнаруживаешь, что половина новых эндпоинтов агрегирует данные из четырёх баз, отчёт за квартал требует ETL-процесса, а у DBA вместо двух Postgres-ов в проде стоит шесть, плюс две Mongo и один Redis для кешей. И всё это нужно бэкапить, мониторить, обновлять и расследовать инциденты.
Расскажу, в каких случаях я разворачиваю отдельные базы под каждый сервис, в каких оставляю одну на двоих, и что обычно идёт не так, когда правило применяют слепо.
Что вообще даёт database per service
Главный смысл — это не «изоляция», а возможность независимо менять схему. Если у двух сервисов одна таблица, любое изменение требует согласования: пара недель ожидания миграции, ревью у DBA, координация деплоя. С отдельными базами команда меняет свою схему когда хочет, не оглядываясь на соседей.
Второе следствие — независимость по отказам. Падение базы одного сервиса не валит остальных, если они не зависят от него синхронно. Это работает только в комплекте с правильно поставленными API-вызовами и таймаутами, иначе изоляция остаётся только в схеме, а в проде всё ляжет каскадом.
Третье, менее очевидное — возможность выбрать подходящее хранилище под задачу. Один сервис на Postgres, другой на ClickHouse для аналитики, третий на key-value для кешей. Это плюс, но он же создаёт операционную нагрузку: команда инфраструктуры теперь поддерживает зоопарк.
Когда правило не оправдано
В стартапе на двух разработчиках и трёх сервисах — почти никогда. Цена изоляции (миграции, копирование данных, отдельная админка для каждой БД) не окупается, потому что согласовывать всё равно негде: все за одним столом. На таком этапе одна общая БД с явными схемами под каждый сервис — нормальный компромисс.
В системах, где много отчётов из разных доменов — тоже не везде. Если ваш бизнес строится на больших аналитических запросах, разделение на десять баз превратит каждый отчёт в задачу интеграции данных. Тогда лучше явно отделить операционные базы от аналитического хранилища, в которое события сливаются единообразно.
В тонких сервисах-обёртках — сомнительно. Если сервис делает прокси к API партнёра и хранит мизер информации, отдельная БД для него — лишняя коробка в инфраструктуре.
Что значит «своя база» на самом деле
Вариантов больше двух. Реальная шкала выглядит так.
- Общий инстанс БД, общая схема. Все сервисы пишут в одни и те же таблицы. Это не database per service ни в каком смысле. Жить можно, но любое изменение схемы — это event с участием всех.
- Общий инстанс, разные схемы. В одном Postgres есть схемы
orders,billing,catalog. Каждая команда владеет своей. Доступ к чужой — только через API, не через SQL. Самый прагматичный компромисс на средних проектах. - Разные инстансы (логически). Каждый сервис подключается к своему URL, своему пользователю, своим бэкапам. Может быть на одном или разных физических серверах.
- Разные физические серверы. Полная изоляция. Окупается только при значительной разнице в нагрузке или требованиях.
Я обычно рекомендую начинать с уровня «общий инстанс, разные схемы» и переходить выше, когда появляется явная причина: разные требования к доступности, разная нагрузка, разные бэкапные политики. Прыгать сразу в «разные физические серверы» — лишняя инфраструктурная боль на старте.
Чего нельзя делать ни в каком из вариантов
Главное правило, которое отделяет настоящую изоляцию от показухи — никаких прямых запросов в чужую базу. Никаких JOIN через границу сервиса. Никаких отчётных читаемых вьюшек, которые «просто читают, ничего не пишут». Если такой канал появился, он перестаёт быть аномалией и становится зависимостью, которую сложно убрать.
Это правило проще принять, чем соблюдать. Регулярно встречаю случай, когда разработчик «временно» лезет в чужую таблицу, чтобы поскорее сделать фичу. Через год эта временная связь — единственный способ построить отчёт о продажах. Удалять её больно.
Из этой же области: общие staged/temp таблицы, в которые пишут несколько сервисов. Это шаринг состояния под другим именем. Если данные нужны нескольким сервисам — они должны передаваться явно: через API, через события, через копию в каждой БД.
Как делиться данными
Когда сервису нужны данные соседа, есть три варианта в порядке убывания сложности.
API-вызов на чтение
Самый простой. Сервис A зовёт API сервиса B и получает то, что нужно. Подходит для нечастых запросов, для свежих данных, для ситуаций, когда количество вызовов невелико.
Минусы: связность по доступности (B упал — A не работает), задержка (каждый вызов — сетевой round-trip), невозможность сложных запросов (фильтрация и агрегация в API соседа обычно ограничены).
Локальная копия (read model)
Сервис A держит у себя копию нужных данных из B, обновляемую через события или CDC. Свои запросы делает по своей копии.
Плюсы: производительность, независимость по доступности (B упал — A работает на старой копии). Минусы: согласованность (копия отстаёт), сложность поддержки (если схема в B меняется, копия требует обновления), удвоенное хранилище.
Я применяю этот вариант, когда A часто читает данные из B и любая задержка/сбой влияет на пользователя. Цена — внимательное проектирование событий и контрактов.
Событийная архитектура с фактами
B публикует события (OrderPlaced, CustomerUpdated), любой сервис собирает свою проекцию. Это масштабируемый вариант для системы с множеством потребителей одних и тех же фактов.
Сложнее в реализации: нужны брокер, схема событий, контракт, обработка дубликатов и порядка. Но окупается на системах, где данные одного домена нужны пяти-десяти другим.
Транзакции через несколько баз
Самая болезненная тема. Прямой ответ: не пытайтесь. Распределённая транзакция через несколько БД (XA, two-phase commit) технически возможна, но в современных стеках это либо плохо работает, либо плохо поддерживается, либо сильно бьёт по доступности.
Если бизнес-операция трогает данные нескольких сервисов, есть два пути.
Первый — saga. Цепочка локальных транзакций с компенсациями: «зарезервировали товар, списали деньги, оформили доставку; если на третьем шаге сорвалось — компенсируем первые два». Сложно в реализации, но честно.
Второй — пересмотреть границы сервисов. Если операция действительно атомарная и без неё бизнес ломается — может быть, эти данные должны быть в одном сервисе. Не каждое разделение, которое выглядело логичным, выживает столкновение с реальной бизнес-логикой.
Эксплуатационная стоимость
Часто недооценивают, что отдельные базы — это не только разные строки в коде. Это:
- Отдельные миграции, версионируемые с релизом каждого сервиса.
- Отдельные бэкапы и тесты восстановления для каждой БД.
- Отдельные мониторинги: размер, медленные запросы, replication lag, размер WAL.
- Отдельные процедуры обновления версии (Postgres 14 → 16 на десяти инстансах).
- Часто — отдельные пулы соединений и отдельные secrets.
Если у вас два сервиса — это терпимо. Если десять — у вас уже половина рабочего времени DBA уходит на координацию обновлений. Это не аргумент против database per service, это аргумент за то, чтобы не множить базы без явной выгоды.
Аналитика и отчёты
В системах с раздельными базами вопрос «как сделать отчёт по всему» поднимается на третий-четвёртый месяц. Решений по сути два.
Один — отдельное аналитическое хранилище (DWH, lakehouse, ClickHouse), куда попадают данные из всех сервисов. Через CDC, через события, через batch-выгрузку. Все аналитические запросы — там, не в операционных БД. Отчёты не нагружают прод, аналитики не ждут разрешения у каждой команды.
Второй — read-сервис для конкретного отчёта, собирающий данные из нескольких источников по событиям. Это вариант для случаев, когда DWH ещё не построен, а отчёт нужен сейчас.
Что точно не работает — собирать SQL из десяти БД через приложение, агрегируя в памяти. Если вижу такое в проде, это первое, что переписываю.
Чек-лист перед выделением отдельной БД
- Сервис меняет свою схему достаточно часто, чтобы согласование с соседями стоило боли.
- Есть хотя бы один сценарий, где независимость по отказам критична.
- Нет частых отчётов, которые требуют объединения этой БД с другими.
- Команда готова поддерживать дополнительный инстанс (или хотя бы схему) с миграциями, бэкапами, мониторингом.
- Понятно, как другие сервисы будут получать нужные данные: API, события, проекция.
Если хотя бы половина пунктов сомнительна — пока оставляйте общую БД и фиксируйте границы на уровне схем и кода. Перейти на отдельные инстансы потом несложно. Свести их обратно — заметно дороже.
Что запомнить
Database per service — это не догма, а инструмент с конкретной ценой. Платите за изоляцию релизами, доступностью, гибкостью схемы. В обмен получаете операционную сложность, проблему распределённых транзакций, необходимость думать о согласованности данных. На зрелых системах с большим количеством команд оно окупается. На маленьких и средних — чаще нет, и спокойная общая БД с разными схемами оказывается лучшим выбором, который не стыдно показать архитектору через два года.