SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

NATS, RabbitMQ или Kafka на одном VPS: что выбрать?

Сравнение брокеров сообщений для одного сервера. Узнайте, когда Postgres заменяет очередь, как ведут себя NATS, RabbitMQ и Kafka при перезагрузке и почему Kafka избыточна на VPS.

Краткий ответ для одного сервера

Использование очереди сообщений на одном VPS — это вопрос гарантий доставки, а не скорости. На отдельном сервере брокер редко становится узким местом, так как ваш код приложения, база данных и диск исчерпают ресурсы раньше. Выберите инструмент, чьё поведение при сбоях вас устраивает, а затем проведите замеры на имеющемся оборудовании.

Четыре варианта в порядке, в котором большинству читателей стоит их рассматривать:

  • Используйте базу данных, которая у вас уже работает. Postgres с SELECT ... FOR UPDATE SKIP LOCKED — это рабочая очередь задач, которая не добавляет новых процессов для мониторинга.
  • Используйте RabbitMQ, если каждое сообщение — это единица работы, которую нужно подтвердить, повторить ограниченное число раз, а затем поместить в хранилище для ручного разбора.
  • Используйте NATS, если сообщения — это события, на которые реагируют несколько частей вашей системы. Включите JetStream для событий, которые должны пережить перезагрузку.
  • Используйте Kafka, если внешний инструмент поддерживает только протокол Kafka. На одном сервере это практически единственная веская причина для выбора.

Остальная часть этого руководства содержит обоснование: сколько памяти и места на диске потребляет каждый вариант на небольшом VPS, что происходит при перезагрузке сервера и какая команда позволяет увидеть очередь сообщений до того, как её заметят пользователи.

Что на самом деле означает гарантия доставки

At most once (не более одного раза) означает, что брокер передает сообщение и «забывает» о нем. Если потребитель не подключен или завершает работу в процессе обработки, сообщение теряется, и уведомление об этом не поступает.

At least once (не менее одного раза) означает, что потребитель отправляет подтверждение (ack) после успешного выполнения работы. Пока подтверждение не получено, брокер удерживает сообщение и доставит его повторно. Из-за возможности повторной доставки ваши обработчики должны быть идемпотентными: повторная обработка того же сообщения не должна приводить, например, к повторному списанию средств с карты. Гарантия «ровно один раз» (exactly once) на всем пути прохождения сообщения не обеспечивается брокером. Она достигается за счет использования уникального ключа в вашей базе данных.

Replay (воспроизведение) — это отдельное свойство. Очередь удаляет сообщение сразу после получения подтверждения. Лог хранит сообщения в течение заданного периода удержания, поэтому новый потребитель может начать чтение с самого начала и просмотреть всю историю. Kafka и NATS JetStream являются логами. RabbitMQ — это очередь. Это различие влияет на архитектуру систем сильнее, чем пропускная способность.

Dead lettering (отправка в очередь недоставленных сообщений) — это механизм обработки сообщений, которые постоянно вызывают ошибки. Без него «ядовитое» сообщение будет зациклено бесконечно, и этот цикл будет выглядеть как высокая нагрузка на воркер, а не как сбой в его работе.

Начните с Postgres и заставьте брокер доказать свою эффективность

Большинство рабочих нагрузок для одного приложения составляют несколько тысяч фоновых задач в день. Это легко помещается в таблицу.

CREATE TABLE job (
  id        bigserial   PRIMARY KEY,
  payload   jsonb       NOT NULL,
  run_after timestamptz NOT NULL DEFAULT now(),
  attempts  int         NOT NULL DEFAULT 0
);
CREATE INDEX job_ready_idx ON job (run_after, id);

Воркер забирает одну задачу внутри транзакции.

BEGIN;
SELECT id, payload
  FROM job
 WHERE run_after <= now()
 ORDER BY id
   FOR UPDATE SKIP LOCKED
 LIMIT 1;
