Сервис доставки еды
Гонка событиймай 2026
Событие об оплате приходило раньше, чем заказ успевал создаться
Order Service публикует order.created и order.paid в NATS JetStream, а Delivery Service создаёт доставку по первому и назначает курьера по второму. Когда сервис запустили в двух подах, order.paid иногда обрабатывался раньше order.created. Обработчик не находил доставку, помечал сообщение как ошибочное и отбрасывал его навсегда. Заказ оставался оплаченным, но курьер его не получал.
Go-микросервисы и инфраструктура как код
Сервисы общаются по gRPC, асинхронные события идут через NATS JetStream. Каждый сервис по Clean Architecture со своей PostgreSQL. Кластер DOKS из двух узлов поднимается через Terraform, для нагрузочного теста есть сценарий на Locust.
- Go
- gRPC
- NATS JetStream
- PostgreSQL
- Redis
- Nuxt
- Kubernetes
- Terraform
// Term = don't redeliver; Nak = retry.
// ErrNotFound is retried: order.paid can arrive
// before order.created is processed.
if errors.Is(err, usecase.ErrInvalidDelivery) {
_ = msg.Term()
} else {
_ = msg.Nak()
}- 01 / 03
Очередь потребителей вместо эксклюзивной подписки
Перевёл js.Subscribe на QueueSubscribe с группой delivery-workers, чтобы несколько подов делили один поток событий.
- 02 / 03
Разделение постоянных и временных ошибок
Невалидное сообщение отбрасывается через Term(). «Доставка не найдена» теперь временная ошибка: Nak() возвращает сообщение в очередь, и оно приходит снова, когда order.created уже обработан.
- 03 / 03
Лимит повторов и асинхронная почта
MaxDeliver(10) не даёт сообщению крутиться бесконечно. Письма клиенту ушли в отдельную горутину, чтобы SMTP не держал обработчик событий.
5 коммитов
Что бы сделал иначе
Порядок событий нельзя считать гарантированным, как только потребителей больше одного. Гонку я нашёл, только когда сервис запустили в двух подах. Сейчас я бы с самого начала сделал обработчики идемпотентными и описал состояния заказа явно.