مقایسه NATS و RabbitMQ و Kafka برای اجرا روی یک VPS
برای انتخاب صف پیام روی یک سرور، این راهنما تفاوت تضمین تحویل، مصرف رم و رفتار در زمان ریستارت را بررسی میکند. بفهمید چه زمانی Postgres جایگزین بهتری برای این ابزارهاست.
پاسخ کوتاه برای یک سرور
استفاده از صف پیام (message queue) روی یک VPS، تصمیمی در مورد تضمین تحویل پیام است، نه سرعت. روی یک سرور واحد، broker بهندرت گلوگاه محسوب میشود، زیرا کد برنامه، پایگاه داده و دیسک شما زودتر از آن به محدودیت میرسند. ابزاری را انتخاب کنید که رفتار آن در زمان خرابی برایتان قابلقبول باشد، سپس سروری که در اختیار دارید را ارزیابی کنید.
چهار گزینه، به ترتیبی که اکثر کاربران باید در نظر بگیرند:
- از پایگاه دادهای که هماکنون اجرا میکنید استفاده کنید. Postgres با
SELECT ... FOR UPDATE SKIP LOCKEDیک صف کاری کارآمد است و هیچ پردازش جدیدی برای مانیتور کردن اضافه نمیکند. - زمانی از RabbitMQ استفاده کنید که هر پیام یک واحد کاری است که باید تایید (acknowledge) شود، تعداد دفعات محدودی بازنشانی (retry) شود و سپس در جایی قرار گیرد که انسان بتواند آن را بررسی کند.
- زمانی از NATS استفاده کنید که پیامها رویدادهایی هستند که چندین بخش از سیستم شما به آنها واکنش نشان میدهند. برای رویدادهایی که باید پس از راهاندازی مجدد باقی بمانند، JetStream را فعال کنید.
- زمانی از Kafka استفاده کنید که ابزار مقصد فقط از پروتکل Kafka پشتیبانی میکند. روی یک سرور، این تقریباً تنها دلیل باقیمانده برای انتخاب آن است.
باقی این راهنما به استدلالهای این موضوع میپردازد: هر گزینه چه میزان حافظه و دیسک روی یک VPS کوچک اشغال میکند، هنگام reboot شدن سرور چه رفتاری دارد و دستور دقیقی که پیش از متوجه شدن کاربران، انباشت پیامها (backlog) را به شما نشان میدهد چیست.
تضمین تحویل پیام واقعاً به چه معناست
حداکثر یکبار (At most once) به این معنی است که کارگزار (broker) پیام را تحویل میدهد و آن را فراموش میکند. اگر هیچ مصرفکنندهای (consumer) متصل نباشد، یا مصرفکننده در میانهٔ کار از کار بیفتد، پیام از بین میرود و هیچ گزارشی از آن ثبت نمیشود.
حداقل یکبار (At least once) به این معنی است که مصرفکننده پس از موفقیتآمیز بودن کار، یک تأییدیه (ack) ارسال میکند. تا زمانی که این تأییدیه نرسد، کارگزار پیام را نگه میدارد و دوباره آن را تحویل خواهد داد. تحویل مجدد دلیل اصلی این است که هندلرهای (handlers) شما باید idempotent باشند: پردازش یک پیام تکراری نباید باعث شود که مثلاً کارت بانکی دو بار شارژ شود. تضمین «دقیقاً یکبار» (Exactly once) به صورت سرتاسری، چیزی نیست که کارگزار به تنهایی ارائه دهد. این ویژگی از طریق یک کلید یکتا در پایگاه دادهٔ خودتان حاصل میشود.
بازپخش (Replay) یک ویژگی مجزا است. یک صف (queue)، پیام را پس از تأیید شدن حذف میکند. اما یک لاگ (log)، پیام را برای یک بازهٔ زمانی مشخص نگه میدارد تا یک مصرفکنندهٔ جدید بتواند از ابتدا شروع کرده و کل تاریخچه را بخواند. Kafka و NATS JetStream در دستهٔ لاگها قرار میگیرند. RabbitMQ یک صف است. این تفاوت، بیش از میزان throughput، بر معماریهای مختلف تأثیر میگذارد.
صف پیامهای مرده (Dead lettering) سرنوشت پیامی است که پیوسته با خطا مواجه میشود. بدون این قابلیت، یک پیام سمی (poison message) در یک حلقهٔ بیپایان گیر میکند و این حلقه ممکن است در ظاهر شبیه به یک worker پرمشغله به نظر برسد، نه یک worker خراب.
با Postgres شروع کنید و اجازه دهید کارگزار (broker) خود را اثبات کند
بیشتر بارهای کاری تکبرنامهای، شامل چند هزار job پسزمینه در روز هستند. این حجم بهراحتی در یک جدول جای میگیرد.
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);یک worker، یک job را درون یک تراکنش (transaction) تصاحب میکند.
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 تمام ترفند کار است. این دستور ردیفی را که برمیگرداند قفل میکند و از هر ردیفی که تراکنش دیگری قبلاً قفل کرده است، عبور میکند؛ بنابراین دو worker هرگز یک job مشابه را تصاحب نمیکنند. اگر یک worker کرش کند، Postgres تراکنش آن را لغو (abort) میکند، قفل آزاد میشود و ردیف برای worker بعدی قابل مشاهده خواهد بود. شما تحویل حداقل-یکبار (at-least-once delivery)، تلاش مجدد با افزایش attempts و یک جدول dead letter را به دست میآورید؛ همه اینها با پایداری (durability) که از قبل بابت آن هزینه پرداخت کردهاید. صف عقبافتاده (backlog) تنها یک کوئری است: SELECT count(*) FROM job WHERE run_after <= now();
جایی که این روش دیگر کار نمیکند: هر تصاحب و حذف، یک عملیات نوشتن (write) است؛ بنابراین نرخ بالای job باعث باقی ماندن نسخههای مرده ردیفها میشود و جدول صف، نمونه کلاسیک تورم (bloat) است که از autovacuum پیشی میگیرد. jobهای طولانی وضعیت را بدتر میکنند، زیرا تراکنشی که به اندازه طول مدت کار باز میماند، افق vacuum را برای کل دیتابیس عقب نگه میدارد. Polling باعث ایجاد تأخیر میشود و LISTEN با NOTIFY عمل polling را حذف میکند اما عملیات نوشتن همچنان باقی است. زمانی که جدول job شلوغترین جدول شماست، یا سرویس دومی به همان رویدادها نیاز دارد، کار را به خارج از دیتابیس منتقل کنید. این انتخاب با نحوه استقرار خود دیتابیس در تعامل است، بنابراین پیش از افزودن یک کارگزار در کنار آن، در مورد اجرای دیتابیس در Docker یا روی host تصمیم بگیرید.
Redis گزینه دیگری است که ممکن است همین حالا از آن استفاده کنید. Redis Streams به شما consumer groupهایی با XADD و XREADGROUP، یک لیست معلق (pending list) برای هر گروه و XAUTOCLAIM برای بازپسگیری کار از یک consumer که از کار افتاده است، میدهد. این ابزار کوچک و سریع است. نکته صادقانه در مورد یک سرور واحد: با تنظیم معمول appendfsync everysec، قطع برق میتواند باعث از دست رفتن حدود یک ثانیه از عملیات نوشتن شود. این برای ابطال cache مناسب است اما برای پرداختها اشتباه است. اگر برنامه شما یک پردازش واحد است که حول SQLite در محیط production روی یک VPS ساخته شده، همان الگوی تصاحب-و-حذف کار میکند، هرچند SQLite معادل SKIP LOCKED را ندارد و هر worker روی همان یک قفل نوشتن (write lock) سریالسازی میشود.
NATS core: مسیریابی موضوعمحور (subject routing) بدون حافظه
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 محدود کنید.
NATS core از مدل تحویل حداکثر یکبار (at most once) استفاده میکند و هیچ دادهای را ذخیره نمیکند. ناشر (publisher) پیامی را به یک موضوع (subject) مانند orders.created ارسال میکند و هر مشترکی (subscriber) که فیلترش با آن مطابقت داشته باشد، یک نسخه از پیام را دریافت میکند. اگر هیچکس مشترک نباشد، پیام حذف میشود و ناشر هیچ خطایی دریافت نمیکند، زیرا وظیفه ناشر در لحظهای که سرور بایتها را دریافت کرد، به پایان رسیده است. یک گروه صف (queue group) که در آن چندین مشترک یک نام گروه مشترک دارند، باعث میشود سرور برای هر پیام یکی از اعضا را انتخاب کند؛ این کار توزیع بار را بدون نیاز به ذخیرهسازی صف انجام میدهد.
میزان مصرف منابع (footprint) شامل وضعیت اشتراکها به اضافه یک بافر نوشتن برای هر اتصال است؛ بنابراین این سیستم تعداد اتصالات را ردیابی میکند، نه حجم پیامها را، و هیچ دادهای روی دیسک انباشته نمیشود. رفتار سیستم پس از راهاندازی مجدد (restart) نیز از همین ویژگی ناشی میشود: پیامهای در حال انتقال از بین میروند، کلاینتها بهطور خودکار دوباره متصل میشوند و هیچ مرحله بازیابی (recovery) برای انتظار وجود ندارد.
هیچ صف عقبماندهای (backlog) برای نظارت وجود ندارد، بنابراین باید مراقب از دست رفتن پیامها باشید. زمانی که یک مشترک سوکت خود را کندتر از سرعت نوشتن سرور میخواند، بافر سرور برای آن کلاینت پر میشود. اگر کلاینت تا زمان تعیینشده برای نوشتن (write deadline) عقبماندگی خود را جبران نکند، سرور کل اتصال را میبندد و یک شمارنده را افزایش میدهد.
curl -s http://localhost:8222/varz | jq '.slow_consumers, .connections, .in_msgs, .out_msgs'بالا رفتن مداوم مقدار slow_consumers به معنای حذف شدن پیامهاست، بنابراین بهجای خواندن مقطعی آن، روی این مقدار هشدار (alert) تنظیم کنید. NATS core برای پیامهایی مناسب است که ارزش آنها بهسرعت منقضی میشود: مانند یک متریک، بهروزرسانی وضعیت حضور (presence update)، یا ابطال کش (cache invalidation) که رویداد بعدی بههرحال جایگزین آن خواهد شد.
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 همان چیزی است که پس از کرش باقی میماند، چرا که استریمهای حافظهمحور (memory stream) پس از کرش از بین میروند. --max-bytes=1073741824 سقف استریم را روی 1 GiB تنظیم میکند (به صورت تعداد بایت) و --discard old با رسیدن به این سقف، پیامهای قدیمیتر را حذف میکند تا از رد شدن پیامهای جدید جلوگیری شود. اگر سقف را تعیین نکنید، یک ناشر (publisher) خارج از کنترل، دیسک را پر میکند و در آن لحظه دیتابیس شما نیز از کار میافتد، زیرا هر دو از یک دیسک مشترک استفاده میکنند.
یک مصرفکننده پایدار (durable consumer) موقعیت خود را در استریم حفظ میکند و این وضعیت پس از راهاندازی مجدد (restart) نیز باقی میماند. گزینه --max-deliver را روی مصرفکننده تنظیم کنید تا پیامی که همیشه با خطا مواجه میشود، برای همیشه بازنشر نشود. وقتی تعداد دفعات تحویل یک پیام به پایان میرسد، JetStream یک اعلان (advisory) روی $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.> منتشر میکند؛ اشتراک در این موضوع (subject) روشی است که با آن میتوانید مسیر پیامهای مرده (dead letter path) را بسازید؛ قابلیتی که در RabbitMQ به صورت پیشفرض وجود دارد. این کاری است که باید خودتان پیادهسازی کنید.
برای مشاهده صفهای عقبمانده (backlog)، دستور nats stream report را برای تعداد پیامهای ذخیرهشده و nats consumer report ORDERS را برای تأییدیههای معلق و پیامهای پردازشنشده به ازای هر مصرفکننده اجرا کنید. عدد مربوط به پیامهای پردازشنشده همان موردی است که باید برای آن هشدار (alarm) تنظیم کنید. هزینه دیسک با اجرای du -sh روی دایرکتوری ذخیرهسازی قابل مشاهده است و این مقدار تا زمانی که محدودیت نگهداری (retention limit) آن را کاهش دهد، رشد میکند.
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 (پروتکل صفبندی پیام پیشرفته) و پورت 15672 برای رابط مدیریتی استفاده میشود. رابط مدیریتی را روی localhost نگه دارید و از طریق SSH tunnel به آن دسترسی پیدا کنید.
صفها را با تنظیم آرگومان x-queue-type روی quorum تعریف کنید؛ مقدار پیشفرض همچنان classic است. صفهای Quorum همیشه پایدار (durable) هستند و پیش از هر عملیاتی، دادهها را روی دیسک مینویسند. بنابراین در یک گره (node)، بهجای مواجهه با ماتریسی از گزینههای پایدار و موقت، رفتاری واحد و شفاف خواهید داشت. مقصد پیامهای ناموفق (dead letter) را با استفاده از یک 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 (زمان ماندگاری) پیام منقضی میشود، طول صف از حد مجاز فراتر میرود، یا پیام از حد مجاز تحویل در صف Quorum عبور میکند. این حد مجاز از RabbitMQ 4.0 به بعد بهصورت پیشفرض 20 است؛ بنابراین هندلری که خطا میدهد و پیام را nack میکند، پس از 20 بار تلاش مجدد، پیام را بهجای ایجاد حلقه، به dead letter exchange میفرستد.
حافظه (RAM) جایی است که RabbitMQ در VPSهای کوچک کاربران را غافلگیر میکند. حد آستانه بالای حافظه بهصورت پیشفرض 0.6 از RAM موجود است و وقتی گره از آن عبور کند، RabbitMQ تمام اتصالات در حال انتشار (publishing) را مسدود میکند. برنامه شما خطایی دریافت نمیکند؛ بلکه عملیات انتشار هرگز بازنمیگردد که در کد شما بهصورت یک وقفه (hang) دیده میشود. لاگ راهاندازی، عددی را که گره محاسبه کرده است نمایش میدهد:
Memory high watermark set to 1024 MiB (1073741824 bytes) of 8192 MiB (8589934592 bytes) totalهشدار دیسک نیز به همین ترتیب، زمانی که فضای خالی بهصورت پیشفرض به زیر 50 MB برسد، ناشران را مسدود میکند. صفهای Quorum محاسبات خاص خود را اضافه میکنند: مستندات حداقل 32 بایت متادیتای درونحافظهای برای هر پیام (حدود 1 MB برای هر 30,000 پیام) را در نظر میگیرند و توصیه میکنند حداقل سه برابر اندازه موثر write-ahead log در RAM موجود باشد. حد مجاز WAL بهصورت پیشفرض 512 MiB است، بنابراین همین توصیه بهتنهایی 1.5 GB حافظه میطلبد. در یک سرور 2 GB، این مقدار را در rabbitmq.conf کاهش دهید و به پیشفرضها تکیه نکنید.
raft.wal_max_size_bytes = 64000000
vm_memory_high_watermark.relative = 0.5Backlog شامل دو عدد است و این جفت عدد به شما میگوید با چه نوع شکستی مواجه هستید.
docker exec rabbitmq rabbitmqctl list_queues name messages messages_ready messages_unacknowledgedmessages_ready نشاندهنده انتظار برای مصرفکننده است. messages_unacknowledged پیامهایی هستند که تحویل داده شده اما هرگز تأیید (ack) نشدهاند. افزایش تعداد پیامهای تأییدنشده در کنار ثابت ماندن تعداد پیامهای آماده، به این معناست که 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این راهنمای سریع برای Kafka 4.3.1 است که در آگوست 2026 بهروزرسانی شده و در حالت KRaft (یعنی Kafka Raft، کنترلکننده داخلی که در Kafka 4.0 جایگزین ZooKeeper شد) اجرا میشود. معادل کانتینری آن apache/kafka:4.3.1 است.
اسکریپت راهاندازی در صورتی که خودتان مقداردهی نکرده باشید، export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" را تنظیم میکند. بنابراین بروکر پیش از ذخیره حتی یک پیام، 1 گیگابایت از Java heap را رزرو میکند و علاوه بر آن، انتظار دارد رم آزاد برای page cache در دسترس باشد. روی یک VPS با 2 گیگابایت رم، برنامه شما برای باقیمانده منابع با JVM رقابت خواهد کرد.
موضوع بعدی که ممکن است غافلگیرکننده باشد، Retention است. مقدار پیشفرض log.retention.hours برابر با 168 است که معادل هفت روز میباشد و log.retention.bytes بهطور پیشفرض روی -1 تنظیم شده که به معنای عدم وجود محدودیت حجمی است. Kafka پیامها را در تمام این بازه زمانی نگه میدارد، فارغ از اینکه همه مصرفکنندگان (consumers) آنها را خوانده باشند یا خیر. این همان قابلیتی است که به خاطرش Kafka را انتخاب کردهاید، اما روی یک دیسک کوچک، همین ویژگی میتواند باعث بروز خطا شود؛ بنابراین پیش از آنکه با مشکل مواجه شوید، برای هر topic یک محدودیت حجمی (byte limit) تعیین کنید.
حالا بخش واقعبینانه ماجرا: یک بروکر واحد به معنای replication factor 1 است، بنابراین acks=all به یک fsync روی یک دیسک ختم میشود. شما پایداری یک ماشین را با هزینه عملیاتی یک بروکر JVM به اضافه یک کنترلکننده دریافت میکنید. پارتیشنها برای شما موازیسازی در میان بروکرهایی را فراهم میکنند که ندارید. قابلیتهای Replication، آگاهی از رک (rack awareness) و سایر ویژگیهای کلاستر در اینجا غیرفعال هستند. JetStream همان قابلیت بازپخش (replay) پایدار را روی همان ماشین با مصرف حافظه بسیار کمتر به شما میدهد. دو دلیل برای توجیه استفاده از Kafka در این سناریو وجود دارد: یا یک ابزار پاییندستی فقط از پروتکل Kafka پشتیبانی میکند (مانند تغییر دادهها با Debezium یا یک بارگذار تحلیلی)، یا در حال شبیهسازی توپولوژی تولید (production) در مقیاس کوچک هستید. برنامهریزی برای رشد و تبدیل شدن به یک کلاستر، در واقع برنامهریزی برای خرید ماشینهای بیشتر است و تا آن زمان، این معامله همانند اجرای k3s روی یک نود است که در آن پیچیدگی کلاستر را برای پایداری یک نود پرداخت میکنید.
بکلاگ در Kafka همان consumer lag است.
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-groupستون LAG را بخوانید که حاصل تفریق CURRENT-OFFSET از LOG-END-OFFSET برای هر پارتیشن است. افزایش Lag روی یک پارتیشن در حالی که سایر پارتیشنها ثابت ماندهاند، نشاندهنده توزیع نامتوازن کلید (key) است؛ زیرا تمام پیامهایی که کلید یکسانی دارند در یک پارتیشن قرار میگیرند و تنها یک مصرفکننده مسئول پردازش آنهاست.
هنگام راهاندازی مجدد سرور چه اتفاقی میافتد
سرویس Core NATS تمام دادههای در حال انتقال را از دست میدهد و بلافاصله بالا میآید، زیرا چیزی برای بازیابی وجود ندارد. JetStream جریانها (streams) و موقعیت مصرفکنندگان (consumer positions) را از دایرکتوری ذخیرهسازی بارگذاری میکند، بنابراین مصرفکنندگان از همان آفستی که داشتند ادامه میدهند. RabbitMQ صفهای quorum را از دیسک بازیابی میکند، در حالی که صفهای کلاسیک موقت و هر پیامی که بدون حالت persistent delivery منتشر شده باشد، از بین میروند. Kafka قطعات لاگ خود را هنگام شروع مجدد بازپخش میکند و پس از یک خاموشی غیرمنتظره (unclean shutdown)، این اسکن بازیابی ممکن است روی یک دیسک کوچک چند دقیقه طول بکشد تا broker اتصال را بپذیرد.
دو مورد ارزش تنظیم کردن دارند. به container یک restart policy (restart: unless-stopped) بدهید یا unit مربوط به systemd را فعال کنید تا broker پس از راهاندازی مجدد برای ارتقای هسته (kernel)، بدون دخالت شما بازگردد. سپس ترتیب را مدیریت کنید: brokerای که بیست ثانیه پس از برنامه شما آماده میشود، اتصالات اولیه را رد میکند و برخی کتابخانههای کلاینت بهجای تلاش مجدد، خارج میشوند. برنامه را با استفاده از بررسیهای سلامت در Compose که سرویس وابسته را تا زمان آماده شدن broker متوقف نگه میدارند، کنترل کنید.
هزینه روی VPS شخصی، اندازهگیری بهجای نقلقول
اعداد توان عملیاتی (throughput) منتشرشده، روی سختافزاری اندازهگیری شدهاند که شما در اختیار ندارید؛ معمولاً یک سرور چندهستهای با NVMe محلی. آنها را بهعنوان سقف توانایی در نظر بگیرید و عملکرد سرور خود را شخصاً اندازهگیری کنید.
docker stats --no-stream
free -m
sudo du -sh /var/lib/docker/volumes/*/_dataاین تستها را یکبار در حالت بیکار (idle) بودن بروکر و بار دیگر تحت ترافیک واقعی خود اجرا کنید. شکاف بین این دو عدد، معیاری است که تعیین میکند آیا بروکر در کنار اپلیکیشن شما جای میگیرد یا خیر. برای بهدست آوردن یک کفِ تقریبی برای توان عملیاتی، بهجای تکیه بر پستهای وبلاگی دیگران، از ابزار تولید بار (load generator) اختصاصی هر پروژه استفاده کنید: 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) استفاده میکنند، دیسک تعیینکننده محدودیت است و تعویض بروکرها تغییری در آن ایجاد نخواهد کرد.
سه بار کاری و صف پیامی که هر کدام نیاز دارند
- کارهای پسزمینه برای یک برنامه وب، مانند ارسال ایمیل، تغییر اندازه تصاویر یا تحویل webhooks. با Postgres و
SKIP LOCKEDشروع کنید. زمانی که به تاییدیه (ack) برای هر پیام، محدودیت در تحویل و یک صف پیامهای ناموفق (dead letter queue) نیاز دارید که بتوانید بدون نوشتن منطق آن، بررسیاش کنید، یا زمانی که جدول کارهای شما به شلوغترین جدول در پایگاه داده تبدیل شده است، به سراغ RabbitMQ با quorum queues بروید. - رویدادهایی که چندین سرویس داخلی به آنها واکنش نشان میدهند، جایی که پیام از دست رفته بهسرعت با یک پیام جدیدتر جایگزین میشود. از Core NATS با استفاده از موضوعات (subjects) به عنوان طرح مسیریابی و گروههای صف (queue groups) در جایی که نیاز به اشتراکگذاری کار دارید، استفاده کنید. برای مجموعه محدودی از موضوعات که باید پس از راهاندازی مجدد باقی بمانند، یک JetStream stream اضافه کنید و بقیه را در حافظه (memory) نگه دارید.
- یک لاگ رویداد که مصرفکنندگان (consumers) آن را از ابتدا میخوانند، برای مسیر حسابرسی (audit trail)، بازسازی یک مدل خواندن (read model) یا تغذیه تحلیلها در آینده. از JetStream با ذخیرهسازی فایل و محدودیت بایت صریح استفاده کنید. تنها زمانی Kafka را انتخاب کنید که یک ابزار پاییندستی به پروتکل Kafka نیاز داشته باشد و هزینه استفاده از JVM heap را به عنوان بهای آن سازگاری بپذیرید.
هزینه انتخاب اشتباه روی یک سرور، توان عملیاتی (throughput) نیست. بلکه بازیابی سیستم در ساعت 3 صبح است، زمانی که باید بدانید آیا پیامها هنوز وجود دارند یا خیر. بر اساس آن انتخاب کنید.
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 را میپردازید بدون اینکه از مدل پایداری (durability) آن بهرهمند شوید. NATS JetStream روی همان سختافزار، قابلیت replay پایدار را با مصرف حافظه بسیار کمتر ارائه میدهد.
آیا اگر از قبل Postgres دارم، باز هم به صف پیام نیاز دارم؟
اغلب خیر. خواندن یک جدول job با استفاده از SELECT ... FOR UPDATE SKIP LOCKED در یک تراکنش، تحویل حداقل یکباره (at-least-once)، کارگران (workers) همزمان ایمن، تلاش مجدد و جدول پیامهای ناموفق (dead letter table) را بدون نیاز به سرویس اضافی برای مانیتورینگ و با استفاده از بکآپهایی که از قبل میگیرید، فراهم میکند. نشانههایی که زمان مهاجرت به یک سیستم صف اختصاصی را مشخص میکنند عبارتند از: جدول صف به سنگینترین بار نوشتن شما تبدیل شده و autovacuum عقب میافتد، کارهای طولانیمدت تراکنشها را باز نگه داشته و vacuum کل دیتابیس را مسدود میکنند، یا سرویس دومی نیاز دارد که همان رویدادها را بهطور مستقل مصرف کند.
برای کارهای پسزمینه (background jobs) از NATS JetStream استفاده کنم یا RabbitMQ؟
اگر به تأییدیه (acknowledgement) برای هر پیام، محدودیت در تعداد دفعات تحویل و مسیریابی پیامهای ناموفق (dead letter routing) به عنوان قابلیتهای داخلی نیاز دارید، RabbitMQ را انتخاب کنید. Quorum queues همیشه پایدار هستند، محدودیت تحویل از نسخه 4.0 RabbitMQ بهطور پیشفرض روی 20 تنظیم شده است و یک policy پیامهای تمامشده را به یک dead letter exchange میفرستد که میتوانید آن را تخلیه و بررسی کنید. اگر همان رویدادها باید بعداً توسط مصرفکنندگان (consumers) دیگر بازخوانی (replay) شوند، JetStream را انتخاب کنید، زیرا یک stream پیامها را پس از تأییدیه نگه میدارد اما صف این کار را نمیکند. در JetStream شما --max-deliver را تنظیم میکنید و مسیر dead letter را خودتان بر اساس $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.> advisory میسازید.
چگونه بفهمم مصرفکنندگان من چقدر عقب هستند؟
هر بروکر یک دستور اختصاصی دارد. برای 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 را برای هر پارتیشن چاپ میکند. NATS اصلی هیچ backlog برای خواندن ندارد، زیرا چیزی ذخیره نمیکند؛ بنابراین به جای آن، شمارنده slow_consumers را در http://localhost:8222/varz مانیتور کنید: این شمارنده تعداد اتصالاتی را نشان میدهد که سرور به دلیل عقب ماندن مصرفکننده بسته است، که به معنای از دست رفتن پیام است.