← назад к разделу

Микросервисов становится больше, и между ними нужны события: заказ оформили, платёж прошёл, товар отгрузили. Можно поставить Kafka или RabbitMQ, но каждый из них - это отдельный кластер со своей эксплуатацией, своими настройками и своим ценником по памяти. А если событий много, но они простые - «уведоми всех, кому интересно» - поднимать тяжёлый брокер все равно что ездить за хлебом на грузовике.

NATS решает эту задачу одним бинарником меньше десяти мегабайт. Он не хранит события месяцами и не умеет в exchange-типы, но доставляет миллионы сообщений в секунду и запускается за секунду. Его часто ставят там, где Kafka и RabbitMQ избыточны: внутренняя шина микросервисов, IoT-телеметрия, edge-вычисления.

Subject: адрес без очередей

издатель orders.new.eu NATS матчит темы orders.*.* биллинг orders.new.* аналитика orders.> аудит три подписки поймали одно событие — без exchange и binding RabbitMQ: exchange + binding Kafka: topic + consumer group NATS: subject subject - это и адрес, и фильтр, и топик: точка разделяет уровни orders . new . eu wildcard * замещает одно слово, > - все остальные

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: ORDERS seq 1 seq 2 seq 3 consumer: биллинг ack: seq 2 consumer: аналитика ack: seq 1 stream хранит события, consumers ведут свою позицию не подтвердил - JetStream перешлёт; подтвердил - двигает offset как Kafka: log + consumer offset + ack

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 для очередей задач и сложной маршрутизации.

Что почитать дальше