Это первая статья серии «Погружение в NATS». В обзорной статье о выборе брокера NATS уже появлялся как «лёгкий messaging layer»: pub/sub, request/reply, queue groups, минимальный operational footprint. Здесь — подробно, но без магии: если вы уже работали с RabbitMQ или Kafka, у вас есть вся нужная интуиция, просто термины и границы модели немного другие.
Заход простой: вы знаете exchange, routing key, binding, consumer group, topic и partition. NATS — не наследник ни одной из этих моделей и не их упрощение. Это отдельная система с одной центральной идеей — subject — и она закрывает часть задач RabbitMQ и Kafka с гораздо меньшей операционной ценой. Цена в ответ — это то, чего NATS сознательно не делает: Core NATS ничего не хранит и ничего не гарантирует после отправки. Это не недостаток, а осознанная граница модели, и её важно понимать до того, как заводить NATS в продакшн.
В статье
Один бинарник и модель
nats-server — один статически собранный бинарник без внешних зависимостей. Никакого Erlang-рантайма как у RabbitMQ, никакого ZooKeeper/KRaft-контроллера и JVM как у Kafka. Поднять сервер — это скопировать бинарник (или образ nats:2.12) и запустить его с портом 4222. Именно этим NATS и славится: минимальный operational footprint, быстрый старт, предсказуемое поведение под нагрузкой.
Небольшая оговорка про версии на всю серию: команды, флаги, имена метрик и поведение сервера здесь проверены на nats-server 2.12.x, и все demo-стенды пиновано именно к нему. Это сознательный выбор, а не «последняя версия»: на момент выхода серии актуальна уже ветка 2.14.x (2.13 в NATS пропустили). Модель и API, о которых идёт речь, между этими версиями стабильны, но если вы на 2.14 — конкретные флаги и нюансы стоит сверять с upgrade notes NATS.
Протокол — текстовый, поверх TCP: клиент видит PUB, SUB, MSG, +OK в открытом виде, это упрощает отладку и понимание того, что происходит на проводе. Клиентские библиотеки существуют для всех популярных языков, но в этой статье все примеры — через nats CLI, чтобы модель была видна без кода.
Ключевое разделение, о котором нужно знать с первой минуты: Core NATS vs JetStream. Core — это то, что описано в этой статье: pub/sub, request/reply, queue groups, работающие поверх subjects. Core ничего не хранит: нет подписчика в момент публикации — сообщения больше нет, максимум-once доставка “as is”. JetStream — надстройка над тем же сервером, добавляющая persistence, ретенцию, replay и at-least-once гарантии — то есть закрывающая ровно те задачи, которые в обзорной статье относили к Kafka и RabbitMQ. JetStream — тема второй статьи серии; здесь она сознательно вне scope, чтобы не размывать модель Core.
Subjects и wildcards
В RabbitMQ маршрутизация — это exchange, binding и routing key. В Kafka — topic и partition, без иерархии внутри имени топика. В NATS всё это заменяет одна концепция — subject: строка из токенов, разделённых точкой, например orders.eu.new. Это одновременно и адрес назначения (publisher его использует как есть), и шаблон подписки (subscriber может подставить wildcard).
Два wildcard-токена:
*— совпадает ровно с одним токеном.orders.*.newсовпадёт сorders.eu.newиorders.us.new, но не сorders.eu.retail.new.>— совпадает с одним или несколькими токенами, и может стоять только в конце subject.orders.eu.>совпадёт сorders.eu.newи сorders.eu.retail.new.urgent.
Publisher всегда публикует в полностью специфицированный subject — без wildcard. Wildcard имеет смысл только на стороне подписки.
graph TD
R["orders"] --> EU["orders.eu"]
R --> US["orders.us"]
EU --> EUNEW["orders.eu.new"]
EU --> EURET["orders.eu.retail"]
EURET --> EURETURG["orders.eu.retail.urgent"]
US --> USNEW["orders.us.new"]
S1["Подписка: orders.*.new"] -.->|matches| EUNEW
S1 -.->|matches| USNEW
S2["Подписка: orders.eu.>"] -.->|matches| EUNEW
S2 -.->|matches| EURET
S2 -.->|matches| EURETURG
style S1 fill:#f9f3e3,stroke:#8b7355
style S2 fill:#f9f3e3,stroke:#8b7355
style EUNEW fill:#c9e4c5,stroke:#5b8a5e
style USNEW fill:#c9e4c5,stroke:#5b8a5e
style EURET fill:#c9e4c5,stroke:#5b8a5e
style EURETURG fill:#c9e4c5,stroke:#5b8a5e
Мост от RabbitMQ и Kafka. Ближе всего по духу — topic exchange в RabbitMQ: routing key orders.eu.new и binding pattern orders.*.new (* — один токен) или orders.eu.# (# — ноль и более токенов). Разница в основном терминологическая: * в NATS ведёт себя как * в RabbitMQ, а > — как #, за вычетом того, что # в RabbitMQ может матчить и ноль токенов, а > в NATS — только один и более. В Kafka такой иерархии в имени топика вообще нет: orders.eu.new — это просто строка-идентификатор топика, а не путь для wildcard-матчинга; если нужна маршрутизация по региону, её обычно реализуют отдельными топиками или полем в сообщении, а не подстрокой имени.
Pub/Sub
Базовый паттерн Core NATS — fire-and-forget: publisher отправляет сообщение в subject, сервер рассылает его всем текущим подписчикам этого subject и тут же забывает о сообщении. Это at-most-once по конструкции, не по настройке: нет подтверждений, нет retry на уровне сервера, нет очереди на диске.
Прямое следствие: если в момент публикации нет ни одного подписчика — сообщение исчезает безвозвратно. Это не баг и не край случай, а нормальная работа Core NATS. Если приложению нужно гарантированно получить сообщения, отправленные до его старта, — это уже задача для JetStream (ст. 2), а не для Core.
# Терминал 1 — подписка на всё дерево orders.>
nats sub "orders.>"
# Терминал 2 — публикация
nats pub orders.eu.new '{"id":1}'
Мост от RabbitMQ и Kafka. В RabbitMQ и Kafka сообщение переживает отсутствие потребителя: очередь хранит его до ack, топик Kafka хранит его по retention policy независимо от того, читает ли его кто-то прямо сейчас. В Core NATS такого буфера нет вообще — это ближе к Redis Pub/Sub, где сообщение тоже теряется, если некому его принять. Если вы привыкли, что “брокер сообщений” по умолчанию что-то хранит, это первое место, где интуиция от RabbitMQ/Kafka может подвести.
Queue groups
Если несколько подписчиков регистрируются на один subject с одинаковым именем queue group, сервер доставит каждое сообщение только одному случайно выбранному участнику группы — а не всем сразу, как в обычном pub/sub. Это встроенный механизм competing consumers, без дополнительной настройки на стороне сервера: имя группы задаёт само приложение при подписке.
# Терминал 1 и терминал 2 — два подписчика в одной группе workers
nats sub --queue workers jobs.*
# Терминал 3 — публикация нескольких сообщений
nats pub jobs.a x
nats pub jobs.a x
nats pub jobs.a x
(queue: workers) participant C2 as Subscriber 2
(queue: workers) P->>S: pub jobs.a "x" S->>C1: msg (случайный выбор) P->>S: pub jobs.a "x" S->>C2: msg (случайный выбор) P->>S: pub jobs.a "x" S->>C1: msg (случайный выбор)
sequenceDiagram
participant P as Publisher
participant S as nats-server
participant C1 as Subscriber 1
(queue: workers)
participant C2 as Subscriber 2
(queue: workers)
P->>S: pub jobs.a "x"
S->>C1: msg (случайный выбор)
P->>S: pub jobs.a "x"
S->>C2: msg (случайный выбор)
P->>S: pub jobs.a "x"
S->>C1: msg (случайный выбор)
Мост от Kafka и RabbitMQ. Ближайший аналог по назначению — consumer group в Kafka: несколько consumer читают один топик, и каждое сообщение достаётся только одному из них (в Kafka это ещё жёстко привязано к партициям, в NATS распределение внутри группы не партиционировано и не гарантирует порядок между подписчиками). В RabbitMQ то же самое поведение получается по-другому: несколько consumer подключаются к одной и той же очереди, и брокер раздаёт им сообщения round-robin с учётом prefetch — там queue group не отдельная концепция, а просто следствие модели “одна очередь — много consumer”. В NATS это явный примитив на уровне подписки, а не побочный эффект топологии очередей.
Request/Reply
В RabbitMQ и Kafka request/reply — это паттерн, который приходится собирать руками: producer публикует сообщение с полем reply-to (обычно временная очередь в RabbitMQ) и correlation-id, consumer обрабатывает и публикует ответ в reply-to, а исходный producer сопоставляет ответ по correlation-id. Работает, но требует явной инфраструктуры: временные очереди, TTL на них, обработку “ответ не пришёл”.
В NATS request/reply — часть протокола, а не паттерн поверх него. nats request создаёт уникальный inbox-subject, публикует запрос с этим subject в заголовке “куда отвечать”, ждёт первый ответ на inbox и завершает вызов. Отвечающая сторона просто подписывается на subject запроса — обычно через nats reply, который автоматически входит в queue group NATS-RPLY-22, так что несколько экземпляров сервиса можно поднять без лишней настройки.
# Терминал 1 — сервис, отвечающий на запросы
nats reply service.time '{{Time}}'
# Терминал 2 — запрос, ждёт первый ответ
nats request service.time ''
sequenceDiagram
participant Req as Requester
participant S as nats-server
participant Rep as Replier (nats reply)
Req->>S: SUB _INBOX.uid (авто)
Req->>S: PUB service.time reply-to=_INBOX.uid
S->>Rep: MSG service.time reply-to=_INBOX.uid
Rep->>S: PUB _INBOX.uid 12:34:56
S->>Req: MSG _INBOX.uid 12:34:56
Мост от RabbitMQ и Kafka. В RabbitMQ это ручной reply-to + correlation-id (с временной exclusive-очередью или паттерном direct reply-to). В Kafka встроенного request/reply вообще нет — есть только два независимых топика (запросов и ответов) плюс сопоставление по ключу или заголовку на уровне приложения, и задержка здесь заметно выше, чем у синхронного RPC. В NATS то же самое — одна строка в CLI и один вызов клиентской библиотеки, потому что inbox и сопоставление ответа встроены в протокол.
Демонстрация
Рабочий стенд — nats/00-core в digital-cookbook. Один узел NATS (образ nats:2.12), без JetStream — только то, что описано в этой статье.
git clone https://github.com/khorost-tech/digital-cookbook.git
cd digital-cookbook/nats/00-core
docker compose up -d
nats CLI ставится отдельно от сервера (nats-io/natscli), после чего один раз настраивается контекст на локальный сервер:
nats context save local --server localhost:4222 --select
Дальше — три сценария из README стенда, в двух-трёх терминалах каждый: pub/sub на orders.>, request/reply на service.time, queue group workers на jobs.*. Все команды и ожидаемое поведение — в README демо.
Вывод
Core NATS — это транспорт, а не хранилище. Subject заменяет exchange/routing key и topic одной иерархической моделью с двумя wildcard-токенами. Pub/sub — fire-and-forget и at-most-once по конструкции: нет подписчика — нет сообщения. Queue groups дают competing consumers без отдельной инфраструктуры. Request/reply встроен в протокол, а не собирается руками поверх reply-to и correlation-id.
Это делает Core NATS отличным выбором для service-to-service messaging, где скорость и простота важнее гарантий доставки. Как только нужна persistence, ретенция и replay — это уже другой слой поверх того же сервера. Про JetStream — во второй статье серии.
Комментарии