RabbitMQ — תור הודעות וחלוקת עבודה
מה זה?
בשיעור הקודם למדנו לבחור בין תקשורת sync ל-async, עם Kafka כדוגמת ה-async היחידה. אבל יש בעיה ש-Kafka לא נועד לפתור בצורה הפשוטה ביותר: כששירות ה-Orders מפרסם “צריך לשלוח מייל אישור”, רוצים שבדיוק worker אחד מתוך קבוצה יטפל בכל מייל — לא שכל מופע (instance) של Email Service ישלח את אותו מייל בנפרד. RabbitMQ פותר בדיוק את זה: תור הודעות (Message Queue) קלאסי שמחלק משימות בין קבוצת workers, כשכל הודעה מטופלת בדיוק פעם אחת, עם אישור (Acknowledgment) מפורש שה-worker באמת סיים — ואם הוא קורס באמצע, ההודעה חוזרת לתור אוטומטית.
מילות מפתח שחשוב לזכור
• Queue — תור הודעות; כל הודעה ממתינה בו עד ש-worker פנוי לוקח אותה
• Producer — הצד ששולח (מפרסם) הודעה לתור
• Consumer (Worker) — הצד שמקבל הודעה מהתור ומעבד אותה
• Acknowledgment (ack) — אישור מפורש מה-Consumer ש"סיימתי לטפל בהודעה" — רק אז היא נמחקת סופית מהתור
• Work Queue — תבנית שבה כמה workers “מתחרים” על אותו תור, וכל הודעה הולכת ל-worker אחד בלבד (Competing Consumers)
var factory = new ConnectionFactory { HostName = "localhost" };
using var connection = await factory.CreateConnectionAsync();
using var channel = await connection.CreateChannelAsync();
await channel.QueueDeclareAsync(
queue: "email-tasks", durable: true,
exclusive: false, autoDelete: false);
await channel.BasicPublishAsync(
exchange: "", routingKey: "email-tasks", body: taskJson);// Consumer — acknowledges only after the work actually succeeds
consumer.ReceivedAsync += async (sender, ea) =>
{
var task = Encoding.UTF8.GetString(ea.Body.ToArray());
try
{
await SendEmailAsync(task);
await channel.BasicAckAsync(ea.DeliveryTag, false); // done — remove from queue
}
catch
{
await channel.BasicNackAsync(ea.DeliveryTag, false, true); // failed — requeue
}
};
await channel.BasicConsumeAsync(queue: "email-tasks", autoAck: false, consumer: consumer);הסבר עיקרי
Work Queue — צרכנים מתחרים
בניגוד לרעיון של “כולם מקבלים את ההודעה”, ב-RabbitMQ תור עם כמה workers מפזר את ההודעות ביניהם — כל הודעה הולכת ל-worker אחד בלבד (בברירת מחדל, round-robin). זה בדיוק הפתרון לבעיה מתחילת השיעור: אפשר להריץ 3 מופעים של Email Service, וכל מייל עדיין יישלח בדיוק פעם אחת — לא שלוש פעמים.
Acknowledgment — למה autoAck: false
autoAck: true אומר ל-RabbitMQ “תחשיב את ההודעה כמטופלת ברגע שהיא נשלחה ל-worker” — אם ה-worker קורס באמצע העיבוד, ההודעה כבר “נעלמה” מהתור לצמיתות, גם שבפועל היא לא טופלה. autoAck: false + BasicAckAsync ידני פותר את זה: ההודעה נשארת בתור (ומסומנת כ"תפוסה") עד שה-worker מאשר בפירוש שסיים. אם ה-worker קורס לפני האישור, RabbitMQ מזהה זאת ומחזיר את ההודעה לתור אוטומטית — worker אחר (או אותו אחד אחרי הפעלה מחדש) יקבל אותה שוב.
עמידות (Durability)
durable: true בהצהרת התור אומר ש-RabbitMQ עצמו יזכור אותו אחרי restart. זה לא מספיק לבד — צריך גם לסמן את ההודעות עצמן כ-Persistent (נשמרות לדיסק, לא רק בזיכרון). בלי שני אלה, קריסה של RabbitMQ עצמו יכולה למחוק תור שלם עם כל המשימות הממתינות בו.
יתרונות
חלוקת עבודה טבעית בין כמה workers, בלי לכתוב לוגיקת חלוקה בעצמכם; מנגנון retry מובנה (nack + requeue) בלי קוד נוסף; עמידה בפני קריסת worker בודד בלי לאבד משימות.
חסרונות
לא בנוי לשידור אירועים להמונים או ל"replay" היסטוריה כמו Kafka — הודעה בדרך כלל נצרכת פעם אחת ונעלמת; רכיב תשתית נוסף לפריסה ולניהול, בדיוק כמו Kafka.
נקודות חשובות
• RabbitMQ = תור עבודות (Work Queue) — כל הודעה ל-worker אחד בלבד, בניגוד ל-Kafka שמשדר לכל הצרכנים
• autoAck: false + BasicAckAsync ידני מבטיחים שהודעה לא הולכת לאיבוד אם worker קורס
• BasicNackAsync(..., requeue: true) מחזיר הודעה שנכשלה לתור לניסיון חוזר
• durable: true על התור + Persistent = true על ההודעה = שני התנאים לעמידות אחרי restart
טעויות נפוצות
• להשתמש ב-autoAck: true ואז worker קורס — ההודעה “נחשבת” כמטופלת אף שלא, ואבודה לצמיתות
• לבלבל בין RabbitMQ (תור עבודות, כל הודעה ל-worker אחד) לבין Kafka (יומן אירועים, כל צרכן יכול לקרוא הכל)
• לשכוח durable: true/Persistent ואז restart של RabbitMQ מוחק תור שלם עם כל ההודעות הממתינות
סיכום
RabbitMQ הוא message broker מבוסס תורים (queues) שמחלק משימות בין קבוצת workers — כל הודעה מטופלת בדיוק פעם אחת (Work Queue), עם אישור (Acknowledgment) מפורש שרק אחריו ההודעה נמחקת סופית, ואם worker קורס — היא חוזרת אוטומטית לתור. בניגוד ל-Kafka (יומן אירועים לשידור), RabbitMQ מתאים במיוחד לחלוקת עבודה — תרחישים כמו עיבוד מיילים, תשלומים או כל משימת רקע שצריכה טיפול פעם אחת בדיוק, לא שידור לכולם.
דוקומנטציה רשמית
תרגילים
תרגיל 1 — Producer/Consumer בסיסי
המשימה: הריצו RabbitMQ מקומית (Docker: docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:management), כתבו Producer שמפרסם 5 משימות לתור greeting-tasks, וConsumer שצורך אותן עם autoAck: false ומדפיס כל אחת.
בדיקה: כל 5 המשימות מודפסות, ותור greeting-tasks ריק בממשק הניהול (localhost:15672) אחרי שה-Consumer סיים.
תרגיל 2 — סימולציית כישלון
המשימה: שנו את ה-Consumer מתרגיל 1 כך שיזרוק Exception על ההודעה השלישית (לדוגמה בלבד), עם try/catch ש-BasicNackAsync(..., requeue: true) על כישלון.
בדיקה: ההודעה השלישית חוזרת לתור ומעובדת שוב (אפשר לראות אותה מגיעה פעמיים ב-log).
פרויקט מסכם
המשימה: הוסיפו תור עבודות אמיתי ל-TasksController (מהפרויקטים המסכמים הקודמים) — כשמשימה נוצרת, פרסמו הודעה לתור task-notifications, וכתבו worker נפרד (console app) שצורך אותה ומדמה שליחת התראה.
דרישות:
POST /tasksיוצר את המשימה ב-DB (כרגיל) וגם מפרסם הודעה ל-RabbitMQ עם פרטי המשימה- ה-worker הנפרד צורך מהתור, מדפיס “Notification sent for task X”, ומאשר (ack) רק אחרי ההדפסה
- אם ה-worker “נופל” (Exception מדומה) — ההודעה חוזרת לתור, לא נעלמת
בדיקה: יצירת משימה חדשה ב-API גורמת להודעה חדשה בתור; ה-worker מעבד אותה תוך שניות; כיבוי מוקדם של ה-worker (לפני ack) גורם להודעה לחזור ולהיות מטופלת מחדש כשהוא עולה שוב.
מה בפרק הבא
בפרק הבא נעבור ליחידה חדשה — פרויקט צוותי: מערכת מודיעין — הפרויקט הצוותי הראשון של הקורס, שמיישם בפועל את עקרונות המיקרוסרביסים שנלמדו (Bounded Context, תקשורת sync/async) בעבודת צוות אמיתית עם ריפו וקוד-ריוויו.