Модуль 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 shutdownNotifyContext + drain во всех 5 сервисах, budget 25s
Retryэкспоненциальный backoff + jitter (6.3), лимит попыток
DLQRabbitMQ DLX-цепочка, Kafka poison-topic, Redis: счётчик попыток + DLQ-стрим
Health checksliveness (процесс жив) + 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 в заголовках сообщения. Продюсер инжектит, консьюмер экстрактит — цепочка спанов сшивается в один трейс.

go
import (
	"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. Это не только красота: по трейсу ищут, на каком хопе «зависли» заказы (и это же — готовый артефакт защиты проекта).

📦 Финальный артефакт

text
orderflow/
├── 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-ручке — только через outboxorder service
2Все консьюмеры: manual ack, конечный prefetch/MaxAckPendingкаждый consumer
3Ретраи: ограничены, с backoff+jitter, потом DLQpkg/middleware
4Идемпотентность: ключ у каждого эффекта (БД/письмо/пуш)каждый handler
5Graceful shutdown: drain + порядок закрытиякаждый main
6Метрики: 4 золотых + business-счётчикикаждый сервис
7Трейс-контекст едет в заголовках сообщенийpkg/watermillx
8Секреты не в коде: env/файлы, в лог не пишутсяconfig
9Порядок по ключу не ломается параллелизмомinventory/saga
10Тесты: unit (in-memory) + integration (docker) + loadCI

🧩 Quiz "Проверь себя"

1. Партиционирование orders.events по order_id даёт...
2. p50 = 20 мс, p99 = 4 сек. Какой вывод корректен?
3. Readiness vs liveness для консьюмера:
4. Почему Order Service публикует order.created через Outbox, а не напрямую из HTTP-ручки?
5. Как трейс события связывается от продюсера до консьюмера?
6. Какие элементы входят в спринт S5 (Resilience)? — выберите все подходящие
7. Почему Notification Service на Redis (Pub/Sub + Streams), а не только на Kafka?
8. Что даёт Watermill в спринте S4?
9. Зачем в учебном MVP сразу три брокера?
10. Payment списал деньги, Inventory не смог зарезервировать. Что делает Saga Orchestrator?

✅ Итоги модуля

После прохождения ты должен уметь:

  • Собрать микросервисный 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.