MAATRIX / Блог / NATS в Docker Compose: готовый файл

NATS в Docker Compose: готовый файл

MAATRIX

Как только в проекте появляется больше двух сервисов, которым нужно обмениваться событиями, начинается выбор между RabbitMQ, Kafka и десятком других брокеров — и почти всегда это выбор избыточно тяжёлого инструмента под простую задачу. NATS решает ту же проблему compose-файлом на 15 строк, бинарником размером 15-20 МБ и задержками, которых обычно не замечаешь на фоне сети. Ниже — рабочая конфигурация от простого pub/sub до кластера с персистентностью и мониторингом.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое NATS и когда он уместен

NATS — это message broker, написанный на Go командой, которая позже создала CNCF-проект Synadia. В базовом режиме (Core NATS) он держит сообщения только в памяти и ничего не гарантирует, кроме доставки «at most once»: подписчик не в сети — сообщение потеряно. Это осознанный компромисс ради скорости и простоты, а не недоработка.

Для случаев, когда нужна персистентность, повторная доставка и очереди с подтверждением, есть JetStream — надстройка над тем же протоколом, встроенная в тот же бинарник с версии 2.2. Не нужно поднимать отдельный кластер Kafka или ZooKeeper — JetStream включается одним флагом и хранит данные на диске рядом.

Где NATS обычно выигрывает у альтернатив:

  • микросервисная коммуникация внутри одного датацентра или VPC — request-reply с таймаутами из коробки, без городить HTTP-клиенты с ретраями;
  • event bus для десятков-сотен сервисов — subject-based маршрутизация (orders.created, orders.*, orders.>) заменяет очереди и exchange'и RabbitMQ;
  • IoT и edge — низкое потребление памяти (десятки МБ на процесс) и способность работать в кластере с нестабильной связью через leaf nodes.

Где не стоит: если нужна сложная маршрутизация с трансформацией сообщений (тут ближе RabbitMQ с его exchange-типами) или террабайтные топики с долгим ретеншеном и consumer group semantics один-в-один как в Kafka — для такой нагрузки Kafka спроектирован лучше.

Минимальный docker-compose.yml для Core NATS

Для старта — pub/sub без персистентности, с открытым HTTP-портом мониторинга:

services:
  nats:
    image: nats:2.10-alpine
    container_name: nats
    restart: unless-stopped
    command: "-js -m 8222"
    ports:
      - "4222:4222"   # клиентский протокол
      - "8222:8222"   # HTTP monitoring
    volumes:
      - nats_data:/data
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8222/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5

volumes:
  nats_data:

Флаг -js уже включает JetStream (без него сервер работает в чистом Core-режиме и данные /data не пишет — том можно убрать). -m 8222 открывает HTTP-эндпоинт для мониторинга и health-check.

Проверка, что брокер поднялся и отвечает:

docker compose up -d
curl -s http://localhost:8222/varz | head -c 300

Если в ответе пришёл JSON с полем server_id — сервер живой. Для быстрой проверки pub/sub без написания кода удобно использовать natscli (ставится отдельно, brew install nats-io/nats-tools/nats или бинарник с GitHub):

nats sub "orders.>" &
nats pub orders.created '{"id": 42}'

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

JetStream: персистентность и стримы

Голый -js включает JetStream, но не создаёт стримы — их нужно объявить явно, либо через natscli, либо кодом приложения при старте. Пример: стрим для событий заказов с ретеншеном по времени.

nats stream add ORDERS \
  --subjects "orders.>" \
  --storage file \
  --retention limits \
  --max-age 168h \
  --replicas 1

Ключевые параметры JetStream, которые стоит выставлять осознанно, а не оставлять по умолчанию:

ПараметрЧто делаетТипичное значение
--storagefile (на диске) или memoryfile для продакшена
--retentionlimits (по объёму/времени), interest (пока есть подписчики), workqueue (пока не подтверждено)зависит от паттерна
--max-ageсколько хранить сообщения168h (неделя) как стартовая точка
--replicasсколько копий стрима в кластере1 для одной ноды, 3 для кластера
--max-bytesжёсткий лимит объёма стримаограничивайте всегда, иначе диск переполнится

