> Зачем создавать отдельный сервис воркер для аналитики вместо обработки в ордер сервисе (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Ozon

Стек: Go

> Пример ответа

Короткий ответ

Отдельный воркер для аналитики нужен, чтобы изолировать тяжёлые вычисления от критического пути обработки заказов. Ордер сервис должен отвечать быстро и предсказуемо, а аналитика часто требует агрегации, чтения больших объёмов данных или обращения к внешним системам. Разделение позволяет независимо масштабировать компоненты, переживать пиковые нагрузки и не терять события при сбоях - воркер может работать асинхронно через очередь.

Подробное объяснение

Основная причина - разделение ответственности и защита основного бизнес-процесса. Ордер сервис обрабатывает запросы клиентов в реальном времени: валидация, резервирование, сохранение. Любая дополнительная работа - расчёт метрик, построение отчётов, обогащение данными - увеличивает latency и снижает пропускную способность.

Аналитика, как правило, имеет другие требования к консистентности и времени выполнения. Если аналитический запрос упадёт или зависнет, это не должно блокировать оформление заказа. Асинхронная обработка через очередь (например, Kafka, RabbitMQ) даёт гарантию доставки события даже при сбое воркера - событие останется в очереди и будет обработано позже.

Также важна масштабируемость. Нагрузка на аналитику может расти независимо от количества заказов: например, при добавлении новых отчётов или увеличении глубины агрегации. Отдельный воркер позволяет горизонтально масштабировать только аналитический компонент, не трогая ордер сервис.

Ещё один аспект - изоляция сбоев. Если воркер упадёт из-за ошибки в логике аналитики или недоступности внешнего хранилища, ордер сервис продолжит работать. При монолитной обработке один panic или OOM может положить весь сервис.

Наконец, отдельный воркер упрощает lifecycle: можно менять версии аналитики, деплоить её отдельно, проводить A/B тесты и откаты без влияния на основной сервис.

На практике

В Go типичная схема: ордер сервис публикует событие OrderCreated в брокер сообщений. Воркер подписывается на топик, читает событие, обогащает его данными из других сервисов (например, из каталога или CRM) и пишет в аналитическое хранилище (ClickHouse, BigQuery, Elasticsearch).

Важно продумать идемпотентность обработки: событие может прийти повторно, поэтому воркер должен уметь обрабатывать дубликаты. Для этого используют уникальный ключ события или проверку по offset в очереди.

Также стоит настроить retry с backoff и dead letter queue для событий, которые не удалось обработать после нескольких попыток. Это позволяет не терять данные и диагностировать проблемы.

На практике часто используют паттерн outbox: ордер сервис пишет событие в свою БД в той же транзакции, что и заказ, а отдельный процесс читает outbox и публикует в очередь. Это гарантирует атомарность и не теряет события при сбое.

Пример кода

GO
// Ордер сервис: публикация события через outbox
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
order := NewOrder(req)
if err := s.orderRepo.Save(ctx, tx, order); err != nil {
return err
}
event := OrderCreatedEvent{OrderID: order.ID, UserID: order.UserID, Amount: order.Amount}
if err := s.outboxRepo.Add(ctx, tx, event); err != nil {
return err
}
return tx.Commit()
}
// Воркер: чтение из очереди и запись в аналитику
func (w *AnalyticsWorker) Run(ctx context.Context) error {
for {
msg, err := w.consumer.ReadMessage(ctx)
if err != nil {
return err
}
var event OrderCreatedEvent
if err := json.Unmarshal(msg.Value, &event); err != nil {
w.logger.Error("failed to unmarshal event", "error", err)
continue
}
if err := w.processEvent(ctx, event); err != nil {
// retry с backoff, при исчерпании - в DLQ
w.logger.Error("failed to process event", "order_id", event.OrderID, "error", err)
continue
}
w.consumer.CommitMessage(msg)
}
}
func (w *AnalyticsWorker) processEvent(ctx context.Context, event OrderCreatedEvent) error {
enriched, err := w.enricher.Enrich(ctx, event)
if err != nil {
return err
}
return w.analyticsRepo.Save(ctx, enriched)
}

Как отвечать на собеседовании

Начни с главной причины - защита критического пути. Затем перечисли остальные: масштабируемость, изоляция сбоев, независимый деплой. Упомяни асинхронность и очередь как ключевой механизм. Покажи, что понимаешь trade-off: появляется дополнительная инфраструктура, сложнее отладка, нужна идемпотентность. Если спросят про альтернативы - скажи, что для простых случаев можно использовать фоновые горутины в том же сервисе, но это не масштабируется и не даёт гарантий доставки.

Что проверяет интервьюер

Интервьюер хочет увидеть, что ты понимаешь архитектурные принципы: разделение ответственности, асинхронную коммуникацию, отказоустойчивость. Важно, чтобы ты не просто назвал причины, а объяснил последствия для системы в целом. Также проверяется знание паттернов: outbox, retry, DLQ, идемпотентность. Для senior уровня ожидается, что ты можешь обосновать выбор и указать на ограничения.

Типичные ошибки

  • Ответ "чтобы не тормозить ордер сервис" без деталей - слишком поверхностно.
  • Предложение обрабатывать аналитику синхронно в том же сервисе, если "она быстрая" - не учитывает рост нагрузки.
  • Игнорирование проблемы дубликатов и потери событий.
  • Утверждение, что воркер всегда нужен - в простых случаях достаточно фоновой обработки, но важно объяснить, когда это оправдано.
  • Неупоминание outbox или аналогов для гарантии доставки - это критично для senior.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью