3. Data ownership
В монолитной архитектуре реляционная база данных долгое время служила уютной «коммунальной квартирой». Любой модуль или фоновый воркер мог без лишних церемоний выполнить размашистый JOIN между таблицей пользователей, корзиной, счетами и историей заказов. Все жили под защитой транзакций ACID, внешних ключей (FOREIGN KEY) и единого планировщика блокировок СУБД.
Но в мире микросервисов эта «коммуналка» мгновенно превращается в архитектурный капкан. Если два независимых сервиса напрямую читают и модифицируют одни и те же таблицы, они оказываются жестко связаны на уровне физической схемы данных. Достаточно переименовать или разделить колонку, изменить тип поля или добавить индекс, как вам придется синхронно тестировать, перекомпилировать и выкатывать оба сервиса одновременно. Независимость разработки рассыпается в прах.
Принцип Data Ownership (Владение данными) гласит: каждый микросервис является единственным и единоличным владельцем своих данных. Никакой другой сервис или внешний компонент не имеет права обращаться к его хранилищу напрямую в обход публичного API.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph AntiPattern["Антипаттерн: Shared Database (Распределенный монолит)"]
direction TB
S1["Order Service"]:::data --> DB[("Общая база данных<br/>(Shared DB)")]
S2["Billing Service"] --> DB
S3["Delivery Service"] --> DB
end
subgraph BestPractice["Паттерн: Database per Service (Четкое владение)"]
direction TB
OS["Order Service"]:::data --> ODB[("Order DB")]
BS["Billing Service"]:::data --> BDB[("Billing DB")]
DS["Delivery Service"]:::data --> DDB[("Delivery DB")]
OS -.->|"gRPC / Events"| BS
OS -.->|"gRPC / Events"| DS
end
Золотое правило: Database per Service
Фундаментальное правило микросервисной гигиены требует: у каждого сервиса должна быть своя собственная база данных (или как минимум изолированная логическая схема/tenant в кластере СУБД), доступ к которой осуществляется строго через API этого сервиса.
Этот барьер обеспечивает три важнейших инженерных свойства:
- Инкапсуляция: Внутренняя структура таблиц, шардирование и нормализация — это сугубо приватная деталь реализации. Вы можете безболезненно сменить PostgreSQL на MongoDB или ScyllaDB внутри сервиса
Comments, и остальная система этого даже не заметит, пока контракт API остается стабильным. - Масштабируемость: Вы получаете свободу настраивать СУБД под специфику профиля нагрузки. Например, сервис
Recommendationsможет использовать графовую Neo4j или векторную БД, логам подойдет ClickHouse, а сервисуAuthдля хранения сессий и токенов — сверхбыстрый Redis. - Изоляция сбоев: Тяжелый аналитический отчет или зависший
Seq Scanв БД сервисаRecommendationsбольше не способен заблокировать пул соединений и исчерпать CPU дискового массива, а значит, сервисAuthпродолжит штатно и мгновенно авторизовывать пользователей.
Проблемы децентрализации данных
Когда мы лишаемся общей базы данных, мы неизбежно сталкиваемся с тремя классическими вызовами распределенного хранения:
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Client["Клиентский запрос<br/>(Собрать экран заказа)"]:::entry --> Gateway["API Gateway / Агрегатор"]
subgraph Problem1["1. Распределенные запросы (API Composition)"]
Gateway -->|"GET /orders/123"| OS["Order Service"]
Gateway -->|"GET /users/42"| US["User Service"]
Gateway -->|"GET /deliveries/99"| DS["Delivery Service"]
end
subgraph Problem2["2. Событийная консистентность"]
US -.->|"Событие: UserDeleted"| Broker["Брокер сообщений"]
Broker -.-> OS
Broker -.-> DS
end
1. Распределенные запросы (JOIN across services)
Как отобразить на экране мобильного приложения список заказов пользователя вместе с его текущим адресом, балансом бонусных баллов и статусом доставки, если эти сущности физически раскиданы по трем разным базам данных на разных серверах?
- API Composition (Агрегация в коде): Специальный сервис-агрегатор (BFF — Backend For Frontend или API Gateway) выполняет параллельные сетевые запросы к нужным микросервисам (например, через горутины и
sync.WaitGroupв Go) и сшивает результаты воедино в оперативной памяти. - CQRS (Command Query Responsibility Segregation): Для высоконагруженных экранов сшивать данные на лету каждым HTTP-запросом слишком накладно. Мы создаем выделенное представление на чтение (Read Model), которое подписывается на события от сервисов и формирует денормализованные, предварительно соединенные срезы, читаемые за один O(1) запрос.
2. Целостность и консистентность
В монолите целостность связей опирается на внешние ключи (FOREIGN KEY) и каскадные операции на уровне движка InnoDB или Postgres. В распределенной среде внешних ключей между базами физически не существует.
- Событийная консистентность: Если пользователь удален в
Auth Service, сервис генерирует и публикует во внешнюю шину событиеUserDeleted. Заинтересованный сервисOrdersпоймает это событие в бэкграунде и выполнит собственную доменную политику (например, очистит персональные данные или переведет историю в архив). - Мягкие ссылки: Вместо физического внешнего ключа в реляционной таблице сохраняется просто числовой или строковый идентификатор
user_id. Приложение и предметная логика берут на себя ответственность за обработку ситуаций, когда сущность по указанномуuser_idуже отсутствует или еще не создана.
3. Дублирование данных
Попытка любой ценой соблюсти академический принцип DRY (Don't Repeat Yourself) в микросервисах часто приводит к потере автономности. Чтобы не опрашивать соседний сервис по сети на каждый чих, архитекторы сознательно идут на контролируемую денормализацию.
Например, Delivery Service может сохранять локальную неизменяемую копию имени клиента и номера телефона на момент создания отправления, чтобы не дергать User Service при каждом расчете маршрута курьера и формировании путевого листа.
Mechanical Sympathy: Shared Data и Go Runtime
Инженеры, привыкшие писать эффективный многопоточный код на Go, легко поймут глубинную суть Data Ownership через аналогию с многопоточностью.
В рантайме Go существует золотой принцип:
«Do not communicate by sharing memory; instead, share memory by communicating.»
Когда две горутины бесконтрольно читают и пишут в одну область памяти без мьютекса или атомиков, мы получаем гонку данных (data race), порчу памяти и крах рантайма. Попытка повесить глобальный мьютекс убивает масштабируемость: ядра процессора встают в очередь, протокол когерентности кэшей MESI раскаляет шину инвалидацией Cache Lines (64 байта), конвейер инструкций сбрасывается.
Общая база данных между микросервисами — это в точности распределенный аналог расшаренной памяти без синхронизации! Сервисы начинают конкурировать за блокировки строк, создавать распределенные взаимоблокировки (deadlocks) и страдать от неожиданных изменений чужих схем.
Единственный способ масштабировать систему — изолировать локальное состояние и общаться через передачу сообщений. База данных микросервиса — это его приватная память, а каналы связи — типизированные очереди сообщений и сетевые RPC.
Стратегии передачи владения
Если бизнес-процесс выходит за рамки одного сервиса и требует надежного переноса состояния, применяются проверенные паттерны координации:
- Transactional Outbox: Нельзя просто сделать
INSERTв локальную БД, а затем вызватьproducer.Publish()в Kafka — если приложение упадет между этими двумя действиями, система окажется в рассинхронизированном состоянии. Паттерн Outbox сохраняет бизнес-сущность и исходящее событие в одной локальной ACID-транзакции в специальную таблицуoutbox, откуда фоновый ретранслятор гарантированно доставляет его в брокер (8. Distributed transactions). - Control Plane vs Data Plane: Один сервис может являться владельцем эталонной конфигурации и мастер-данных (Control Plane), но централизованно рассылать снапшоты остальным сервисам для локального кэширования и мгновенного чтения без задержек (Data Plane).
- Change Data Capture (CDC): Использование специализированных коннекторов (таких как Debezium) для асинхронного вычитывания логов упреждающей записи СУБД (Write-Ahead Log / WAL) и автоматической трансляции изменений напрямую в шину событий.
Итог
Data Ownership — это дисциплина архитектурных границ. Мы осознанно отказываемся от мгновенного удобства произвольных SQL-запросов и каскадных внешних ключей ради независимости команд, надежной изоляции отказов и автономного масштабирования сервисов. Без строгого владения данными микросервисная архитектура неизбежно скатывается в самый опасный антипаттерн — распределенный ком грязи (Distributed Big Ball of Mud).
Когда данные надежно изолированы внутри границ своих сервисов, возникает следующая ключевая инженерная задача: как организовать эффективный, устойчивый к сетевым сбоям и задержкам обмен сообщениями между ними. Об этом подробно пойдет речь в следующей статье: 4. Service communication.