Микросервисы: границы, коммуникация, данные, эксплуатация
Микросервисы — самый обсуждаемый и самый неправильно применяемый архитектурный стиль последних пятнадцати лет. Проблема не в том, что он плох: в нужных условиях он работает отлично. Проблема в том, что его берут ради слова, а не ради свойства, и получают систему, у которой сложность распределённой архитектуры уже есть, а выгод ещё нет.
Статья построена вокруг четырёх вопросов, на которые нужно уметь ответить до первого kubectl apply:
где резать, как разговаривать, где живут данные, чем платить в эксплуатации. Предыдущая
статья трека — Монолит и модульный монолит —
описала альтернативу, которую стоит держать в голове всю дорогу.
1. Зачем: единственное свойство, за которое платят
Определение Сэма Ньюмена из «Building Microservices», 2nd ed.:
Микросервисы — это независимо развёртываемые сервисы, смоделированные вокруг бизнес-домена. Они общаются друг с другом через сеть.
Ключевое слово — независимо развёртываемые. Не «маленькие», не «на разных языках», не «в контейнерах»:
всё остальное — следствия. Если нельзя выкатить orders, не выкатывая payments, у вас не микросервисы,
а распределённый монолит — вы уже платите за сеть, но всё ещё координируете релизы.
Зачем нужна независимая развёртываемость? Она снимает координационный налог. Общий деплой N команд означает N-стороннюю синхронизацию: общий релизный поезд, общий откат, общий инцидент. Стоимость координации растёт как число пар команд, O(N²). Микросервисы — способ обменять координационную сложность на техническую: вместо согласования людей вы получаете сеть, частичные отказы и эвентуальную консистентность.
Отсюда главный критерий: нет проблемы координации — вы платите за решение, которое вам не нужно. Фаулер называет это «Microservice Premium»: наценка окупается только при достаточной сложности домена и достаточном числе команд.
Правый нижний квадрант требует пояснения: иногда сервис выделяют не из-за домена, а из-за несовместимого профиля ресурсов — транскодинг видео, ML-инференс, тяжёлые отчёты. Законная причина, но она даёт один-два сервиса, а не тридцать.
Предпосылки, без которых не начинать — Фаулер перечисляет их прямо:
- Быстрый провижининг окружения — новый сервис поднимается за минуты, не за тикет в инфру.
- Базовый мониторинг — вы видите отказ до звонка клиента: метрики, логи, трассировка.
- Автоматический деплой с откатом — иначе N сервисов = N ручных релизов.
- Владение вместо передачи — команда эксплуатирует то, что написала («you build it, you run it» из интервью Вернера Фогельса).
Нет пункта 2 или 3 — микросервисы усилят каждую операционную слабость ровно в N раз.
2. Границы: где резать
Самая дорогая ошибка делается здесь. Границу тяжело менять после того, как она стала сетевым контрактом и отдельной БД: перенос ответственности между сервисами стоит на два порядка дороже, чем между модулями монолита.
Плохой разрез — по техническим слоям: «сервис API», «сервис бизнес-логики», «сервис данных». Резать так интуитивно (это же слои из первой статьи трека), но катастрофично: почти любая фича меняет все три слоя, значит каждый релиз снова согласованный.
Правило: граница сервиса должна быть перпендикулярна направлению изменений. Изменения приходят в форме бизнес-требований — значит и резать по бизнес-возможностям, а сервис содержит вертикальный ломоть целиком: API, доменную логику, схему хранения.
Хороший разрез — ограниченные контексты (bounded context из DDD Эрика Эванса). Признак границы: одно и то же слово означает разные вещи. «Товар» в каталоге — описание, картинки, атрибуты поиска; на складе — SKU, габариты, остаток; в биллинге — позиция с ценой и налоговой ставкой. Три модели, три кандидата в сервисы. Единый «сервис товаров на все случаи» даёт entity service — анемичное CRUD-хранилище, которое дёргают все и которое не владеет никаким поведением.
Чек-лист границы:
- Связность по изменению. Какие файлы меняются в одном коммите? Если два куска кода всегда правят вместе — они в одном сервисе. Самая честная метрика, и она уже лежит в вашем git-логе.
- Автономность решения. Сервис отвечает на свой основной запрос, не ходя синхронно в чужие? Если для валидации заказа нужны три синхронных вызова — граница проведена неверно.
- Владение данными. У каждого куска данных ровно один владелец; остальные читают копии или спрашивают.
- Соответствие команде. По закону Конвея архитектура повторит структуру коммуникации организации; Team Topologies предлагает пользоваться этим намеренно — сначала выстроить команды так, как хотите видеть систему.
- Когнитивная нагрузка. Сервис должен помещаться в голову одной команды.
Полезная оценка. Пусть N — число сервисов, E — число зависимостей между ними. Изменение внутри сервиса стоит O(размер сервиса) и дешевеет с ростом N; изменение, задевающее контракт, стоит O(числа команд на границе) и дорожает; операционная база — O(N) пайплайнов, дежурств и дашбордов. Здоровая система — разреженный граф, E ≈ O(N), у большинства сервисов 2–5 зависимостей. Средняя степень вершины 10+ и приближение E к O(N²) означают одно: резали не по доменам.
authn, rate limit, роутинг"] end subgraph core["Домены с собственными БД"] ORD["Заказы"] CAT["Каталог"] PAY["Оплата"] SHP["Доставка"] end subgraph infra["Асинхронная шина"] BUS[("Брокер сообщений
топики событий")] end GW -->|"sync: команда"| ORD GW -->|"sync: чтение"| CAT ORD -->|"sync: авторизация платежа"| PAY ORD -->|"публикует OrderPlaced"| BUS PAY -->|"публикует PaymentCaptured"| BUS BUS -->|"подписка"| SHP BUS -->|"подписка"| CAT ORD --- ODB[("orders_db")] CAT --- CDB[("catalog_db")] PAY --- PDB[("payments_db")] SHP --- SDB[("shipping_db")]
Обратите внимание на асимметрию: синхронных стрелок мало и все они на «горячем пути» ответа пользователю, всё остальное — события. Это не случайность, а следствие арифметики из раздела 4.
3. Извлечение сервиса: strangler fig, а не big bang
Переписать монолит «с нуля на микросервисы» — надёжный способ потратить два года и не поставить ничего. Рабочий подход — Strangler Fig Application Фаулера: новая система прорастает вокруг старой, перехватывая трафик по одной возможности за раз, пока старая не станет ненужной. Жизненный цикл одного извлекаемого куска:
Два замечания. Первое: порядок «сначала данные, потом код» почти всегда правильный — если таблицы не разделяются без боли, это и есть ответ, что выделять сервис рано. Второе: шаг сверки (сравнение ответов старой и новой реализации на живом трафике, shadow traffic) стоит день работы и экономит недели инцидентов. Начинают с периферии — уведомления, отчёты, поиск; ядро домена извлекают последним.
4. Коммуникация: синхронная и асинхронная
Арифметика доступности. Если ответ пользователю требует синхронного успеха N сервисов, доступности
перемножаются: A = A₁ × A₂ × … × A_N. Десять сервисов по 99,9 % дают 0.999¹⁰ ≈ 99,0 % — около 7 часов
простоя в месяц вместо 43 минут. Каждая синхронная зависимость на горячем пути — вычет из вашего SLO.
Отсюда правило: синхронный вызов нужно обосновать; асинхронный — дефолт для всего, что не требуется
прямо сейчас в ответе.
Хвостовая латентность. Эффект тоньше, и на нём горят даже опытные команды: при fan-out клиент ждёт не среднее, а максимум времени ответа зависимостей. Классическая работа Дина и Барросо «The Tail at Scale» (CACM, 2013): если у сервиса p99 = 100 мс, запрос к 100 таким сервисам попадёт в хвост с вероятностью 1 − 0.99¹⁰⁰ ≈ 63 %.
Средства борьбы: сокращение fan-out, hedged requests (продублировать запрос по истечении p95 и взять первый ответ), деградация с частичным результатом, кэширование — см. Кэширование и масштабирование и Устойчивость.
Оркестрация против хореографии. Один и тот же бизнес-процесс выражается двумя способами.
Оркестрация (как выше): один сервис знает процесс целиком — его видно в одном месте, легко отлаживать и отслеживать состояние; но оркестратор накапливает чужие знания и становится узлом связанности. Хореография: сервисы публикуют события и реагируют на чужие, дирижёра нет — минимальная связанность и лёгкое добавление подписчиков ценой того, что процесс нигде не записан целиком и отладка держится на трассировке.
Практическое правило: хореография для уведомлений о фактах, оркестрация для процессов с деньгами и компенсациями. Подробный разбор — в Saga, распределённые транзакции, outbox и идемпотентность.
Контракты и версионирование. Сеть между сервисами — это API, а API нельзя ломать в одностороннем порядке. Минимум: (1) в пределах мажорной версии только обратно совместимые изменения — поля добавлять можно, удалять и менять смысл нет; (2) consumer-driven contract tests — потребитель описывает ожидания, поставщик прогоняет их в своём CI (Pact, идея — у Фаулера); это дешёвая замена сквозным интеграционным тестам, которые в микросервисах не масштабируются, потому что требуют поднимать всё разом и ломаются от чужих релизов; (3) expand/contract при миграции схемы: добавили новое поле → перевели потребителей → убрали старое, три релиза вместо одного. Детали — в Стили API.
5. Данные: главная трудность
Микросервисы почти всегда ломаются не на коде, а на данных. Правило номер один — никакого доступа к чужой БД. Если два сервиса пишут в одну таблицу, у вас нет двух сервисов: схема становится неявным общим контрактом, который никто не версионирует, и любая миграция снова требует согласованного релиза.
Пунктирные связи — важная часть картинки: между базами разных сервисов нет внешних ключей и джойнов,
есть только идентификаторы и осознанно продублированные снимки. sku_title_snapshot в строке заказа —
не «нарушение нормализации», а требование домена: в чеке должны остаться название и цена на момент
покупки, даже если каталог их потом изменит.
Три способа получить чужие данные. (1) Синхронный запрос — просто и всегда свежо, но добавляет зависимость по доступности и латентности; годится вне горячего пути. (2) Реплика через события — сервис подписан на события владельца и держит собственную read-модель: быстро и автономно, ценой эвентуальной консистентности и кода на поддержание проекции; это дорога к CQRS. (3) API composition в шлюзе/BFF — собрать ответ на кромке: просто, но подвержено fan-out-проблемам из раздела 4.
Чего делать не надо — распределённых транзакций поверх двухфазного коммита: 2PC блокирует ресурсы на время раунда и делает координатор единой точкой отказа. Стандартный ответ — сага с компенсациями.
Эвентуальная консистентность при этом не техническая уступка, а бизнес-решение, и проговаривать его нужно явно: «остаток на складе отстаёт до 2 секунд; в этом окне возможен овербукинг примерно раз на тысячу заказов; компенсация — автоматический возврат и письмо». Бизнес не готов — либо данные живут в одном сервисе, либо нужно резервирование на горячем пути. Теория — CAP в разборе Брюера и глава 9 «Designing Data-Intensive Applications» Клеппмана.
Outbox: атомарность «записал и опубликовал». Самый частый баг: запись в БД и публикация в брокер — две разные операции, и между ними процесс может упасть; итог — либо заказ без события, либо событие без заказа. Transactional outbox решает это: событие пишется в ту же БД той же транзакцией, а отдельный воркер досылает его в брокер.
-- Таблица исходящих событий живёт в БД сервиса-владельца
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ -- NULL = ещё не отправлено
);
-- Частичный индекс: сканируем только неотправленные, их обычно единицы
CREATE INDEX outbox_unpublished_idx ON outbox (id) WHERE published_at IS NULL;
# Создание заказа: одна транзакция на изменение состояния И на событие. psycopg3.
import json, uuid
def place_order(conn, customer_id: uuid.UUID, lines: list[dict],
idempotency_key: str) -> uuid.UUID:
order_id = uuid.uuid4()
with conn.transaction(): # BEGIN ... COMMIT
cur = conn.cursor()
# 1. Идемпотентность. UNIQUE-ограничение делает проверку атомарной, в отличие
# от "сначала SELECT, потом INSERT" — там между шагами есть гонка.
cur.execute("""INSERT INTO idempotency (key, order_id) VALUES (%s, %s)
ON CONFLICT (key) DO NOTHING""", (idempotency_key, order_id))
if cur.rowcount == 0: # ключ уже был — отдаём прежний заказ
cur.execute("SELECT order_id FROM idempotency WHERE key = %s",
(idempotency_key,))
return cur.fetchone()[0]
# 2. Состояние. Название и цену копируем в строку заказа: чек не должен
# меняться задним числом, даже если каталог обновит товар.
cur.execute("INSERT INTO orders (order_id, customer_id, status) "
"VALUES (%s, %s, 'PENDING')", (order_id, customer_id))
cur.executemany(
"""INSERT INTO order_lines (line_id, order_id, sku_id, sku_title_snapshot,
qty, price_minor) VALUES (%s,%s,%s,%s,%s,%s)""",
[(uuid.uuid4(), order_id, ln["sku_id"], ln["title"], ln["qty"], ln["price"])
for ln in lines])
# 3. Событие в outbox — той же транзакцией: либо есть и заказ, и событие, либо ничего.
cur.execute("INSERT INTO outbox (aggregate_id, event_type, payload) "
"VALUES (%s, %s, %s)",
(order_id, "OrderPlaced", json.dumps({
"event_id": str(uuid.uuid4()), # для дедупликации у потребителя
"order_id": str(order_id),
"version": 1, # версия схемы события
})))
return order_id
def relay_outbox(conn, broker, batch: int = 100) -> int:
"""Отдельный воркер: досылает события в брокер. Гарантия — at-least-once."""
with conn.transaction():
cur = conn.cursor()
# SKIP LOCKED позволяет запускать несколько воркеров без конфликтов.
cur.execute("""SELECT id, aggregate_id, event_type, payload FROM outbox
WHERE published_at IS NULL ORDER BY id
LIMIT %s FOR UPDATE SKIP LOCKED""", (batch,))
rows = cur.fetchall()
for row_id, aggregate_id, event_type, payload in rows:
# Ключ партиционирования = id агрегата: порядок событий заказа сохранён.
broker.publish(topic=f"orders.{event_type}",
key=str(aggregate_id), value=payload)
cur.execute("UPDATE outbox SET published_at = now() WHERE id = %s", (row_id,))
return len(rows)
def handle_order_placed(conn, event: dict) -> None:
"""Потребитель обязан быть идемпотентным: at-least-once = дубликаты неизбежны."""
with conn.transaction():
cur = conn.cursor()
cur.execute("INSERT INTO processed_events (event_id) VALUES (%s) "
"ON CONFLICT DO NOTHING", (event["event_id"],))
if cur.rowcount == 0:
return # уже обрабатывали — выходим
cur.execute("INSERT INTO shipments (order_id, status) VALUES (%s, 'NEW')",
(event["order_id"],)) # бизнес-эффект в той же транзакции
Сложность: вставка в outbox — O(1) к транзакции заказа; воркер благодаря частичному
индексу сканирует только неотправленные строки — O(k log n) на батч из k событий вместо
O(n). Память — O(batch). Дубликаты неизбежны (процесс мог упасть после publish, но до
UPDATE), поэтому дедупликация по event_id у потребителя — не опция, а требование.
Альтернатива polling-воркеру — Change Data Capture: читать WAL базы через Debezium и публиковать изменения без опроса. Дороже в эксплуатации, зато меньше задержка и меньше нагрузки на БД.
6. Межсервисный вызов: как выглядит правильный клиент
Синхронный вызов через сеть отличается от вызова функции тремя вещами: он может зависнуть, может
выполниться частично и может быть повторён. Клиент, который этого не учитывает, — источник каскадных
отказов. Ниже — Go, потому что в нём работа с дедлайнами явная (c.http создан с
&http.Client{Timeout: 900 * time.Millisecond}; клиент без таймаута ждёт вечно).
var ErrOpenCircuit = errors.New("payments: цепь разомкнута")
// Authorize авторизует платёж. Повтор с тем же idempotencyKey не спишет деньги дважды —
// это часть контракта сервиса платежей, а не только забота вызывающего.
func (c *Client) Authorize(ctx context.Context, orderID string, amount int64,
idempotencyKey string) (string, error) {
if !c.breaker.Allow() {
return "", ErrOpenCircuit // быстрый отказ вместо ожидания таймаута
}
// Бюджет на ВСЕ попытки: дедлайн вызывающего важнее нашей политики ретраев.
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
body, _ := json.Marshal(map[string]any{"order_id": orderID, "amount": amount})
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
// Экспоненциальная задержка с полным джиттером: без джиттера клиенты
// синхронизируются и бьют по восстанавливающемуся сервису волнами.
select {
case <-time.After(time.Duration(rand.Int63n(int64(50*time.Millisecond) << attempt))):
case <-ctx.Done():
return "", ctx.Err() // бюджет исчерпан — больше не ретраим
}
}
req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
c.baseURL+"/authorizations", bytes.NewReader(body))
req.Header.Set("Idempotency-Key", idempotencyKey) // один ключ на все попытки
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil { // сетевая ошибка или таймаут — ретраибельно
c.breaker.OnFailure()
lastErr = err
continue
}
switch {
case resp.StatusCode < 300:
var out struct {
AuthID string `json:"authorization_id"`
}
err = json.NewDecoder(resp.Body).Decode(&out)
resp.Body.Close()
c.breaker.OnSuccess()
return out.AuthID, err
case resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500:
resp.Body.Close() // 5xx и 429 ретраибельны
c.breaker.OnFailure()
lastErr = errors.New("payments: временная ошибка " + resp.Status)
default:
resp.Body.Close()
c.breaker.OnSuccess() // 4xx — сервис жив, виноват запрос: не ретраим
return "", errors.New("payments: отказ " + resp.Status)
}
}
return "", lastErr
}
Здесь пять решений, каждое из которых предотвращает конкретный класс аварий:
| Решение | Что предотвращает |
|---|---|
| Таймаут на запрос | Исчерпание пула соединений и потоков вызывающего |
Общий бюджет (дедлайн в ctx) |
Ретраи, переживающие терпение пользователя |
| Ретрай только 5xx/429/сетевых | Повтор заведомо провальных запросов, усиление шторма |
| Джиттер | Синхронизацию клиентов и «стадный» повтор |
| Circuit breaker | Каскадный отказ: держим 100 % нагрузки на упавший сервис |
Дополнительно на уровне системы нужны retry budget (не больше ~10 % ретраев от трафика: три уровня по три попытки дают 27-кратное усиление шторма) и bulkhead — изоляция пулов, чтобы одна медленная зависимость не съела все воркеры. Полный разбор — в Устойчивость и в Amazon Builders’ Library.
7. Эксплуатация: цена входного билета
С монолитом было «одно приложение — один дашборд»; с двадцатью сервисами это не работает.
Наблюдаемость — три сигнала, и все три обязательны. Метрики: RED (Rate, Errors, Duration) на сервис
и USE (Utilization, Saturation, Errors) на ресурсы, обязательно в перцентилях — среднее время ответа
скрывает ровно ту проблему, о которой был раздел 4. Логи: структурированные, с trace_id в каждой
строке; без корреляции логи двадцати сервисов бесполезны. Трассировка: сквозная,
OpenTelemetry как стандарт де-факто, контекст в заголовке
W3C traceparent — единственный способ ответить, где потерялись
600 мс. Правило: новый сервис не выходит в прод без дашборда и алертов на SLO, причём алерты — на
видимые пользователю симптомы, а не на загрузку CPU
(Google SRE Workbook).
# Минимальный контракт сервиса перед Kubernetes: пробы, лимиты, graceful shutdown.
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders
spec:
replicas: 3
template:
spec:
# Даём время долить in-flight запросы перед убийством пода
terminationGracePeriodSeconds: 30
containers:
- name: orders
image: registry.internal/orders:1.42.0 # только неизменяемые теги, не latest
readinessProbe: # готов ли принимать трафик
httpGet: { path: /readyz, port: 8080 } # проверяет зависимости: БД, брокер
periodSeconds: 5
livenessProbe: # жив ли процесс вообще
httpGet: { path: /healthz, port: 8080 } # НЕ проверяет зависимости!
periodSeconds: 10
failureThreshold: 3
resources:
requests: { cpu: "250m", memory: "256Mi" }
limits: { memory: "512Mi" } # лимит CPU часто вреден: троттлинг
lifecycle:
preStop:
exec: { command: ["sleep", "5"] } # ждём, пока endpoints разойдутся
Тонкость, на которой горят регулярно: liveness-проба не должна проверять внешние зависимости. Иначе кратковременная недоступность БД заставит Kubernetes перезапустить все поды всех сервисов разом — самоинициированный каскадный отказ. Зависимости проверяет readiness.
Деплой и релизы: один сервис — один пайплайн — один артефакт (общий релизный поезд убивает весь смысл); прогрессивная выкатка canary или blue-green с автооткатом по метрикам; обязательная совместимость версий N и N−1, потому что во время выкатки в проде работают обе; миграции БД отдельными шагами — сначала расширяющая, потом код, потом сужающая, иначе откат кода ломает данные.
Сервисная сетка. mTLS, ретраи, таймауты, circuit breaking, трассировку и канареечный роутинг можно вынести из приложения в инфраструктуру (Istio, Linkerd): единая политика без изменения кода, что особенно ценно в полиглотной среде. Цена — ещё один сложный распределённый компонент, новые режимы отказа и расход ресурсов на sidecar. Разумный порог — примерно 15–20 сервисов и/или два языка и больше; до этого хватает библиотек.
Локальная разработка — боль, о которой забывают при выборе: двадцать сервисов на ноутбуке не поднимаются. Рабочие подходы: контрактные заглушки (WireMock, Pact stubs), персональные namespace в общем dev-кластере, telepresence-подобные инструменты. Если ответ на «как запустить это локально» — «никак», продуктивность упадёт сильнее, чем вырастет от независимых релизов.
8. Типичные ошибки
| Ошибка | Симптом | Что делать |
|---|---|---|
| Распределённый монолит | Релиз требует согласованной выкатки нескольких сервисов | Пересмотреть границы; временно объединить обратно |
| Общая БД | Два сервиса пишут в одну таблицу | Разделить владение, доступ только через API/события |
| Нано-сервисы | Сервис на одну функцию, вызовов больше, чем логики | Слить обратно; сервис ≈ ограниченный контекст |
| Разрез по слоям | «Сервис данных», «сервис логики» | Резать по бизнес-возможностям |
| Entity-сервисы | Анемичный CRUD «сервис пользователей», куда ходят все | Логика к данным; сервис владеет поведением |
| Синхронные цепочки | A → B → C → D на горячем пути | События, кэш, денормализация |
| Ретраи без бюджета | Локальная деградация → полный отказ | Бюджет ретраев, джиттер, circuit breaker |
| Общие библиотеки домена | Обновление либы требует релиза всех | В общее выносить только инфраструктуру, не домен |
| Микросервисы без CI/CD | Релиз занимает день, деплоят раз в спринт | Сначала предпосылки, потом разрез |
| Начало с микросервисов | Границы переделываются каждый месяц | Модульный монолит, пока домен не устоялся |
Последняя строка заслуживает пояснения: «Monolith First» — не консерватизм, а следствие того, что правильные границы обнаруживаются, а не проектируются заранее. В монолите цена ошибочной границы — рефакторинг; в микросервисах — миграция данных и согласованный релиз нескольких команд.
9. Как это выглядит в проде
- Amazon пришла к сервисам из проблемы деплоя монолита, а не из моды; «two-pizza teams» — правило про владение, и оно первично по отношению к архитектуре.
- Netflix — канонический пример: сотни сервисов, но вместе с ними Hystrix (ныне resilience4j), Chaos Monkey и большая внутренняя платформа. Копировать архитектуру, не копируя платформу и практики, — карго-культ.
- Segment публично вернулась от микросервисов к монолиту: 140+ сервисов давали операционные издержки выше выгоды. Обратный ход — нормальное решение, не поражение.
- Shopify, GitHub, Stack Overflow десятилетиями живут на крупных модульных монолитах под серьёзной нагрузкой: объём трафика сам по себе микросервисов не требует.
- Amazon Prime Video собрала часть распределённого пайплайна обратно в один процесс и снизила стоимость на 90 % — сетевые хопы и передача данных стоят денег.
Общий вывод: микросервисы — организационный инструмент с технической ценой. Он окупается, когда координация людей стала узким местом; пока не стала — вы платите без возврата.
10. Мини-итог
- Покупают одно свойство — независимую развёртываемость; всё остальное следствия.
- Границы — по бизнес-возможностям и ограниченным контекстам, перпендикулярно направлению изменений.
- Синхронные зависимости перемножают недоступность и складывают хвосты латентности; дефолт для всего, что не нужно прямо в ответе, — события.
- Данные: БД на сервис, никаких общих таблиц и джойнов, осознанная денормализация, outbox и идемпотентные потребители вместо распределённых транзакций.
- Эксплуатация — не «потом»: трассировка, SLO-алерты, автоматический деплой, корректные пробы, прогрессивная выкатка. Без них N сервисов = N × ваши нынешние проблемы.
- Двигайтесь strangler fig и не стесняйтесь склеивать обратно, если граница оказалась неверной.
Что почитать: Sam Newman, Building Microservices, 2nd ed. и Monolith to Microservices; Chris Richardson, microservices.io — каталог паттернов с trade-offs; Microservices Guide Фаулера; Kleppmann, DDIA, главы 5, 7, 9; Nygard, Release It!; Dean, Barroso, The Tail at Scale; Google SRE Book и Workbook; Amazon Builders’ Library.
Что дальше
Мы несколько раз упёрлись в события как в дефолтный способ связи между сервисами, но обошли главное: какие бывают брокеры, чем топик отличается от очереди, что означают at-most-once / at-least-once / exactly-once и почему порядок сообщений — не бесплатное свойство. Об этом — следующая статья: