SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor

NATS, RabbitMQ чи Kafka на одному VPS: що вибрати

Порівняння NATS, RabbitMQ і Kafka на одному VPS: гарантії доставки, витрати пам’яті та диска, відновлення після збою, перевірка черги й переваги Postgres.

Коротка відповідь для одного сервера

Черга повідомлень на одному VPS — це рішення про гарантії доставки, а не про швидкість. На одному сервері брокер рідко є вузьким місцем, оскільки спочатку обмеженнями стають код застосунку, база даних і один диск. Виберіть інструмент, чия поведінка під час відмов вас влаштовує, а потім виміряйте фактичні характеристики свого сервера.

Чотири варіанти в порядку, у якому їх варто розглядати більшості читачів.

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

Далі в цьому посібнику наведено обґрунтування: скільки пам’яті та дискового простору потребує кожен варіант на невеликому VPS, як він поводиться після перезавантаження сервера та яка точна команда показує накопичені повідомлення до того, як це відчують користувачі.

Що насправді означає гарантія доставки

Не більше одного разу означає, що брокер передає повідомлення і забуває про нього. Якщо жоден споживач не підключений або споживач завершує роботу посеред обробки, повідомлення втрачається, і про це ніхто не повідомляє.

Щонайменше один раз означає, що споживач надсилає підтвердження (ack) після успішного завершення обробки. Поки це підтвердження не надійшло, брокер зберігає повідомлення і доставить його повторно. Повторна доставка означає, що обробники мають бути ідемпотентними: повторна обробка того самого повідомлення не повинна двічі списувати кошти з картки. Гарантію доставки рівно один раз від початку до кінця брокер не забезпечує. Вона реалізується через унікальний ключ у вашій базі даних.

Повторне відтворення — це окрема властивість. Черга видаляє повідомлення після підтвердження. Журнал зберігає його протягом періоду зберігання, тому новий споживач може почати з початку й прочитати всю історію. Kafka і NATS JetStream — це журнали. RabbitMQ — це черга. Ця відмінність впливає на архітектуру сильніше, ніж пропускна здатність.

Переміщення в dead-letter відбувається з повідомленням, обробка якого постійно завершується помилкою. Без цього проблемне повідомлення зациклиться назавжди, а цикл виглядатиме як активна робота воркера, а не як його несправність.

Почніть із 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 і таблицю недоставлених повідомлень — усе на основі надійності збереження даних, за яку ви вже платите. Весь backlog отримується одним запитом: SELECT count(*) FROM job WHERE run_after <= now();

Коли це перестає працювати. Кожне отримання і видалення завдання є операцією запису, тому висока частота завдань залишає після себе невидалені версії рядків, а таблиця черги — класичний випадок, коли розростання перевищує можливості autovacuum. Тривалі завдання погіршують ситуацію, оскільки транзакція, відкрита протягом усього виконання, також затримує горизонт vacuum для всієї бази даних. Опитування додає затримку, а LISTEN разом із NOTIFY усуває опитування, але не операції запису. Якщо таблиця завдань є найбільш завантаженою таблицею або другому сервісу потрібні ті самі події, винесіть цю роботу в окрему систему. Це рішення залежить від способу розгортання самої бази даних, тому спочатку визначте чи працює база даних у Docker, чи на хості, а вже потім додавайте брокер поруч із нею.

Redis — ще один компонент, який у вас уже може працювати. Redis Streams надають групи споживачів із XADD і XREADGROUP, список очікуваних повідомлень для кожної групи та XAUTOCLAIM, щоб забрати роботу у споживача, який завершився аварійно. Це компактне й швидке рішення. Але на одному сервері є важливе обмеження: зі стандартним параметром appendfsync everysec у разі втрати живлення можна втратити приблизно секунду операцій запису. Для інвалідації кешу це прийнятно, але для платежів — ні. Якщо ваш застосунок є одним процесом і побудований навколо SQLite у production на VPS, той самий шаблон отримання і видалення завдань працює, хоча SQLite не має аналога SKIP LOCKED, а всі воркери послідовно очікують на єдине блокування запису.

NATS core: маршрутизація за subject без збереження

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

