> Как модифицировать архитектуру микросервиса, чтобы обеспечить надежность обработки заказов при падении аналитического сервиса (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: Ozon
Стек: Go
> Пример ответа
Короткий ответ
Надёжность обработки заказов при падении аналитического сервиса достигается через отделение критического пути от некритического: аналитика должна потреблять события асинхронно, через брокер сообщений или outbox-паттерн, а не участвовать в синхронном запросе. Заказ фиксируется в своей БД, после чего публикуется событие; аналитический сервис становится потребителем, и его недоступность не влияет на приём заказов. Дополнительно - ретраи, dead letter queue и идемпотентность.
Подробное объяснение
Проблема: если аналитический сервис вызывается синхронно внутри обработки заказа (например, для валидации, обогащения или записи метрик), его падение приводит к ошибке или таймауту всего запроса. Клиент не получает подтверждение, заказ может быть потерян или продублирован.
Решение - разделение на две части:
- Критический путь - приём заказа, валидация, сохранение в собственную БД, возврат ответа клиенту. Здесь участвуют только сервис заказов и его хранилище.
- Некритический путь - аналитика, аудит, метрики, рекомендации. Всё, что можно сделать позже, делается асинхронно.
Архитектурные варианты:
- Брокер сообщений (Kafka, RabbitMQ, NATS): сервис заказов публикует событие
order_createdпосле коммита транзакции. Аналитический сервис подписан на топик. Если он лежит - события накапливаются в брокере, после восстановления он их дочитает. - Outbox-паттерн: в той же транзакции, что и запись заказа, пишется запись в таблицу
outbox. Отдельный релизер (в Go - горутина с ticker) читает outbox и отправляет события в брокер. Это гарантирует, что событие не потеряется даже при падении между записью заказа и публикацией. - Полный отказ от синхронных вызовов: аналитический сервис не вызывается из обработчика заказа вообще. Он читает данные из своей копии (например, из CDC-стрима) или из событий.
Дополнительные меры надёжности:
- Ретраи с экспоненциальной задержкой на стороне потребителя.
- Dead letter queue - если событие не обработалось после N попыток, оно уходит в DLQ для ручного разбора, но не блокирует основной поток.
- Идемпотентность - аналитический сервис должен переживать повторную доставку события (ключ -
order_id). - Graceful degradation - если аналитика нужна для ответа клиенту (например, расчёт стоимости), то при её недоступности можно вернуть ответ с дефолтными значениями или пометить заказ как "требует уточнения".
На практике
В Go это выглядит так:
- Сервис заказов:
POST /orders→ валидация → запись в PostgreSQL (заказ + outbox в одной транзакции) → ответ 201. - Отдельная горутина (или отдельный сервис
order-relayer) читает outbox, отправляет в Kafka, помечает отправленным. - Аналитический сервис: consumer из Kafka, обработка с идемпотентностью по
order_id, ретраи черезsaramaилиsegmentio/kafka-go.
Важно: не использовать синхронный HTTP-вызов к аналитике внутри обработчика. Если аналитика нужна для бизнес-логики (например, скоринг), то либо выносим её в отдельный шаг с асинхронным подтверждением, либо делаем fallback.
Пример кода
GO// Обработчик создания заказаfunc (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {order := &Order{ID: uuid.New(), Status: "created", ...}tx, err := s.db.BeginTx(ctx, nil)if err != nil {return nil, err}defer tx.Rollback()// 1. Пишем заказif err := s.repo.InsertOrder(ctx, tx, order); err != nil {return nil, err}// 2. Пишем в outbox в той же транзакцииoutbox := OutboxEvent{ID: uuid.New(),Aggregate: "order",EventType: "order_created",Payload: order,CreatedAt: time.Now(),}if err := s.repo.InsertOutbox(ctx, tx, outbox); err != nil {return nil, err}// 3. Коммит - только после этого событие может быть отправленоif err := tx.Commit(); err != nil {return nil, err}return order, nil}// Релизер outbox - отдельная горутинаfunc (s *OrderService) RunOutboxRelayer(ctx context.Context) {ticker := time.NewTicker(500 * time.Millisecond)defer ticker.Stop()for {select {case <-ctx.Done():returncase <-ticker.C:events, err := s.repo.GetUnsentOutbox(ctx, 100)if err != nil {log.Printf("outbox read error: %v", err)continue}for _, ev := range events {if err := s.producer.Publish(ctx, "orders", ev); err != nil {log.Printf("publish failed: %v", err)continue // оставляем как unsent, повторим позже}if err := s.repo.MarkOutboxSent(ctx, ev.ID); err != nil {log.Printf("mark sent failed: %v", err)}}}}}
Как отвечать на собеседовании
Начни с ключевой идеи: аналитика не должна быть на критическом пути. Затем опиши два подхода - брокер и outbox, и объясни, почему outbox предпочтительнее для гарантии доставки. Упомяни идемпотентность и DLQ как обязательные элементы. Если спросят про альтернативы - скажи про CDC (Debezium) или event sourcing. Не углубляйся в детали реализации конкретного брокера, если не просят. Заверши trade-off: асинхронность добавляет сложность (консистентность в конечном счёте, дубликаты), но радикально повышает доступность.
Что проверяет интервьюер
- Понимание разницы между синхронной и асинхронной интеграцией.
- Знание паттернов: outbox, transactional outbox, event-driven architecture.
- Умение проектировать отказоустойчивость: ретраи, DLQ, идемпотентность.
- Понимание trade-off: согласованность vs доступность (CAP).
- Практический опыт с Go: горутины, транзакции, работа с брокерами.
Типичные ошибки
- Предлагать синхронный вызов с таймаутом и ретраем - это не решает проблему, а лишь откладывает её.
- Игнорировать outbox и полагаться только на публикацию в брокер после коммита - есть окно потери события.
- Не упоминать идемпотентность - дубликаты событий неизбежны.
- Говорить "просто добавь очередь" без объяснения, как гарантировать доставку и порядок.
- Забывать про DLQ - без него "мёртвые" события будут вечно крутиться в ретраях.
> Похожие задачи по Go
Что такое condition variable в синхронизации
Что такое полиморфизм в Go
Как написать запрос, который выберет названия всех книг с более чем тремя авторами
Какой индекс построить для запроса с условиями на несколько столбцов
> Похожие задачи по backend
Что такое condition variable в синхронизации
Что такое полиморфизм в Go
Как написать запрос, который выберет названия всех книг с более чем тремя авторами
Какой индекс построить для запроса с условиями на несколько столбцов
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью