1. NATS. Легковесный брокер
Философия максимальной простоты и скорости
«Простота — непременное условие надежности. Сложные системы всегда ломаются сложным образом; простые системы, как правило, работают».
— Эдсгер Дейкстра
Философия и происхождение
В мире распределенных систем существует негласное соревнование в сложности: брокеры сообщений обрастают терабайтными логами на дисках, распределенными консенсусами Paxos/Raft, схемами валидации данных и многоуровневыми транзакционными менеджерами. За всё это приходится платить: гигабайтами потребляемой оперативной памяти, десятками миллисекунд сетевых задержек и армией инженеров по эксплуатации.
NATS был рожден как антитеза этой индустриальной громоздкости.
Создатель NATS, Дерек Коллисон (Derek Collison), ранее спроектировавший архитектуру коммерческих систем обмена сообщениями в TIBCO, в 2010 году (в рамках проекта Apcera, с открытием исходного кода в 2012 году) сформулировал радикальную идею: что, если убрать из брокера вообще всё лишнее, оставив только молниеносную доставку байтов по сети в оперативной памяти?
Название проекта происходит от метафоры «NASCAR At Television Speed»: максимальная скорость передачи событий в реальном времени. NATS построен на основе асинхронного publish-subscribe (pub/sub) шаблона, где отправители (publishers) и получатели (subscribers) не связаны напрямую — они взаимодействуют через subject-based routing, что делает систему слабо связанной (loosely coupled).
Физическая модель: радиоэфир вместо почтового склада
Чтобы прочувствовать разницу между традиционными брокерами и Core NATS, проведем физическую аналогию:
- Kafka и RabbitMQ работают как огромный централизованный логистический хаб со складами, стеллажами и сейфами: каждое письмо бережно упаковывается, регистрируется в журнале на диске, кладется на полку и ждет, когда адресат придет за ним с распиской. Это надежно, но требует огромных складских площадей (диск, RAM) и времени.
- Core NATS работает как радиопередатчик или оптоволоконный световой импульс: передатчик вспыхивает фотонами света в эфир (
Publish). Любой приемник, настроенный на эту частоту (Subject), мгновенно улавливает сигнал за микросекунды. Если в момент передачи в комнате никого не было и радиоприемник был выключен — волна бесследно рассеивается в пространстве. Брокер не хранит историю, не ждет подтверждений от отсутствующих подписчиков и не тратит ни одного такта CPU на сохранение на диск!
Архитектура NATS
Центральный элемент NATS — это сервер (nats-server), написанный на языке Go. Он представляет собой компактный статически скомпилированный бинарный файл весом около 15–20 МБ, запускающийся за доли секунды и потребляющий в базовом состоянии менее 20–30 МБ оперативной памяти.
Сервер выступает в роли сверхбыстрого диспетчера (message broker): он принимает входящие сообщения от publisher'ов и мгновенно рассылает их соответствующим subscriber'ам на основе строковых тем — subject'ов.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph ClientsPub["Publishers (Отправители)"]
P1["Order Service"]:::process
P2["User Service"]:::entry
end
subgraph NATSServer["NATS Server (nats-server в RAM)"]
direction TB
subgraph Engine["In-Memory Routing Engine"]
S["Subject Router<br>(Radix Trie в памяти)"]
P["In-Memory Store<br>(Буферы сокетов)"]
end
end
subgraph ClientsSub["Subscribers (Получатели)"]
C1["Billing Service<br>(Sub: 'orders.new')"]
C2["Notification Service<br>(Sub: 'orders.*')"]
C3["Analytics Service<br>(Sub: 'users.update')"]
end
P1 -->|"Publish to 'orders.new'"| S
P2 -->|"Publish to 'users.update'"| S
S -->|"Deliver to 'orders.new'"| C1
S -->|"Deliver to 'orders.*'"| C2
S -->|"Deliver to 'users.update'"| C3
Server в NATS — это stateless компонент, который работает в памяти. Сообщения по умолчанию не persistent, что обеспечивает минимальные задержки, но требует осторожного подхода к гарантиям доставки. Для persistent сообщений существует расширение JetStream (о котором будет подробнее в статье 5. JetStream. Persistence и stream processing).
Core Concepts
Архитектура Core NATS держится на трех китах: субъектах (Subjects), паттерне Publish-Subscribe и синхронном Request-Reply.
1. Subject (Тема)
Subject — это чувствительная к регистру строка в кодировке ASCII, разделенная точками на логические токены. Это аналог топика в Kafka или Routing Key в RabbitMQ, но с более гибкой и быстрой моделью сопоставления.
Примеры иерархических субъектов:
orders.new
orders.completed
users.created
users.profile.updated
news.sports.football
Подписчики могут подписываться как на точные имена субъектов, так и использовать два типа подстановочных символов (wildcards):
- Символ
*(звездочка) — подменяет ровно один уровень (токен) в иерархии. Например, подпискаorders.*будет получать событияorders.newиorders.completed, но не получитorders.new.specialилиorders. - Символ
>(больше) — подменяет все последующие уровни до конца строки (может стоять только в самом конце субъекта). Например, подпискаnews.>будет матчитьnews.sports,news.sports.football,news.tech.aiи любую более глубокую вложенность.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Msg["Сообщение в: news.sports.football"]:::process
W1["Подписка: news.sports.*"]:::process -->|"Матч! (ровно 1 токен)"| R1["Получено!"]
W2["Подписка: news.>"]:::process -->|"Матч! (все уровни)"| R2["Получено!"]
W3["Подписка: news.*"]:::process -->|"Мимо! (пропущен уровень)"| R3["Не получено!"]
Msg -.-> W1
Msg -.-> W2
Msg -.-> W3
2. Publish-Subscribe (Pub/Sub)
Это основной паттерн взаимодействия в NATS. Publisher отправляет сообщение в определённый subject, а все активные подписчики на этот subject получают персональную копию сообщения. Это классический fan-out паттерн: одно сообщение может быть параллельно доставлено десяткам независимых микросервисов.
Пример на Go с использованием официального клиента github.com/nats-io/nats.go:
package main
import (
"fmt"
"log"
"time"
"github.com/nats-io/nats.go"
)
func main() {
// Подключение к серверу (по умолчанию nats://localhost:4222)
nc, err := nats.Connect(nats.DefaultURL)
if err != nil {
log.Fatalf("failed to connect to nats: %v", err)
}
defer nc.Close()
// Subscriber 1: Подписка на точный субъект
sub1, err := nc.Subscribe("orders.new", func(msg *nats.Msg) {
fmt.Printf("[Consumer 1] received: %s (subject: %s)\n", string(msg.Data), msg.Subject)
})
if err != nil {
log.Fatalf("failed to subscribe: %v", err)
}
defer sub1.Unsubscribe()
// Subscriber 2: Второй независимый подписчик (fan-out)
sub2, err := nc.Subscribe("orders.*", func(msg *nats.Msg) {
fmt.Printf("[Consumer 2 - Wildcard] received: %s\n", string(msg.Data))
})
if err != nil {
log.Fatalf("failed to subscribe: %v", err)
}
defer sub2.Unsubscribe()
// Publisher: Отправка события
payload := []byte("Order #123 created for $250")
if err := nc.Publish("orders.new", payload); err != nil {
log.Fatalf("failed to publish: %v", err)
}
// Flush гарантирует, что локальный клиентский буфер сброшен в сокет
if err := nc.FlushTimeout(1 * time.Second); err != nil {
log.Fatalf("flush failed: %v", err)
}
// Даем время горутинам обработать событие
time.Sleep(100 * time.Millisecond)
}
3. Request-Reply (Sync RPC over Async Transport)
В микросервисной архитектуре постоянно возникает необходимость в синхронном получении ответа на запрос (RPC — Remote Procedure Call). Вместо того чтобы поднимать отдельный стек HTTP/REST или gRPC, NATS предоставляет элегантный паттерн Request-Reply прямо поверх асинхронного pub/sub:
sequenceDiagram
autonumber
actor Client as Клиент (Requester)
participant NATS as NATS Server
actor Worker as Воркер (Responder)
Note over Client: Генерирует уникальный обратный адрес:<br>_INBOX.random123
Client->>NATS: SUB _INBOX.random123 (локальная временная подписка)
Client->>NATS: PUB api.get.user [ReplyTo: _INBOX.random123] Data: "user-42"
NATS->>Worker: MSG api.get.user [ReplyTo: _INBOX.random123] Data: "user-42"
Note over Worker: Обработка запроса...
Worker->>NATS: PUB _INBOX.random123 Data: {"name":"John Doe"}
NATS->>Client: MSG _INBOX.random123 Data: {"name":"John Doe"}
Note over Client: Возврат данных приложению.<br>Автоматический Unsubscribe inbox.
Как это работает:
- Клиент вызывает метод
nc.Request(subject, data, timeout). - Клиентская библиотека автоматически генерирует уникальный одноразовый субъект для ответа — так называемый Inbox (например,
_INBOX.x7z9Q1...), подписывается на него и передает его имя в служебном поле сообщенияReply. - Сервер-обработчик получает сообщение, извлекает
msg.Replyи отправляет ответ в этот адрес:nc.Publish(msg.Reply, response). - Клиент получает ответ, отменяет временную подписку и возвращает результат в вызывающий код, укладываясь в заданный таймаут.
// Сервер (Responder)
_, err = nc.Subscribe("api.get.user", func(msg *nats.Msg) {
userID := string(msg.Data)
response := fmt.Sprintf(`{"id": "%s", "name": "John Doe"}`, userID)
// Отправляем ответ на временный subject из поля msg.Reply
if msg.Reply != "" {
_ = nc.Publish(msg.Reply, []byte(response))
}
})
// Клиент (Requester)
msg, err := nc.Request("api.get.user", []byte("123"), 2*time.Second)
if err != nil {
log.Printf("Request failed: %v", err)
return
}
fmt.Printf("Response: %s\n", string(msg.Data))
Преимущества NATS
- Минималистичность: Сервер
nats-serverвесит всего ~15 МБ, потребляет минимум RAM и CPU, запускается за сотые доли секунды в scratch-контейнерах Docker. - Экстремальная производительность: Способен маршрутизировать от 1 до 10 миллионов сообщений в секунду на одном сервере с задержками менее 100 микросекунд.
- Простота деплоя и эксплуатации: Единый бинарный файл, плоский текстовый конфигурационный файл, 0 внешних зависимостей (нет ZooKeeper, нет etcd, нет JVM).
- Легковесные клиенты: Клиентская библиотека на Go не содержит CGO, использует только стандартный пакет
netи весит считанные мегабайты. - Cloud Native & Multi-tenancy: Нативная интеграция с Kubernetes, поддержка Leaf Nodes для Edge/IoT, JWT-аутентификация с децентрализованными учетными записями (NATS 2.0+).
Устройство сообщения NATS
Протокол NATS предельно прост: это текстовый протокол поверх TCP, напоминающий HTTP/1.1 или Redis RESP.
Структура сообщения в Go-клиенте отражает эту спартанскую простоту:
type Msg struct {
Subject string // Тема (субъект) сообщения
Reply string // Субъект для ответа (в request-reply)
Data []byte // Полезная нагрузка (сырые байты)
Sub *Subscription // Указатель на подписку, принявшую сообщение
Header Header // Карта метаданных и трассировки (NATS 2.2+)
}
NATS vs. другие брокеры
| Характеристика | NATS (Core) | RabbitMQ | Apache Kafka |
|---|---|---|---|
| Хранилище данных | In-Memory (RAM) | Persistent на диске (Erlang Mnesia/Khepri) | Append-Only Log на диске (сегменты) |
| Гарантии доставки | At-most-once (Fire-and-forget) | Configurable (At-least-once / At-most-once) | At-least-once / Exactly-once |
| Модель маршрутизации | Subject-based (иерархии + wildcards) | Exchange / Queue / Routing Keys | Partitioned Topics по хэшу ключа |
| Задержка (Latency p99) | Микросекунды (10–100 мкс) | Миллисекунды (1–5 мс) | Миллисекунды (3–10 мс) |
| Потребление ресурсов | Минимальное (~30 МБ RAM) | Среднее (~200–500 МБ RAM) | Высокое (~4–8 ГБ JVM + Page Cache) |
| Эксплуатационная сложность | Близкая к нулю (1 бинарник) | Средняя/Высокая | Высокая |
Security и аутентификация
Безопасность в NATS не принесена в жертву простоте. Платформа поддерживает полный спектр средств разграничения доступа:
- Token authentication: Простая аутентификация по секретному токену в строке подключения;
- Username / Password: Классические пары логин/пароль с хэшированием;
- TLS/SSL Encryption: Полное шифрование трафика с взаимной валидацией сертификатов (mTLS);
- Децентрализованная безопасность на JWT (NATS 2.0+): Революционная система без единого центра авторизации на базе открытых ключей Эдвардса (Ed25519 NKeys). Пользователи получают JWT-токены с ограниченными правами (например, право публиковать только в
orders.*и читать только изorders.processed). Сервер проверяет криптографическую подпись локально без запросов во внешние базы данных.
Пример безопасного подключения на Go:
nc, err := nats.Connect(nats.DefaultURL,
nats.Token("secret-token"),
nats.Secure(), // TLS
)
Кластеризация и отказоустойчивость
Серверы NATS объединяются в Full Mesh кластер: брокеры устанавливают TCP-соединения между собой и синхронизируют информацию о зарегистрированных подписчиках через облегченный протокол gossip.
Для гео-распределенных сред NATS поддерживает две уникальные концепции:
- Superclusters: Объединение нескольких независимых кластеров в разных дата-центрах или регионах мира через шлюзы (Gateways).
- Leaf Nodes («Листовые узлы»): Микро-серверы NATS, работающие на удаленных IoT-устройствах, кораблях, заводах или в закрытых периметрах. Они работают локально даже при полном обрыве интернета, а при восстановлении связи прозрачно синхронизируются с головным кластером в облаке.
Performance Benchmarks
В независимых тестах производительности Core NATS стабильно демонстрирует выдающиеся показатели:
- Более 1 000 000+ сообщений в секунду на одно процессорное ядро;
- Задержка доставки (End-to-End Latency) менее 1 миллисекунды на перцентиле p99;
- Способность удерживать десятки тысяч активных TCP-соединений при расходе оперативной памяти менее 50–100 МБ.
Когда использовать NATS
- Внутренняя шина микросервисов (Internal Event Bus): Мгновенная рассылка событий между сервисами внутри кластера Kubernetes.
- Service Mesh & Sidecar: Легковесный транспорт для межсервисной коммуникации взамен тяжелых прокси.
- Real-time нотификации и WebSockets: Рассылка котировок, чатов, пушей миллионам клиентов.
- IoT и Edge Computing: Запуск на слабых процессорах (Raspberry Pi, ARM-микроконтроллеры) с нестабильной сетью.
- Высокочастотные торговые системы (HFT): Где каждая микросекунда задержки напрямую конвертируется в финансовые потери.
Итог
NATS — это минималистичный, быстрый и облегченный брокер сообщений, идеально подходящий для построения event-driven и real-time систем. Его сила — в простоте и производительности. Однако для production-систем с требованиями к durability и exactly-once доставке нужно использовать JetStream, который добавляет persistent возможности поверх core NATS.
В следующей статье мы рассмотрим 2. Core NATS vs JetStream, чтобы детально сопоставить две модели и научиться безошибочно выбирать между ними.