> Go Батя

NATS в микросервисах — мой опыт

#go #nats #архитектура #микросервисы

Когда я начинал проектировать свою микросервисную платформу, вопрос шины данных был одним из первых. RabbitMQ? Kafka? NATS? Я перебрал все варианты, почитал сравнения, померил — и в итоге выбрал NATS. Прошло больше года. Расскажу, почему не пожалел, и что всплыло в процессе.

Почему не Kafka

Kafka — это зверь. Он хорош, когда у тебя Big Data, команда админов и ты готов платить за железо. Когда у тебя — один разработчик и VPS за 500 рублей, Kafka превращается в головную боль. Даже поднять её в докере и настроить топики с ретеншном — это уже квест. А если нужно, чтобы consumer гарантированно обработал сообщение, а прод не упал, потому что кто-то забыл про размер партиции — начинается цирк.

NATS в этом плане — как швейцарский нож. Поднял, настроил, забыл.

JetStream vs Core NATS

Самое важное, что я понял — это разделение на Core NATS и JetStream. Это не просто два режима одного продукта. Это два совершенно разных паттерна.

Core NATS — это at-most-once. Сообщение доставляется, если есть подписчик. Нет подписчика — сообщение потеряно. Звучит как недостаток, но на самом деле это фича под определённые сценарии. Например, я использую Core NATS для:

  • Уведомлений об изменении состояния (подписчикам всё равно на историю)
  • Пересылки команд, где важен only-one
  • Метрик и трейсинга (упало — и ладно)

JetStream — это уже полноценная очередь с гарантиями. Я использую его для:

  • Сообщений, которые надо гарантированно доставить
  • Event sourcing (события сохраняются и могут быть воспроизведены)
  • Персистентных очередей для долгих операций

А ещё Core NATS — просто зверь по пропускной способности. Да, без гарантий доставки, но во многих тестах обгоняет Kafka существенно. Да и JetStream не всегда и не сильно отстаёт.

Что я вынес за год

1. Не кладите всё в JetStream.

Когда я только разобрался с JetStream, мне захотелось засунуть туда всё. Уведомления о входе пользователя — в JetStream. Изменение профиля — в JetStream. Результат: сложные consumer-группы, лишние ack, потеря производительности. Core NATS для несрочного трафика — это нормально.

2. Структура топиков — это архитектура.

У меня формат: {service}.{domain}.{entity}.{id}.{action}. Например, auth.session.123.created. Это позволило:

  • Однозначно маршрутизировать запросы
  • Использовать wildcard-подписки (>.>) для логирования всего трафика
  • Быстро находить проблемы — grep по логу NATS даёт полную картину

3. NATS не решает проблему стыковки сервисов.

Сам по себе NATS — это просто шина. Он не знает, как сервисы договариваются друг с другом. Пришлось добавить свой слой: заголовки CorrelationID, RequestID, AccountID. Без этого — хаос:

  • Сообщение ушло, а кто его обработал — непонятно.
  • Пришёл ответ, а к какому запросу — неясно.
  • Трейсинг не работает как надо.

Свой контракт для общения через Nats для систем больше блога или списка дел — обязателен, иначе когнитивная сложность системы будет расти с каждым новым узлом или топиком. Заголовки сообщений ­— сильная и удобная фича, не стоит ею пренебрегать.

4. Кластер — это просто.

Поднять кластер NATS из 3 нод — пять минут конфигурации. В отличие от того же Kafka, где кластеризация требует понимания конфигов брокеров, зоокипера, репликации. NATS просто работает.

Поднял новые ноды, указав им адрес существующей — кластер готов. В случае падения ноды-лидера — оставшиеся сами выберут нового лидера и продолжат жить как жили. Подключился к одной ноде, тут же узнал обо всех остальных, в случае падения той, к которой подключался — продолжаешь общаться с соседней без простоя. Ну разве не красота?

Request-Reply из коробки

Это та фича, ради которой стоит попробовать NATS, даже если вы сомневаетесь. В RabbitMQ или Kafka сделать RPC-стиль общения между сервисами — это танец с бубном: отдельная очередь на ответ, correlationId в заголовках, таймауты, обработка сиротских ответов.

В NATS — это встроенный паттерн. Отправляешь запрос с указанием reply-топика, подписчик отвечает, и всё. NATS сам создаёт временный inbox, сам разруливает таймауты. Выглядит так:

// Запрос
msg, err := nc.Request("auth.verify", payload, 2*time.Second)
fmt.Println("Verify result:", string(msg.Data))

// Обработчик
nc.Subscribe("auth.verify", func(msg *nats.Msg) {
    result := verifyUser(msg.Data)
    msg.Respond(result)
})

В микросервисной архитектуре я использую это постоянно: проверка токенов, получение данных профиля, разрешение конфликтов. Фактически, это мой основной паттерн синхронного общения между сервисами — и он не требует никакой дополнительной инфраструктуры.

Что мне не хватает

  • Встроенного rate limiting на уровне топика (пришлось делать свой)
  • Механизма dead letter queues “из коробки” (реализовал через отдельный потребитель)
  • Человеческого мониторинга — метрики есть, но встроенного дашборда нет

Итог

NATS — отличный выбор для микросервисной архитектуры среднего размера. Если у вас от 3 до 20 сервисов, вы не строите data pipeline на сотни гигабайт в день и хотите, чтобы шина просто работала — берите NATS. Если вам нужна сложная маршрутизация с гарантиями и вы готовы платить операционными расходами — RabbitMQ. Если у вас Hadoop и Big Data — Kafka.

А мне NATS позволяет сосредоточиться на логике сервисов, а не на поддержке шины. Что для соло-разработчика — самое важное.