Микросервисов становится больше, и между ними нужны события: заказ оформили, платёж прошёл, товар отгрузили. Можно поставить Kafka или RabbitMQ, но каждый из них - это отдельный кластер со своей эксплуатацией, своими настройками и своим ценником по памяти. А если событий много, но они простые - «уведоми всех, кому интересно» - поднимать тяжёлый брокер все равно что ездить за хлебом на грузовике.
NATS решает эту задачу одним бинарником меньше десяти мегабайт. Он не хранит события месяцами и не умеет в exchange-типы, но доставляет миллионы сообщений в секунду и запускается за секунду. Его часто ставят там, где Kafka и RabbitMQ избыточны: внутренняя шина микросервисов, IoT-телеметрия, edge-вычисления.
Subject: адрес без очередей
Subject - строка с точками: orders.new.eu. Подписчик матчит тему или wildcard.
В RabbitMQ адрес события - это exchange плюс routing key плюс binding. В Kafka - topic и партиция. В NATS адрес - subject, одна строка с точками: orders.new.eu. Издатель отправляет событие на эту строку, и NATS доставляет его всем, чья подписка совпадает.
Совпадение бывает трёх видов:
- точное: подписка
orders.new.euловит толькоorders.new.eu; - звёздочка
*замещает одно слово: подпискаorders.*.euловитorders.new.euиorders.paid.eu; >замещает все слова дальше: подпискаorders.>ловит любой subject, начинающийся сorders..
Никаких обменников и привязок настраивать не надо: subject сам себе routing key. Хотите слушать всё про заказы - orders.>. Хотите только новые из Европы - orders.new.eu.
Pub/sub: пожарить и забыть
Базовый режим - core NATS: издатель шлёт сообщение, NATS доставляет его всем активным подписчикам и забывает. Это at-most-once: если подписчика нет в момент отправки, сообщение не ждёт - оно просто исчезает.
nc, _ := nats.Connect(nats.DefaultURL)
defer nc.Drain()
nc.Publish("orders.new.eu", []byte(`{"id": 42, "amount": 1500}`))
import nats, json
nc = await nats.connect()
await nc.publish("orders.new.eu", json.dumps({"id": 42, "amount": 1500}).encode())
Это не недостаток, а конструкция: core NATS задуман для живой шины, где событие ценно сейчас и теряет смысл через секунду. Телеметрия IoT, логи, метрики, уведомления интерфейса - если подписчик был офлайн, он всё равно не догонит упущенное.
Но если сообщение должно доехать даже до offline-подписчика, нужен JetStream - о нём ниже.
Queue group: балансировка
Когда один сервис запущен в трёх копиях, каждое сообщение должно обработать одну копию, а не все три. В RabbitMQ для этого несколько потребителей слушают одну очередь. В Kafka - consumer group. В NATS - queue group.
nc.QueueSubscribe("orders.>", "billing-workers", func(m *nats.Msg) {
process(m.Data)
})
async def handler(msg):
await process(msg.data.decode())
await nc.subscribe("orders.>", queue="billing-workers", cb=handler)
Три копии биллинга с queue="billing-workers" получают события по очереди: NATS сам решает, кому отправить следующее. Подписчики вне queue group продолжают получать все события - как обычно.
Request-reply: вопрос и ответ
В RabbitMQ для request-reply нужно создать ответную очередь и correlation id. В Kafka - целый протокол поверх topic. В NATS это встроенный паттерн:
msg, err := nc.Request("user.get", []byte("42"), 2*time.Second)
if err == nil {
fmt.Println(string(msg.Data))
}
msg = await nc.request("user.get", b"42", timeout=2)
print(msg.data.decode())
Издатель шлёт запрос, NATS подставляет inbox-тему для ответа, responder делает m.Respond(data). Одна строка вместо настройки очередей. Это делает NATS удобным для синхронных вызовов между микросервисами, когда HTTP кажется тяжёлым, а RPC-фреймворк - избыточным.
JetStream: персистентность
Core NATS не хранит сообщения. JetStream - встроенная надстройка, которая добавляет персистентность и гарантии at-least-once:
JetStream: stream хранит события в журнале, consumer ведёт ack-позицию.
JetStream вводит понятие stream - именованный журнал, который хранит события по фильтру (например, orders.>). На stream подписываются consumer - они ведут свою позицию и подтверждают обработку. Похоже на Kafka: stream = topic + партиция, consumer = consumer group.
js, _ := jetstream.New(nc)
js.CreateStream(ctx, jetstream.StreamConfig{
Subjects: []string{"orders.>"},
Retention: jetstream.LimitsPolicy,
MaxAge: 24 * time.Hour,
})
cons, _ := js.CreateOrUpdateConsumer(ctx, "ORDERS", jetstream.ConsumerConfig{
Durable: "billing",
FilterSubject: "orders.new.>",
})
for msg := range cons.Messages() {
process(msg.Data)
msg.Ack()
}
js = nats.js.Client(nc)
stream = await js.add_stream(name="ORDERS", subjects=["orders.>"])
consumer = await js.add_consumer("ORDERS", durable_name="billing",
filter_subject="orders.new.>")
async for msg in consumer.messages():
await process(msg.data.decode())
await msg.ack()
JetStream поддерживает ограничение хранения по времени (MaxAge), по размеру (MaxBytes) и по количеству. Политика LimitsPolicy удаляет старые записи при превышении лимита - как retention в Kafka.
Когда NATS, а когда тяжёлый брокер
NATS выбирают, когда:
- нужна скорость и лёгкость: один бинарник, миллионы сообщений в секунду, микросекундные задержки;
- события простые: «уведоми всех, кому интересно», без сложной маршрутизации через exchange-типы;
- edge и IoT: устройство с NATS на борту весит меньше, чем JVM для Kafka-клиента;
- request-reply: встроенный паттерн удобнее, чем строить его на RabbitMQ или Kafka.
Тяжёлый брокер (Kafka или RabbitMQ) нужен, когда:
- долгое хранение: события нужны днями и неделями, с replay и перечитыванием;
- гарантии доставки: exactly-once, транзакции, сложные сценарии отката;
- сложная маршрутизация: topic exchange, header exchange, consistent hash;
- экосистема: Kafka Connect, Kafka Streams, Schema Registry.
На практике NATS и тяжёлый брокер не конкурируют, а живут рядом: NATS - как внутренняя шина для событий реального времени, Kafka - как журнал для аналитики и интеграции.
Коротко
- NATS - лёгкий брокер: один бинарник, subject-based адресация, миллионы сообщений в секунду.
- Subject - строка с точками (
orders.new.eu); wildcard*замещает одно слово,>- все остальные. - Core NATS -
at-most-once: сообщение доставляется активным подписчикам и забывается. - Queue group - несколько копий одного сервиса делят нагрузку: каждый получает своё.
- Request-reply - встроенный паттерн «вопрос-ответ» в одну строку.
- JetStream - персистентность поверх core NATS: stream хранит журнал, consumer ведёт ack-позицию, гарантия
at-least-once. - NATS для лёгкой шины и edge; Kafka для журнала и аналитики; RabbitMQ для очередей задач и сложной маршрутизации.
Что почитать дальше
- AMQP vs Kafka — чем очередь отличается от журнала и как выбрать между RabbitMQ и Kafka.
- RabbitMQ в продакшене — Quorum Queues, кластер, гарантии доставки.
- Паттерны обмена сообщениями — saga, outbox, идемпотентность: что меняется при смене брокера.