Станом на серпень 2026 року актуальна гілка сервера — 2.14. -m 8222 вмикає HTTP-порт моніторингу. За замовчуванням він вимкнений і не має автентифікації, тому прив’яжіть його до localhost, як описано вище.

Core NATS працює за моделлю доставки не більше одного разу й нічого не зберігає. Видавець надсилає повідомлення в subject, наприклад orders.created, а кожен підписник із відповідним фільтром отримує копію. Якщо підписників немає, повідомлення відкидається, а видавець не отримує помилки, оскільки його робота завершується після прийняття сервером байтів. Queue group, тобто кілька підписників зі спільним іменем групи, змушує сервер вибирати одного учасника для кожного повідомлення. Це дає змогу розподіляти роботу без збереження черги.

Обсяг використовуваних ресурсів визначається станом підписок і буфером запису для кожного з’єднання. Тому він залежить від кількості з’єднань, а не від обсягу повідомлень. На диску нічого не накопичується. Поведінка після перезапуску безпосередньо випливає з цього: повідомлення, які перебували в обробці, втрачаються, клієнти самостійно підключаються повторно, а етапу відновлення, якого потрібно чекати, немає.

Черги невідправлених повідомлень немає, тому потрібно відстежувати втрати. Якщо підписник читає свій socket повільніше, ніж сервер записує в нього дані, буфер сервера для цього клієнта заповнюється. Якщо клієнт не встигає обробити дані до завершення тайм-ауту запису, сервер закриває все з’єднання та збільшує лічильник.

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

Значення slow_consumers, яке постійно зростає, означає, що повідомлення відкидаються. Тому налаштуйте сповіщення про його зростання, а не перевіряйте його одноразово. Core NATS підходить для повідомлень, цінність яких швидко втрачає актуальність: метрики, оновлення стану присутності або інвалідація кешу, яку наступна подія все одно замінить.

NATS JetStream: довговічні streams і replay у тому самому процесі

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, що саме настільки довговічно, як і випливає з назви. Створіть stream за допомогою 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 визначає, що переживе аварійне завершення роботи, оскільки memory stream цього не робить. --max-bytes=1073741824 обмежує stream обсягом 1 GiB у байтах, а --discard old видаляє найстаріші повідомлення після досягнення ліміту, замість того щоб відхиляти нові записи. Якщо не встановити ліміт, один неконтрольований publisher заповнить диск. Тоді зупиниться і база даних, оскільки вони використовують той самий диск.

Durable consumer зберігає власну позицію у stream і не втрачає її після перезапуску. Налаштуйте --max-deliver для consumer, щоб повідомлення, обробка якого постійно завершується помилкою, не доставлялося повторно безкінечно. Коли для повідомлення вичерпано кількість спроб доставки, JetStream публікує advisory у $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>. Підписка на цей subject дає змогу побудувати dead letter path, який RabbitMQ надає як готову функцію. Цю логіку потрібно реалізувати самостійно.

Щоб переглянути backlog, виконайте nats stream report для отримання кількості збережених повідомлень і nats consumer report ORDERS для перегляду непідтверджених повідомлень та необроблених повідомлень для кожного consumer. Контролювати потрібно саме кількість необроблених повідомлень. Використайте du -sh для перевірки використання диска каталогом зі сховищем. Воно зростатиме, доки ліміт retention не почне видаляти старі дані.

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 target за допомогою policy.

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

Повідомлення потрапляє до dead letter з чотирьох причин: consumer відхиляє його з basic.reject або basic.nack, а requeue має значення false; спливає TTL (time to live) повідомлення; черга досягає обмеження довжини; або повідомлення перевищує ліміт доставок quorum queue. Починаючи з RabbitMQ 4.0 цей ліміт за замовчуванням дорівнює 20. Тому handler, який генерує помилку та надсилає nack, повторює спробу двадцять разів, а потім передає повідомлення до dead letter exchange замість нескінченного циклу.