-- run the work, then remove the row
DELETE FROM job WHERE id = $1;
COMMIT;

FOR UPDATE SKIP LOCKED — это и есть весь секрет. Команда блокирует возвращаемую строку и пропускает любые строки, которые уже заблокированы другой транзакцией, поэтому два воркера никогда не заберут одну и ту же задачу. Если воркер аварийно завершается, Postgres откатывает его транзакцию, блокировка снимается, и строка становится доступной для следующего воркера. Вы получаете доставку «как минимум один раз», повторные попытки через увеличение attempts и таблицу для «мертвых» задач — и всё это с надежностью, за которую вы уже платите. Очередь задач — это один запрос: SELECT count(*) FROM job WHERE run_after <= now();

Где этот подход перестает работать. Каждое получение и удаление задачи — это операция записи, поэтому при высокой интенсивности задач накапливаются «мертвые» версии строк, и таблица очереди становится классическим примером раздувания (bloat), которое опережает autovacuum. Длительные задачи усугубляют ситуацию, так как транзакция, удерживаемая на время выполнения работы, также сдерживает горизонт очистки (vacuum horizon) для всей базы данных. Опрос (polling) добавляет задержки, а LISTEN с NOTIFY убирают необходимость опроса, но не устраняют операции записи. Когда таблица задач становится самой нагруженной таблицей в системе или когда события требуются второму сервису, выносите обработку задач наружу. Этот выбор зависит от того, как развернута сама база данных, поэтому определитесь с тем, запущена ли база данных в Docker или на хосте, прежде чем добавлять рядом с ней брокер.

Redis — это еще один инструмент, который у вас, возможно, уже запущен. Redis Streams предоставляют группы потребителей с XADD и XREADGROUP, список ожидающих задач для каждой группы и XAUTOCLAIM для возврата работы от потребителя, который завершил работу. Это компактное и быстрое решение. Честный нюанс для одного сервера: при стандартной настройке appendfsync everysec внезапное отключение питания может привести к потере данных записи примерно за одну секунду. Это допустимо для инвалидации кэша, но неприемлемо для платежей. Если ваше приложение представляет собой один процесс, построенный вокруг SQLite в продакшене на VPS, то работает тот же шаблон «забрать и удалить», хотя в SQLite нет аналога SKIP LOCKED, и все воркеры выстраиваются в очередь на одну блокировку записи.

NATS core: subject routing with no memory

docker run -d --name nats \
  -p 4222:4222 -p 127.0.0.1:8222:8222 \
  nats:2.14 -m 8222

As of August 2026 the current server line is 2.14. -m 8222 turns on the HTTP monitoring port, which is off by default and has no authentication, so bind it to localhost as above.

Core NATS is at most once and it stores nothing. A publisher sends to a subject such as orders.created, and every subscriber whose filter matches gets a copy. If nobody is subscribed, the message is dropped and the publisher sees no error, because the publisher's job ended when the server accepted the bytes. A queue group (several subscribers sharing one group name) makes the server pick one member per message, which shares work without storing a queue.

Footprint is subscription state plus a write buffer for each connection, so it tracks connection count rather than message volume, and nothing accumulates on disk. Restart behaviour follows from that: in-flight messages are gone, clients reconnect on their own, and there is no recovery step to wait for.

There is no backlog to watch, so watch for loss instead. When a subscriber reads its socket more slowly than the server writes to it, the server's buffer for that client fills. If the client has not caught up by the write deadline, the server closes the whole connection and increments a counter.

curl -s http://localhost:8222/varz | jq '.slow_consumers, .connections, .in_msgs, .out_msgs'

A slow_consumers value that keeps climbing means messages are being dropped, so alert on it rather than reading it once. Core NATS suits a message whose value expires quickly: a metric, a presence update, a cache invalidation that the next event will supersede anyway.

NATS JetStream: долговечные потоки и повтор сообщений в одном процессе

