📩 سیستم پیامک در مقیاس بزرگ؛ چرا همهی پیامکها نباید در یک صف باشند؟ (Part1️⃣)
فرض کن ساعت ۵ عصر است.
یک سیستم بانکی باید همزمان این پیامها را ارسال کند:
🔐 پیامک OTP برای ورود کاربران
💳 پیامک برداشت و تراکنش مالی
📦 پیامک وضعیت سفارش
📢 پیامک تبلیغاتی
📰 پیامک اطلاعرسانی عمومی
حالا تصور کن یک کمپین تبلیغاتی شروع شده و ۷ میلیون پیامک وارد سیستم شده است.
اگر همهی پیامکها را داخل یک صف قرار دهیم، چه اتفاقی میافتد؟
[Marketing 1]
[Marketing 2]
...
[Marketing 7,000,000]
[OTP Login]
کاربر منتظر ورود به حسابش است، اما پیامک OTP او پشت میلیونها پیامک تبلیغاتی گیر کرده.
از دید سیستم، صف هنوز در حال کار است.
اما از دید کاربر:
سیستم خراب است! 😐
اینجا باید از Priority Queue استفاده کنیم.
🎯 ایدهی اصلی Priority Queue
در صف معمولی، پیامها بر اساس زمان ورود پردازش میشوند:
First In → First Out
اما در Priority Queue، هر پیام علاوه بر اطلاعات خودش، یک سطح اهمیت هم دارد:
{
"messageId": "msg-123",
"type": "OTP",
"priority": "Critical"
}سیستم بهجای اینکه فقط بپرسد:
کدام پیام زودتر وارد شده؟
میپرسد:
کدام پیام مهمتر است و باید زودتر پردازش شود؟
برای مثال:
Priority 0 → Critical
Priority 1 → High
Priority 2 → Normal
Priority 3 → Low
نکتهی مهم:
ءPriority Queue فقط ترتیب پردازش را مشخص میکند؛ ارسال واقعی همچنان به ظرفیت Provider، محدودیت شبکه و وضعیت مقصد وابسته است.
🧩 یک صف یا چند صف؟
برای پیادهسازی Priority Queue دو روش اصلی وجود دارد.
❗️روش اول: یک صف با Priority
در این روش همهی پیامها در یک Queue هستند، اما هر پیام Priority دارد:
Queue
├── Message A - Priority 3
├── Message B - Priority 1
├── Message C - Priority 2
└── Message D - Priority 0
ءConsumer باید پیامها را بر اساس Priority دریافت کند.
✅مزیت:
مدیریت سادهتر
یک مسیر پردازش
مناسب برای سیستمهای کوچکتر
❌مشکل:
کنترل ظرفیت هر سطح دشوارتر میشود.
ممکن است پیامهای مهم وارد Consumerهای اشغالشده توسط پیامهای کماهمیت شوند.
رفتار واقعی به نوع Broker و تنظیمات Consumer وابسته است.
در RabbitMQ، Priority Queue وجود دارد؛ اما مستندات رسمی آن تأکید میکنند که اگر Consumer مقدار زیادی پیام را با
prefetch دریافت کرده باشد، پیام Priority بالا ممکن است مجبور شود پشت پیامهای کماهمیتی که قبلاً به Consumer تحویل داده شدهاند منتظر بماند.پس این تنظیم مهم است:
Consumer Prefetch
اگر Prefetch بیش از حد بزرگ باشد، Broker تعداد زیادی پیام را زودتر به Consumer تحویل میدهد و فرصت اولویتبندی کاهش پیدا میکند.
❗️روش دوم: چند صف جداگانه
در سیستمهای مهمتر، معمولاً Priorityها را به Queueهای جدا تقسیم میکنیم:
sms-critical
sms-high
sms-normal
sms-low
سپس Dispatcher تصمیم میگیرد از کدام صف پیام بردارد.
این مدل کنترل بیشتری میدهد و میتوان برای هر صف Consumer یا ظرفیت جداگانه داشت.
⚠️ مشکل خطرناک: Starvation
فرض کن سیستم همیشه پیامکهای Critical دریافت میکند.
Dispatcher هم همیشه این منطق را اجرا میکند:
تا وقتی Critical خالی نشده:
فقط Critical را پردازش کن
در این شرایط، پیامکهای Low ممکن است هیچوقت پردازش نشوند.
به این مشکل میگوییم:
Starvation
یعنی یک گروه از کارها دائماً منابع دریافت نمیکنند.
مثلاً:
Critical Queue:
همیشه پر است
Low Queue:
هیچوقت نوبتش نمیرسد
این رفتار برای OTP شاید مناسب باشد، اما برای پیامکهای عادی میتواند باعث شود:
پیامها بیش از حد قدیمی شوند؛
کمپینها بیارزش شوند؛
هزینهی نگهداری Queue افزایش پیدا کند؛
و Backlog دائماً رشد کند.
🛠 راهحل: Weighted Fair Scheduling
بهجای اینکه همیشه فقط صف Critical را مصرف کنیم، برای هر صف سهمی تعیین میکنیم.
مثلاً:
Critical → 70%
High → 20%
Normal → 8%
Low → 2%
یعنی Dispatcher تلاش میکند بیشتر ظرفیت را به پیامهای مهم بدهد، اما صفهای دیگر را کاملاً گرسنه نگذارد.
یک مدل ساده:
در هر 100 پیام:
70 پیام از Critical
20 پیام از High
8 پیام از Normal
2 پیام از Low
⏳ ءAging؛ اولویت پیام قدیمی را افزایش بده
یک راه دیگر برای جلوگیری از Starvation، تکنیک Aging است.
ایده:
هرچه یک پیام بیشتر منتظر بماند، اولویت مؤثر آن بیشتر شود.
مثلاً:
Marketing Message:
Priority = Low
بعد از چند دقیقه:
Effective Priority = Normal
و اگر باز هم پردازش نشد:
Effective Priority = High
این فرمول فقط برای توضیح مفهوم است؛ در سیستم واقعی باید مراقب باشیم Aging باعث نشود پیامهای قدیمی تبلیغاتی ناگهان OTPهای جدید را کنار بزنند.
برای همین معمولاً Priorityهای امنیتی و تراکنشی قوانین سختگیرانهتری دارند.