Пам’ять часто створює несподівані проблеми в RabbitMQ на невеликому VPS. Значення default high watermark дорівнює 0.6 доступної RAM. Коли вузол перевищує цей поріг, RabbitMQ блокує всі з’єднання, які публікують повідомлення. Застосунок не отримує помилку. Він отримує операцію публікації, яка не повертається. У власному коді це виглядає як зависання. У startup log виводиться значення, обчислене вузлом:

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

Аналогічно, disk alarm блокує publisher-и, коли вільного місця залишається менше ніж 50 MB за замовчуванням. Quorum queues додають власні вимоги: документація передбачає щонайменше 32 bytes внутрішніх метаданих у пам’яті на одне повідомлення, приблизно 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

Backlog складається з двох чисел. Їхня комбінація показує, яка саме проблема виникла.

docker exec rabbitmq rabbitmqctl list_queues name messages messages_ready messages_unacknowledged

messages_ready очікує на consumer. messages_unacknowledged було доставлено, але для нього так і не надіслали ack. Зростання кількості unacknowledged повідомлень разом зі стабільною кількістю ready означає, що ваші worker-и отримали завдання, але перестали їх завершувати. Це інша проблема, ніж черга, яка просто не встигає обробляти повідомлення.

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

Це quickstart для Kafka 4.3.1, актуальний станом на August 2026, у режимі KRaft (Kafka Raft — вбудований контролер, який замінив ZooKeeper у Kafka 4.0). Еквівалент для контейнера — apache/kafka:4.3.1.

Скрипт запуску встановлює export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G", якщо ви не встановили його самостійно. Тому broker резервує 1 GB Java heap ще до збереження першого повідомлення. Крім того, йому потрібна вільна RAM для page cache, з якого він читає дані. На VPS із 2 GB оперативної пам’яті ваш застосунок конкурує з JVM за решту ресурсів.

Наступний неочікуваний момент — retention. log.retention.hours за замовчуванням дорівнює 168, тобто семи дням, а log.retention.bytes за замовчуванням дорівнює -1, тобто обмеження за розміром відсутнє. Kafka зберігає повідомлення протягом усього цього періоду незалежно від того, чи прочитав їх кожен consumer. Це основна потрібна вам функція, але на одному невеликому диску вона також може спричинити відмову. Тому встановіть ліміт у байтах для кожного topic заздалегідь.

Тепер про обмеження. Один broker означає replication factor 1, тому acks=all зводиться до одного fsync на одному диску. Ви отримуєте надійність одного сервера та операційні витрати на JVM broker і controller. Partitions забезпечують паралелізм між broker, яких у вас немає. Реплікація, rack awareness та інші функції для кластера залишаються невикористаними. JetStream на тому самому сервері забезпечує такий самий durable replay, використовуючи значно менше пам’яті. Є дві причини все ж обрати Kafka: downstream-інструмент підтримує лише Kafka protocol (наприклад, change data capture через Debezium або завантажувач для аналітики), або ви відтворюєте production topology у зменшеному вигляді. План розширення до кластера означає придбання додаткових серверів. До цього моменту компроміс такий самий, як у випадку з запуском k3s на одному вузлі, коли за надійність одного вузла ви сплачуєте складністю кластера.

Backlog у Kafka — це consumer lag.

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

Перегляньте стовпець LAG. Для кожної partition він дорівнює LOG-END-OFFSET мінус CURRENT-OFFSET. Якщо lag зростає в одній partition, а в інших залишається незмінним, це вказує на нерівномірний розподіл ключів. Усі повідомлення з однаковим ключем потрапляють до тієї самої partition, і їх обробляє один consumer.

Що відбувається після перезапуску сервера

Core NATS втрачає всі повідомлення в обробці та одразу запускається знову, оскільки відновлювати нічого. JetStream завантажує streams і позиції consumers зі store directory, тому consumers продовжують роботу з того самого offset. RabbitMQ відновлює quorum queues з диска, а classic transient queues і всі повідомлення, опубліковані без persistent delivery mode, буде втрачено. Kafka під час запуску повторно відтворює log segments. Після некоректного завершення роботи перевірка для відновлення може тривати кілька хвилин на невеликому диску, перш ніж broker почне приймати з’єднання.