JetStream — это не отдельный продукт. Это подсистема в том же бинарном файле, которая включается одним флагом.

docker run -d --name nats \
  -p 4222:4222 -p 127.0.0.1:8222:8222 \
  -v nats-data:/data \
  nats:2.14 -js -sd /data -m 8222

-sd /data задает каталог для хранения данных. Если его не указать, JetStream сохранит данные в /tmp, что по надежности соответствует названию. Создайте поток с помощью CLI, который входит в образ nats-box.

docker run --rm -it --network host natsio/nats-box:latest \
  nats stream add ORDERS \
    --subjects 'orders.>' \
    --storage file \
    --retention limits \
    --max-age 72h \
    --max-bytes=1073741824 \
    --discard old \
    --defaults

Каждое ограничение здесь оправдано на небольшом сервере. --storage file — это то, что переживет сбой, в отличие от потока в оперативной памяти. --max-bytes=1073741824 ограничивает размер потока 1 GiB (указывается в байтах), а --discard old удаляет самые старые сообщения при достижении лимита, вместо того чтобы отклонять новые записи. Если не задать ограничение, один вышедший из-под контроля издатель заполнит диск, из-за чего остановится и ваша база данных, так как они используют один диск.

Долговечный потребитель (durable consumer) сохраняет свою позицию в потоке и помнит её после перезапуска. Установите --max-deliver для потребителя, чтобы сообщение, которое постоянно вызывает ошибку, перестало доставляться бесконечно. Когда лимит попыток доставки исчерпан, JetStream публикует уведомление в $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>. Подписка на этот субъект — это способ реализации очереди недоставленных сообщений (dead letter path), которую RabbitMQ предоставляет как готовую функцию. Здесь вам придется реализовать это самостоятельно.

Чтобы увидеть очередь сообщений, используйте nats stream report для подсчета сохраненных сообщений и nats consumer report ORDERS для просмотра ожидающих подтверждений и необработанных сообщений для каждого потребителя. Именно по количеству необработанных сообщений стоит настраивать алерты. Затраты дискового пространства можно отследить с помощью du -sh для каталога хранения; данные будут расти, пока их не сократит лимит хранения.

RabbitMQ: подтверждение каждого сообщения, обработка сбоев

docker run -d --name rabbitmq \
  -p 5672:5672 -p 127.0.0.1:15672:15672 \
  -v rabbitmq-data:/var/lib/rabbitmq \
  rabbitmq:4-management

По состоянию на август 2026 года актуальной является серия 4.3. Порт 5672 используется для AMQP (advanced message queuing protocol), а 15672 — для интерфейса управления. Ограничьте доступ к интерфейсу через localhost и подключайтесь к нему с помощью SSH-туннеля.

Объявляйте очереди с аргументом x-queue-type, установленным в quorum; значение по умолчанию по-прежнему classic. Очереди кворума (quorum queues) всегда являются долговечными и записывают данные на диск перед выполнением любых других операций. Это обеспечивает предсказуемое поведение на одном узле, избавляя от необходимости выбирать между долговечными и временными опциями. Настройте целевую очередь для «мертвых» сообщений (dead letter) с помощью политики.

docker exec rabbitmq rabbitmqctl set_policy DLX ".*" \
  '{"dead-letter-exchange":"my-dlx", "dead-letter-routing-key":"my-routing-key"}' \
  --apply-to queues --priority 7

Сообщение попадает в очередь «мертвых» писем по четырем причинам: потребитель отклоняет его с помощью basic.reject или basic.nack при значении requeue, равном false; истекает время жизни сообщения (TTL); очередь достигает лимита по длине; или превышен лимит доставки для очереди кворума. Начиная с RabbitMQ 4.0, этот лимит по умолчанию равен 20. Таким образом, обработчик, который вызывает исключение и выполняет nack, совершит двадцать попыток повтора, после чего передаст сообщение в exchange для «мертвых» писем, вместо того чтобы зацикливаться.

