Модуль 1. MVP-проект «OrderFlow»: три брокера, пять сервисов, один трейс
Цель модуля: собрать сквозной MVP: API Gateway → Order Service → outbox → брокер → Payment/Inventory/Notification + Saga Orchestrator + WebSocket push. Пройти шесть спринтов с чекпоинтами, связать события OpenTelemetry-трейсом от продюсера до консьюмера, обвесить всё Prometheus-метриками и выстоять под нагрузкой Vegeta/k6.
Перед началом: курс «Фундамент» (Outbox, Saga, graceful shutdown — 2.4–2.7) и брокерские курсы трека. Инструменты: docker-compose, Go 1.22+, OpenTelemetry SDK, Prometheus + Grafana, Vegeta/k6. Трудоёмкость: ~20–25 ч (6 спринтов по 3–4 ч + квиз).
🏗 Архитектура
text[API Gateway] --HTTP--> [Order Service] --outbox--> [Брокер] | ┌────────────────────────┬────────────┴─────────────┐ ▼ ▼ ▼ [Payment Service] [Inventory Service] [Notification Service] | | | └────── события ─────────┘ ▼ ▼ [WebSocket push] [Saga Orchestrator]
Почему именно так — и что здесь учебного, а что продакшенского:
- Order Service владеет транзакцией заказа и публикует события только через Outbox (6.4) — никаких «publish из HTTP-ручки».
- Saga Orchestrator ведёт машину состояний платежа/резерва (6.5) —
компенсации (
payment.refund) при отказе инвентаря. - Notification Service — push-потребитель: realtime-статусы в браузер (Redis Pub/Sub в S3).
- Три брокера одновременно — учебная сверхзадача модуля: RabbitMQ для команд Saga, Kafka — событийный лог заказов, Redis — уведомления и конфиги. В финале (S4) Watermill объединит их за одним интерфейсом.
🏃 Sprint 1: Order Service + Outbox + RabbitMQ
Объём спринта: каркас репозитория, Order Service с PostgreSQL, outbox
- диспетчер (SKIP LOCKED), публикация
order.createdв RabbitMQ, Payment-заглушка, потребляющая очередь.
Чекпоинт (конец недели 1):
-
docker-compose upподнимает Postgres + RabbitMQ + 2 сервиса. - POST /orders создаёт заказ; в
outboxпоявилась строка, диспетчер её забрал, в очереди payment — сообщение. - Тест: broker down при создании заказа → после подъёма брокера событие доставлено (ноль потерь).
- Graceful shutdown: SIGTERM посреди диспетчеризации — нет потерь и дублей (консьюмер-заглушка идемпотентен).
Анти-цель спринта: не пишите сразу все сервисы. Один сервис, один паттерн, один тест — движение к чекпоинту.
🏃 Sprint 2: миграция Order Event Log на Kafka
Объём: событие заказа публикуется в Kafka-топик orders.events
(партиционирование по order_id), Inventory Service как consumer group,
проекция резерва в PostgreSQL (идемпотентная запись из 3.7).
Чекпоинт:
- Все события одного заказа — в одной партиции (тест на порядок).
- Два инстанса Inventory делят партиции, zero double-processing (тест с двумя воркерами).
- Убийство инстанса посреди батча → повторная доставка переживается идемпотентно.
- Consumer lag отображается в экспортере (пока без Grafana).
🏃 Sprint 3: Redis Streams — realtime + конфиги
Объём: Notification Service переводится на Redis: стрим orders:notif
(XADD, MAXLEN ~10⁶) с группой notifier — надёжные уведомления с
возможностью replay; Pub/Sub — broadcast статусов в WebSocket-шлюз;
hash flags:notif — каналы уведомлений (включение/выключение без
рестарта).
Чекпоинт:
- Дедуп уведомлений:
processed:notif:{order}:{status}— SETNX с TTL (курс Redis, М3); тест дубля проходит. - WebSocket-клиент видит статус заказа «created → paid → shipped» в реальном времени (Pub/Sub broadcast).
- Флаг в hash переключает канал уведомлений на лету.
- «Больница»: обработчик падает намеренно без XACK → XAutoClaim
(MinIdle 2 мин) дообрабатывает; после 3 попыток — стрим
orders:dlq(DLQ-паттерн курса Redis, М3).
🏃 Sprint 4: Watermill — один интерфейс, три драйвера
Объём: бизнес-хендлеры (Payment/Inventory/Notification) переводятся на Watermill; драйверы — amqp, kafka, nats; роутер с middleware (retry, poison queue, correlation). CQRS-хелперы для command bus оркестратора.
Чекпоинт:
- Ни один хендлер не импортирует брокерный SDK — только
message.Message. - In-memory (GoChannel) драйвер: юнит-тесты всей Saga-логики без docker.
- Poison-тест: битый payload → poison-очередь, сервис жив (middleware).
- Интеграционные тесты гоняются против всех трёх драйверов одним набором тест-кейсов.
Честная цена шага: часть ручек брокеров (retention-политики Redis, acks-настройки Kafka) придётся спрятать за конфигурацией драйверов — это осознанный trade-off абстракции (5.4), зафиксируйте его в README проекта.
🏃 Sprint 5: Resilience — устойчивость как код
Объём: собираем паттерны (Модуль 2 курса «Фундамент») воедино по всем сервисам.
| Компонент | Что должно появиться |
|---|---|
| Graceful shutdown | NotifyContext + drain во всех 5 сервисах, budget 25s |
| Retry | экспоненциальный backoff + jitter (6.3), лимит попыток |
| DLQ | RabbitMQ DLX-цепочка, Kafka poison-topic, Redis: счётчик попыток + DLQ-стрим |
| Health checks | liveness (процесс жив) + readiness (брокер/БД доступны) |
| Метрики | 4 золотых метрики в Prometheus на каждый сервис |
| Circuit breaker | на вызовах downstream (Payment → bank-stub) |
Чекпоинт:
-
kill -TERMкаждому сервису под нагрузкой: ноль потерянных заказов (проверка сверки «создано = оплачено = зарезервировано»). - K8s-манифесты (или compose-аналог) с readiness/liveness.
- Grafana-дашборд: lag, DLQ rate, p99 processing, in-flight — по сервисам.
- Алерт на рост lag > 2 мин и DLQ rate > 0.1%.
🏃 Sprint 6: нагрузочное тестирование
Объём: Vegeta для HTTP-фронта, k6 для сценария «заказ → оплата», замеры p50/p95/p99, throughput; узкие места по методике Модуля 3.
bash# Vegeta: 200 rps в течение 60 секунд echo "POST http://localhost:8080/orders" | \ vegeta attack -duration=60s -rate=200/s -body=order.json | \ vegeta report
javascript// k6: сценарий с проверкой сквозного статуса import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '30s', target: 50 }, { duration: '1m', target: 200 }, { duration: '30s', target: 0 }, ], }; export default function () { const res = http.post('http://localhost:8080/orders', JSON.stringify({ amount: 1000 }), { headers: { 'Content-Type': 'application/json' } }); check(res, { 'created': (r) => r.status === 201 }); sleep(1); }
Чекпоинт:
- Против всех трёх брокеров одним и тем же профилем нагрузки.
- График p99 latency и throughput; метрики не врут (сверка счётчиков Prometheus с отчётом Vegeta).
- Найдено и устранено одно узкое место (и описано в README: что было bottleneck'ом и почему).
🔍 OpenTelemetry: трейс события от продюсера до консьюмера
Связка трейсов через брокер — пропагация trace-context в заголовках сообщения. Продюсер инжектит, консьюмер экстрактит — цепочка спанов сшивается в один трейс.
goimport ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) var propagator = otel.GetTextMapPropagator() // TraceContext + Baggage // продюсер: публикуем с trace-контекстом func PublishWithTrace(ctx context.Context, publish func(map[string]string) error) error { sc := trace.SpanContextFromContext(ctx) hdrs := map[string]string{} propagator.Inject(ctx, propagation.MapCarrier(hdrs)) _ = sc return publish(hdrs) // hdrs едут в Headers сообщения } // консьюмер: продолжаем трейс func HandleWithTrace(ctx context.Context, hdrs map[string]string, handle func(context.Context) error) error { parent := propagator.Extract(context.Background(), propagation.MapCarrier(hdrs)) ctx2, span := tracer.Start(parent, "consumer.process") defer span.End() return handle(ctx2) }
В одном трейсе в Jaeger/Tempo увидите: HTTP POST → outbox-запись → publish → consume → payment → saga → notify. Это не только красота: по трейсу ищут, на каком хопе «зависли» заказы (и это же — готовый артефакт защиты проекта).
📦 Финальный артефакт
textorderflow/ ├── docker-compose.yml # 3 брокера + Postgres + OTel + Prometheus + Grafana + Jaeger ├── services/ │ ├── gateway/ # HTTP-фронт │ ├── order/ # владелец заказа + outbox │ ├── payment/ # RabbitMQ consumer + breaker │ ├── inventory/ # Kafka consumer group + проекция │ ├── notification/ # Redis Streams + Pub/Sub + WebSocket │ └── saga-orchestrator/ # машина состояний ├── pkg/watermillx/ # общие middleware (retry, poison, otel) ├── dashboards/ # Grafana JSON ├── loadtest/ # vegeta + k6 сценарии └── README.md # архитектура, решения, trade-offs, бенчмарки
🧾 Чек-лист код-ревью (для самопроверки перед «защитой»)
| # | Вопрос | Где должно быть |
|---|---|---|
| 1 | Никаких publish в HTTP-ручке — только через outbox | order service |
| 2 | Все консьюмеры: manual ack, конечный prefetch/MaxAckPending | каждый consumer |
| 3 | Ретраи: ограничены, с backoff+jitter, потом DLQ | pkg/middleware |
| 4 | Идемпотентность: ключ у каждого эффекта (БД/письмо/пуш) | каждый handler |
| 5 | Graceful shutdown: drain + порядок закрытия | каждый main |
| 6 | Метрики: 4 золотых + business-счётчики | каждый сервис |
| 7 | Трейс-контекст едет в заголовках сообщений | pkg/watermillx |
| 8 | Секреты не в коде: env/файлы, в лог не пишутся | config |
| 9 | Порядок по ключу не ломается параллелизмом | inventory/saga |
| 10 | Тесты: unit (in-memory) + integration (docker) + load | CI |
🧩 Quiz "Проверь себя"
✅ Итоги модуля
После прохождения ты должен уметь:
- Собрать микросервисный MVP с outbox, Saga-оркестратором и тремя брокерами из docker-compose одной командой.
- Проводить спринты с чекпоинтами: каждый — с тестируемым инвариантом, а не «на глаз».
- Связать сквозной трейс через заголовки сообщений (OpenTelemetry) и пользоваться им при разборе инцидентов.
- Прогнать нагрузочный профиль Vegeta/k6, прочитать p99 и найти узкое место.
- Пройти свой код по чек-листу ревью до того, как это сделает кто-то другой.
Шпаргалка модуля
| Артефакт | Инструмент | Критерий готовности |
|---|---|---|
| Атомарные события | Outbox + SKIP LOCKED | тест «брокер down — ноль потерь» |
| Порядок по заказу | Kafka key=order_id | тест на партицию |
| Realtime-статусы | Redis Pub/Sub + WebSocket | пуш < 100 мс |
| Дедуп уведомлений | Nats-Msg-Id | тест дубля |
| Один интерфейс | Watermill | нет SDK-импортов в хендлерах |
| Наблюдаемость | OTel + Prometheus + Grafana | сквозной трейс + 4 метрики |
| Нагрузка | Vegeta/k6 | отчёт p50/p95/p99 + найденный bottleneck |
🔗 Мостик к Модулю 2
MVP работает — в песочнице. В проде к нему подойдёт кто угодно: поэтому дальше — безопасность брокеров: SASL/SCRAM и TLS-сертификаты, ACL и vhost'ы, сетевая изоляция и секреты в Go. Включим в OrderFlow TLS+SASL для всех трёх брокеров и перестанем использовать guest/guest.