Є два параметри, які варто налаштувати заздалегідь. Задайте для container політику перезапуску (restart: unless-stopped) або увімкніть unit systemd, щоб broker автоматично повертався в роботу після перезавантаження через оновлення kernel. Потім налаштуйте порядок запуску. Якщо broker стає готовим через двадцять секунд після application, він відхилить перші з’єднання, а деякі client libraries завершать роботу замість повторної спроби. Заблокуйте запуск app, доки broker не буде готовий, за допомогою healthchecks у Compose, які затримують запуск залежного сервісу до готовності broker.

Вартість на власному 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, ви виміряєте брокер і генератор разом. Це прийнятно, якщо під час подання результату ви зазначите це.

Для всіх цих брокерів діє одна верхня межа. Кожна надійна опція тут очікує на fsync. Тому на VPS із мережевим сховищем саме диск визначає ліміт, і заміна брокера його не змінить.

Три робочі навантаження та черга повідомлень, потрібна для кожного з них

  1. Фонові завдання для одного вебзастосунку, наприклад надсилання електронної пошти, зміна розміру зображень або доставка webhook. Почніть із Postgres і SKIP LOCKED. Переходьте на RabbitMQ із quorum queues, якщо потрібні підтвердження для кожного повідомлення, ліміт доставок і dead letter queue, яку можна перевіряти без самостійної реалізації цієї логіки, або якщо таблиця завдань стала найбільш завантаженою таблицею в базі даних.
  2. Події, на які реагують кілька внутрішніх сервісів, коли втрачене повідомлення швидко замінюється новішим. Використовуйте Core NATS із subject як схемою маршрутизації та queue groups, якщо потрібно розподіляти обробку. Додайте потік JetStream для вузького набору subject, які мають зберігатися після перезапуску, а решту залиште в пам’яті.
  3. Журнал подій, який споживачі читають від початку, щоб вести аудит, відновлювати read model або згодом передавати дані в аналітичні системи. Використовуйте JetStream із файловим сховищем і явним обмеженням у байтах. Обирайте Kafka лише тоді, коли downstream-інструмент потребує протоколу Kafka, і враховуйте JVM heap як плату за таку сумісність.

На одному сервері наслідком неправильного вибору буде не низька пропускна здатність. Проблема виникне під час відновлення о третій ночі, коли потрібно буде знати, чи збереглися повідомлення. Обирайте з урахуванням цього.

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 забезпечує надійне повторне відтворення повідомлень на тому самому обладнанні та потребує значно менше пам’яті.

Чи потрібна мені message queue, якщо я вже використовую Postgres?

Часто — ні. Таблиця job із читанням за допомогою SELECT ... FOR UPDATE SKIP LOCKED у межах транзакції забезпечує доставку щонайменше один раз, безпечну паралельну роботу обробників, повторні спроби та dead letter table. Для цього не потрібен додатковий сервіс, який треба моніторити, а резервні копії ви вже створюєте. Перехід на окрему чергу виправданий у конкретних випадках: таблиця черги стає найбільшим джерелом операцій запису, а autovacuum не встигає; довготривалі завдання утримують транзакції відкритими та блокують vacuum для всієї бази даних; або другому сервісу потрібно незалежно обробляти ті самі події.

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

RabbitMQ варто вибрати, якщо вам потрібні підтвердження для кожного повідомлення, ліміт доставок і маршрутизація до dead letter як вбудована поведінка. Quorum queues завжди надійно зберігаються, ліміт доставок за замовчуванням становить 20, починаючи з RabbitMQ 4.0, а policy надсилає повідомлення, для яких вичерпано ліміт, до dead letter exchange, звідки їх можна отримати та перевірити. JetStream варто вибрати, якщо ті самі події згодом потрібно повторно відтворювати для інших споживачів, оскільки stream зберігає повідомлення після підтвердження, а queue — ні. У JetStream ви налаштовуєте --max-deliver і самостійно створюєте шлях dead letter на основі advisory $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. Core NATS не має backlog, який можна переглянути, оскільки він нічого не зберігає. Натомість стежте за лічильником slow_consumers у http://localhost:8222/varz: він показує кількість з’єднань, які сервер закрив через відставання споживачів, а це означає втрату повідомлень.

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