Память — это аспект, в котором RabbitMQ часто преподносит сюрпризы на небольших VPS. Пороговое значение использования памяти (high watermark) по умолчанию составляет 0.6 от доступной оперативной памяти. Когда узел превышает этот лимит, RabbitMQ блокирует все соединения, выполняющие публикацию. Ваше приложение не получает ошибку. Оно выполняет публикацию, которая никогда не завершается, что в коде выглядит как зависание. В логе запуска отображается значение, вычисленное узлом:

Memory high watermark set to 1024 MiB (1073741824 bytes) of 8192 MiB (8589934592 bytes) total

Сигнал тревоги по диску (disk alarm) блокирует издателей аналогичным образом, когда свободное место падает ниже 50 MB (значение по умолчанию). Очереди кворума добавляют свои требования: документация предполагает наличие как минимум 32 байт метаданных в оперативной памяти на каждое сообщение (около 1 MB на 30 000 сообщений) и рекомендует иметь в RAM объем, как минимум в три раза превышающий эффективный размер журнала предзаписи (write-ahead log). Лимит WAL по умолчанию составляет 512 MiB, поэтому одна только эта рекомендация требует 1.5 GB. На сервере с 2 GB памяти лучше уменьшить это значение в rabbitmq.conf, чем надеяться, что настройки по умолчанию подойдут.

raft.wal_max_size_bytes = 64000000
vm_memory_high_watermark.relative = 0.5

Бэклог состоит из двух чисел, и эта пара указывает на тип возникшей проблемы.

docker exec rabbitmq rabbitmqctl list_queues name messages messages_ready messages_unacknowledged

messages_ready означает ожидание потребителя. messages_unacknowledged — сообщение было доставлено, но не подтверждено (acked). Рост количества неподтвержденных сообщений при неизменном количестве готовых к обработке означает, что ваши воркеры забирают задачи, но перестают их завершать. Это принципиально иная ошибка, чем просто отставание очереди от нагрузки.

Kafka на одном узле: когда это теряет смысл

KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
bin/kafka-storage.sh format --standalone -t $KAFKA_CLUSTER_ID -c config/server.properties
bin/kafka-server-start.sh config/server.properties

Это краткое руководство для Kafka 4.3.1, актуальное на август 2026 года, работающее в режиме KRaft (Kafka Raft — встроенный контроллер, заменивший ZooKeeper в Kafka 4.0). Эквивалент для контейнеров — apache/kafka:4.3.1.

Скрипт запуска устанавливает export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G", если вы не задали это значение самостоятельно. В результате брокер резервирует 1 ГБ Java heap еще до записи первого сообщения, при этом ему требуется дополнительная свободная оперативная память для page cache, из которого он считывает данные. На VPS с 2 ГБ памяти ваше приложение будет конкурировать с JVM за оставшиеся ресурсы.

Следующий неожиданный момент — настройки хранения данных. log.retention.hours по умолчанию равно 168, что соответствует семи дням, а log.retention.bytes по умолчанию равно -1, что означает отсутствие лимита по размеру. Kafka хранит сообщения в течение всего этого периода, независимо от того, прочитали ли их все потребители. Это та функция, ради которой вы выбрали Kafka, но на диске малого объема она становится причиной сбоя, поэтому установите лимит в байтах для каждого топика, прежде чем столкнетесь с нехваткой места.

Теперь о главном. Один брокер означает коэффициент репликации 1, поэтому acks=all сводится к одному fsync на одном диске. Вы получаете надежность одной машины при эксплуатационных расходах на JVM-брокер и контроллер. Партиции обеспечивают параллелизм между брокерами, которых у вас нет. Репликация, учет стоек и другие функции для кластера остаются неактивными. JetStream предоставляет такую же возможность воспроизведения сообщений на той же машине, потребляя при этом значительно меньше памяти. Есть две причины использовать Kafka в такой конфигурации: либо ваш инструмент для обработки данных поддерживает только протокол Kafka (например, CDC с помощью Debezium или загрузчик аналитики), либо вы воспроизводите производственную топологию в миниатюре. Планирование масштабирования до кластера — это план по покупке дополнительных машин, а до тех пор вы делаете тот же выбор, что и при запуске k3s на одном узле, где вы платите сложностью кластера за надежность одного узла.