Важный нюанс: file-хранилище JetStream пишет данные в том /data, который в docker-compose объявлен выше. Без volume при пересоздании контейнера стрим и все сообщения в нём исчезнут — для persistence это не опция, а обязательное условие.

Consumer (подписчик с подтверждением) создаётся отдельно от стрима — это разделение и даёт JetStream гарантии доставки:

nats consumer add ORDERS worker-1 \
  --filter "orders.created" \
  --ack explicit \
  --pull \
  --max-deliver 5

--max-deliver 5 — сообщение, которое воркер пять раз не подтвердил, уходит либо в лог, либо (при настроенном dead-letter subject) в отдельный поток для ручного разбора — без этого лимита зависший обработчик будет получать одно и то же сообщение бесконечно.

Кластер из трёх нод для отказоустойчивости

Одна нода NATS — единая точка отказа. Кластер решает это через gossip-протокол на порту 6222 и требует нечётного числа нод (минимум 3) для кворума при выборе лидера в JetStream RAFT-группах.

services:
  nats-1:
    image: nats:2.10-alpine
    command: >
      -js -sd /data
      --cluster_name NATS
      --cluster nats://0.0.0.0:6222
      --routes nats://nats-2:6222,nats://nats-3:6222
      -m 8222
    volumes: [nats1_data:/data]
    networks: [nats-net]

  nats-2:
    image: nats:2.10-alpine
    command: >
      -js -sd /data
      --cluster_name NATS
      --cluster nats://0.0.0.0:6222
      --routes nats://nats-1:6222,nats://nats-3:6222
      -m 8222
    volumes: [nats2_data:/data]
    networks: [nats-net]

  nats-3:
    image: nats:2.10-alpine
    command: >
      -js -sd /data
      --cluster_name NATS
      --cluster nats://0.0.0.0:6222
      --routes nats://nats-1:6222,nats://nats-2:6222
      -m 8222
    volumes: [nats3_data:/data]
    networks: [nats-net]

networks:
  nats-net:

volumes:
  nats1_data:
  nats2_data:
  nats3_data:

