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 позволяет сосредоточиться на логике сервисов, а не на поддержке шины. Что для соло-разработчика — самое важное.