Очередь в Kafka — это отставание потребителя (consumer lag).

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group

Следите за столбцом LAG, который представляет собой разницу LOG-END-OFFSET минус CURRENT-OFFSET для каждой партиции. Рост отставания на одной партиции при стабильных показателях на остальных указывает на неравномерное распределение ключей, так как все сообщения с одинаковым ключом попадают в одну партицию, и один потребитель обрабатывает их в одиночку.

Что происходит при перезагрузке сервера

Core NATS теряет все сообщения в процессе передачи и перезапускается мгновенно, так как восстанавливать нечего. JetStream загружает потоки и позиции потребителей из каталога хранилища, поэтому потребители возобновляют работу с того же смещения. RabbitMQ восстанавливает кворумные очереди с диска, в то время как классические временные очереди и любые сообщения, опубликованные без режима постоянной доставки, удаляются. Kafka при запуске перечитывает сегменты своего лога, и после некорректного завершения работы это сканирование восстановления может занять несколько минут на медленном диске, прежде чем брокер начнет принимать соединения.

Стоит настроить две вещи. Установите для контейнера политику перезапуска (restart: unless-stopped) или включите systemd-юнит, чтобы брокер возвращался в работу после перезагрузки при обновлении ядра без вашего участия. Затем проконтролируйте порядок запуска: брокер, который становится готов через 20 секунд после вашего приложения, будет отклонять первые соединения, а некоторые клиентские библиотеки завершают работу вместо того, чтобы повторить попытку. Ограничьте запуск приложения с помощью Compose healthchecks, которые задерживают зависимый сервис до готовности брокера.

Стоимость на собственном VPS: измерения вместо заявленных характеристик

Опубликованные показатели пропускной способности измеряются на оборудовании, которое вам недоступно — обычно это многоядерный сервер с локальным NVMe. Рассматривайте их как верхний предел и проводите замеры на своей машине.