Клиент подключается по списку адресов (nats://nats-1:4222,nats://nats-2:4222,nats://nats-3:4222) — клиентские библиотеки NATS сами переключаются на живую ноду при обрыве соединения, дополнительный балансировщик перед клиентским портом не нужен. А вот стрим, созданный на этапе с одной нодой, реплики автоматически не получит — при переходе на кластер стримы нужно пересоздать с --replicas 3, либо смигрировать через nats stream edit.

Три ноды на одном физическом сервере кластер от падения самого сервера не защитят — для боевой отказоустойчивости ноды разносят по разным VPS, в идеале в разных зонах доступности. Сетевую сторону такой топологии стоит продумать заранее — этому посвящена отдельная статья про типы Docker-сетей, а сам компоуз-файл под несколько окружений (dev/staging с одной нодой, prod с тремя) удобно держать через profiles в docker-compose.

Мониторинг: HTTP-эндпоинт и Prometheus

Встроенный HTTP-монитор на порту 8222 отдаёт JSON без авторизации — за пределы приватной сети или VPN его выставлять не стоит. Основные эндпоинты:

curl http://localhost:8222/varz      # общая статистика сервера
curl http://localhost:8222/connz     # активные соединения
curl http://localhost:8222/jsz       # статистика JetStream: стримы, консьюмеры, объём
curl http://localhost:8222/healthz   # health-check для оркестратора

Для Prometheus официальный prometheus-nats-exporter конвертирует эти эндпоинты в метрики:

  nats-exporter:
    image: natsio/prometheus-nats-exporter:0.15.0
    container_name: nats-exporter
    restart: unless-stopped
    command: "-varz -connz -jsz=all http://nats:8222"
    ports:
      - "7777:7777"
    depends_on:
      - nats

После этого http://localhost:7777/metrics отдаёт метрики в формате Prometheus: число активных подключений, счётчики in/out msgs и bytes, лаг JetStream-консьюмеров. Если Prometheus и Grafana в инфраструктуре уже есть — добавить job на nats-exporter:7777 займёт пару строк в prometheus.yml; с нуля их разворачивание описано в статье про связку Prometheus и Grafana. Для внешнего HTTPS-доступа к дашборду монитора удобнее не пробрасывать порт 8222 напрямую, а спрятать его за Traefik как reverse proxy с базовой авторизацией.

Метрика, за которой стоит следить в первую очередь в продакшене — jetstream_consumer_num_pending (сколько сообщений накопилось неподтверждёнными): её рост означает, что воркеры не успевают за потоком и либо нужно масштабировать consumer group, либо в коде обработчика есть зависание.

Безопасность: аутентификация и TLS

По умолчанию контейнер из примеров выше принимает соединения без пароля — приемлемо только в закрытой Docker-сети без внешних портов. Для чего-то большего NATS поддерживает три способа авторизации: простой user/password, токены и NKeys/JWT (decentralized auth, подходит для мультитенантных систем). Для типичного проекта хватает first-варианта:

  nats:
    image: nats:2.10-alpine
    command: >
      -js
      --user appuser
      --pass "${NATS_PASSWORD}"
      -m 8222
    ports:
      - "4222:4222"
    volumes: [nats_data:/data]

Пароль стоит хранить не в самом compose-файле, а в .env рядом (добавленном в .gitignore) или в секретах CI — ${NATS_PASSWORD} docker compose подставит автоматически. Для более гранулярного контроля (разным сервисам — доступ только к своим subject) используется полноценный конфиг-файл nats-server.conf с секцией authorization и списком permissions на публикацию/подписку по маскам subject — это отдельная тема, но для старта важно хотя бы не оставлять брокер полностью открытым.

TLS между клиентами и сервером включается флагами --tls, --tlscert, --tlskey — обязательно, если клиенты подключаются не из той же приватной сети, а через интернет. Для внутрикластерного трафика на 6222 порту TLS тоже стоит включить отдельно (--cluster_tls в конфиге), иначе трафик между нодами кластера идёт открытым текстом.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Чем NATS принципиально отличается от RabbitMQ?

RabbitMQ реализует AMQP с богатой маршрутизацией через exchange'и (direct, topic, fanout, headers) и гарантированной доставкой из коробки. NATS в Core-режиме — это в первую очередь скорость и простота без гарантий, а гарантии (persistence, ack, redelivery) добавляются отдельным слоем JetStream. Если нужна сложная маршрутизация с трансформацией сообщений на уровне брокера — ближе RabbitMQ.

Нужен ли JetStream, если просто нужен pub/sub между сервисами?

Нет. Если подписчики всегда онлайн и потеря отдельного сообщения при перезапуске сервиса не критична (например, обновление live-метрик или broadcast-уведомления в UI), Core NATS без -js проще и легче. JetStream добавляйте, когда нужна гарантия «сообщение не потеряется, даже если получатель был недоступен».

Сколько памяти и CPU реально нужно NATS в продакшене?

Сам бинарник в простое занимает порядка 15-30 МБ RAM — точные цифры зависят от числа подключений, стримов и объёма JetStream-хранилища на диске, поэтому ориентируйтесь на нагрузочное тестирование под свой профиль трафика, а не на абстрактные цифры. Для старта на небольшой нагрузке достаточно 1 vCPU и 512 МБ-1 ГБ RAM с запасом под рост JetStream-хранилища.

Можно ли обновить NATS без даунтайма, если это кластер?

Да, при кластере из 3+ нод — ноды обновляются по одной (rolling update): останавливаете одну, обновляете образ, поднимаете обратно, ждёте, пока она догонит кластер (nats server list покажет статус), переходите к следующей. Клиенты, подключённые к обновляемой ноде, автоматически переподключатся к оставшимся живым.

Как понять, что стрим JetStream переполняется и данные начнут вытесняться?

Смотрите /jsz или вывод nats stream info ORDERS — там показаны текущее число сообщений/байт против лимитов --max-msgs/--max-bytes/--max-age. При достижении лимита старые сообщения вытесняются автоматически (retention limits) — это ожидаемое поведение, а не сбой, но лимиты стоит выставлять осознанно под реальный профиль нагрузки, а не оставлять безлимитными.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →