Трендовые github проекты в нашем телеграм канале. Подпишись 👉 NATS: лёгкий мессенджер
NATS — это мессенджер, который ломает шаблоны традиционных брокеров. Когда Kafka с его zookeeper и partitioning становится тяжелым, а RabbitMQ — сложным для микросервисов, NATS предлагает радикально простой подход: минимальные зависимости, наносекундная задержка и встроенный pub/sub без лишних конфигураций. Но за этой простотой скрываются важные компромиссы, которые мы разберем в этой статье.
Архитектура NATS: простота с подвохом
NATS построен на принципе “публикуй и подписывай” без центрального маршрутизатора. Вместо сложных эксчейнджей и очередей, как в RabbitMQ, NATS использует плоскую модель Subjects (тем), где любой клиент может публиковать сообщения, а другие — подписываться.
Сервер NATS (gnatsd) выполняет лишь одну задачу: принимает сообщения и пересылает их всем подписчикам соответствующей темы. Это делает его чрезвычайно быстрым — задержки измеряются микросекундами. Но есть и обратная сторона: по умолчанию сообщения не сохраняются. Если подписчик не онлайн, он пропустит сообщение.
Вот здесь на сцену выходит JetStream — расширение NATS для персистентности. Он добавляет:
- Хранение сообщений
- Повторную доставку
- Управление потоками и консьюмерами
- Квантификацию сообщений (каждое получает последовательный номер)
JetStream работает на концепции Streams (потоков) и Consumers (консьюмеров). Stream — это упорядоченное хранилище сообщений, а Consumer — точка доступа с возможностью управления тем, какие сообщения и в каком порядке доставляются.
Практическая интеграция с Python
Для работы с NATS в Python используем библиотеку nats-py. Установка:
pip install nats-py
Пример базовой публикации и подписки:
import asyncio
import nats
async def run_basic_example():
# Подключение к серверу NATS
nc = await nats.connect("nats://localhost:4222")
# Публикация сообщения
await nc.publish("updates.system", b"System is running")
# Простая подписка
async def handler(msg):
print(f"Получено сообщение: {msg.data}")
await nc.subscribe("updates.system", cb=handler)
# Даем время на получение сообщений
await asyncio.sleep(1)
await nc.close()
asyncio.run(run_basic_example())
А теперь более сложный пример с JetStream:
import asyncio
import nats
from nats.errors import TimeoutError
async def run_jetstream_example():
nc = await nats.connect("nats://localhost:4222")
js = nc.jetstream()
# Создание потока, если его нет
try:
await js.add_stream(
name="SYSTEM_LOGS",
subjects=["logs.>"], # Шаблон: logs.*
retention="limits", # Ограничение по количеству сообщений
)
except nats.errors.StreamNameAlreadyInUse:
pass # Поток уже существует
# Публикация сообщения
await js.publish("logs.server", b"Error: Connection timeout")
# Pull-подписка (JetStream)
psub = await js.pull_subscribe("logs.server", "LOGS_CONSUMER")
try:
# Получаем одно сообщение с таймаутом
msgs = await psub.fetch(1, timeout=1.0)
for msg in msgs:
print(f"Получено лог: {msg.data}")
await msg.ack() # Подтверждаем получение
except TimeoutError:
print("Нет новых сообщений")
await nc.close()
asyncio.run(run_jetstream_example())
В этом примере важно отметить:
- Мы используем pull-подписку, которая позволяет явно запрашивать сообщения.
- Сообщения нужно явно подтверждать (
msg.ack()), иначе они будут доставлены заново. - Шаблоны Subjects (
logs.>) позволяют подписаться на несколько тем одновременно.
Узкие места, которые нужно учесть в продакшене
-
Масштабирование кластера Хотя NATS отлично справляется с нагрузкой в одном экземпляре, кластеризация требует внимательности. Каждый узел NATS должен синхронизировать состояние с другими, что при большом количестве узлов создает сетевую нагрузку. В продакшене стоит рассмотреть режимы работы
fullилиdeltaсинхронизации в зависимости от требований к согласованности данных. -
Отсутствие сложной маршрутизации В отличие от RabbitMQ с его эксчейнджами и очередями, NATS предлагает только плоскую модель Subjects. Если вашему приложению требуется сложная логика маршрутизации (например, маршрутизация на основе содержимого сообщений), придется реализовывать это на уровне приложений, что добавляет сложности.
-
Управление состоянием JetStream При большом количестве потоков и консьюмеров управление состоянием может стать узким местом. Stream-ы в JetStream имеют ограничения по размеру и количеству сообщений, и их нужно тщательно настраивать. Неучтенное ограничение может привести к блокировке публикации новых сообщений.
-
Проблемы с порядком сообщений Хотя внутри одного Stream-а сообщения сохраняют порядок, при использовании нескольких консьюмеров или в распределенной системе порядок может нарушиться. Если строгий порядок сообщений критичен для вашего приложения, это нужно учитывать в архитектуре.
-
Мониторинг и отладка По сравнению с зрелыми системами вроде Kafka, инструментирование NATS менее развит. Отладка сложных сценариев с сообщениями может потребовать ручного анализа логов сервера, что не всегда удобно.
Когда выбирать NATS, а когда — альтернативы
NATS идеален для сценариев, где критичны:
- Низкая задержка и высокая пропускная способность
- Простота развертывания и управления
- Минимальные требования к ресурсам
- Прямая модель pub/sub без сложной логики маршрутизации
Это делает NATS отличным выбором для:
- Микросервисной архитектуры с высокой связанностью
- Систем реального времени (IoT, телеметрия)
- Веб-сокетных серверов
- Систем, где важна скорость реакции
Однако, если вашему приложению требуются:
- Сложная маршрутизация с условиями и фильтрацией
- Транзакционная обработка сообщений
- Строгий порядок сообщений в распределенной системе
- Богатая экосистема инструментов мониторинга
Тогда стоит рассмотреть альтернативы:
- Kafka — для потоковой обработки больших данных
- RabbitMQ — для сложной маршрутизации и гарантированной доставки
- Pulsar — для геораспределенных систем
NATS — это инструмент для конкретных задач, и его сила заключается в своей простоте и скорости. Для правильного выбора важно понять компромиссы, которые вы готовы принять, чтобы получить преимущества в нужных сценариях использования.