docker stats --no-stream
free -m
sudo du -sh /var/lib/docker/volumes/*/_data

Запустите тесты при простое брокера, а затем повторите их под реальной нагрузкой. Разница между этими значениями покажет, подходит ли брокер для работы совместно с вашим приложением. Чтобы получить примерный нижний порог пропускной способности, используйте генератор нагрузки от самого проекта, а не данные из сторонних блогов: nats bench pub test --msgs 100000 --clients 2 для NATS, bin/kafka-producer-perf-test.sh для Kafka и PerfTest для RabbitMQ. Запуск генератора на том же VPS измеряет производительность брокера и генератора в связке, что допустимо, если вы укажете это при публикации результатов.

Существует общее ограничение для всех подобных систем. Любая опция с обеспечением сохранности данных (durable) ожидает завершения fsync. Поэтому на VPS с сетевым хранилищем (network-attached storage) предел производительности задает диск, и смена брокера не поможет его преодолеть.

Три типа рабочих нагрузок и соответствующие им очереди сообщений

  1. Фоновые задачи для веб-приложения, такие как отправка электронной почты, изменение размера изображений или доставка webhooks. Начните с Postgres и SKIP LOCKED. Переходите на RabbitMQ с quorum queues, если вам нужны подтверждения доставки для каждого сообщения (acks), ограничение количества попыток доставки и очередь недоставленных сообщений (dead letter queue), которую можно просмотреть без написания собственной логики, или если таблица задач стала самой нагруженной в базе данных.
  2. События, на которые реагируют несколько внутренних сервисов, где потерянное сообщение быстро заменяется новым. Используйте базовый NATS с subjects в качестве схемы маршрутизации и queue groups там, где требуется распределение нагрузки. Добавьте поток JetStream для ограниченного набора тем, которые должны сохраняться после перезагрузки, а остальные оставьте в оперативной памяти.
  3. Журнал событий, который потребители считывают с самого начала для аудита, восстановления модели чтения или последующей передачи данных в аналитические системы. Используйте JetStream с файловым хранилищем и явным ограничением размера в байтах. Выбирайте Kafka только в том случае, если сторонний инструмент требует протокол Kafka, и будьте готовы принять потребление памяти JVM как плату за эту совместимость.

Цена неверного выбора на одном сервере — это не пропускная способность. Это восстановление системы в три часа ночи, когда вам нужно точно знать, существуют ли еще сообщения. Выбирайте решение исходя из этого.

FAQ

Можно ли запустить Kafka на VPS с 2 GB ОЗУ?

Она запустится, но работать будет на пределе. bin/kafka-server-start.sh устанавливает KAFKA_HEAP_OPTS="-Xmx1G -Xms1G", если вы не переопределили это значение, поэтому JVM резервирует 1 GB еще до записи первого сообщения, а Kafka полагается на свободную память сверх этого объема для page cache. Если добавить на тот же сервер приложение и базу данных, система начнет использовать swap. Кроме того, вы получите replication factor 1, а значит acks=all — это один fsync на один диск; вы несете эксплуатационные расходы Kafka, не получая её модели надежности. NATS JetStream обеспечивает надежное воспроизведение сообщений на том же оборудовании при значительно меньшем потреблении памяти.

Нужна ли очередь сообщений, если я уже использую Postgres?

Часто нет. Чтение из таблицы job с использованием SELECT ... FOR UPDATE SKIP LOCKED внутри транзакции обеспечивает доставку «как минимум один раз», безопасную работу параллельных воркеров, повторные попытки и таблицу для «мертвых» сообщений (dead letter table). При этом не нужно мониторить дополнительный сервис, а бэкапы у вас уже настроены. Сигналы к тому, что пора выносить очередь во внешний сервис: таблица очереди становится самым нагруженным местом по записи, autovacuum не справляется, длительные задачи держат транзакции открытыми и блокируют vacuum для всей базы данных, либо второму сервису требуется независимо потреблять те же события.

Что выбрать для фоновых задач: NATS JetStream или RabbitMQ?

RabbitMQ, если вам нужны подтверждение каждого сообщения (per-message acknowledgement), лимит попыток доставки и встроенная маршрутизация «мертвых» сообщений. Quorum queues всегда являются надежными (durable), лимит доставки в RabbitMQ 4.0 по умолчанию равен 20, а политика отправляет исчерпавшие попытки сообщения в dead letter exchange, откуда их можно извлечь и изучить. JetStream, если те же события позже должны быть воспроизведены другими потребителями, так как поток (stream) сохраняет сообщения после подтверждения, а очередь — нет. В JetStream вы настраиваете --max-deliver и самостоятельно реализуете путь для «мертвых» сообщений на основе уведомлений $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>.

Как узнать, насколько отстают мои потребители?

У каждого брокера есть своя команда. Для RabbitMQ rabbitmqctl list_queues name messages messages_ready messages_unacknowledged разделяет работу, ожидающую потребителя, и работу, которая была доставлена, но не получила подтверждения. Для JetStream nats consumer report <stream> показывает необработанные сообщения и ожидающие подтверждения для каждого потребителя. Для Kafka kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group <group> выводит столбец LAG для каждого раздела (partition). В базовом NATS нет очереди для чтения, так как он ничего не хранит, поэтому следите за счетчиком slow_consumers в http://localhost:8222/varz: он показывает количество соединений, которые сервер закрыл из-за отставания, что означает потерю сообщений.

#nats#rabbitmq#kafka#message-queue#architecture