3.3 ארכיטקטורה מונעת אירועים הרצאה
ארכיטקטורה מונעת אירועים - Event-Driven Architecture¶
בקורס צד השרת למדנו על Kafka ותורי הודעות. עכשיו נסתכל על הנושא מזווית ארכיטקטונית - ארכיטקטורה מונעת אירועים (EDA) היא סגנון עיצוב שלם, לא רק כלי טכני.
בקשה לעומת אירוע¶
ישנם שני דפוסי תקשורת עיקריים:
תקשורת מבוססת בקשה - Request-Driven¶
A שולח בקשה ומחכה לתשובה. A יודע ש-B קיים. A תלוי ב-B.
תקשורת מבוססת אירועים - Event-Driven¶
A מפרסם אירוע ולא מחכה לאף אחד. A לא יודע מי מקשיב. A לא תלוי ב-B, C, או D.
מושגים מרכזיים¶
אירוע - Event: משהו שקרה. אירוע הוא עובדה - לא פקודה.
# לא נכון - זו פקודה, לא אירוע
{
"type": "send_welcome_email",
"to": "user@example.com"
}
# נכון - זה אירוע
{
"type": "user_registered",
"user_id": 42,
"email": "user@example.com",
"timestamp": "2026-04-03T10:30:00"
}
Producer: שירות שמפרסם אירועים.
Consumer: שירות שמקשיב לאירועים ומגיב עליהם.
Event Bus / Message Broker: המתווך - Kafka, RabbitMQ, Redis Pub/Sub.
דוגמה: TaskFlow עם EDA¶
בלי EDA:
user registers → UserService → EmailService.send_welcome()
→ AnalyticsService.track_signup()
→ NotificationService.setup_preferences()
UserService מכיר ומקרא לשלושה שירותים. אם אחד נופל - הרשמה כולה נכשלת.
עם EDA:
# user_service.py - רק מפרסם אירוע
import json
from kafka import KafkaProducer
producer = KafkaProducer(bootstrap_servers="localhost:9092")
def register_user(username: str, email: str, password: str) -> dict:
user = save_user_to_db(username, email, password)
# מפרסם אירוע - לא יודע מי יקשיב
event = {
"type": "user_registered",
"user_id": user["id"],
"email": email,
"username": username
}
producer.send("user-events", json.dumps(event).encode())
return user # חוזר מיד - לא מחכה לאף שירות
# email_consumer.py - Consumer נפרד
from kafka import KafkaConsumer
import json
consumer = KafkaConsumer("user-events", bootstrap_servers="localhost:9092")
for message in consumer:
event = json.loads(message.value)
if event["type"] == "user_registered":
send_welcome_email(event["email"], event["username"])
# analytics_consumer.py - Consumer נפרד
consumer = KafkaConsumer("user-events", bootstrap_servers="localhost:9092")
for message in consumer:
event = json.loads(message.value)
if event["type"] == "user_registered":
track_new_signup(event["user_id"])
עכשיו:
- UserService לא יודע ולא אכפת לו מ-email ו-analytics
- אפשר להוסיף consumer חדש בלי לגעת ב-UserService
- אם email_consumer נופל - ההרשמה עדיין מצליחה
Choreography לעומת Orchestration¶
Choreography - כוריאוגרפיה¶
כל שירות יודע על מה לקשיב ומה לפרסם. אין שירות מרכזי שמנהל את הזרימה.
TaskCreated → notify-service: שולח מייל
→ analytics-service: מתעד
→ search-service: מאנדקס
TaskCompleted → notify-service: שולח מייל
→ billing-service: מחשב חיוב
יתרון: מבוזר, אין נקודת כשל מרכזית.
חסרון: קשה לראות את התמונה הכוללת של זרימת העסקה.
Orchestration - אורקסטרציה¶
שירות מרכזי ("המנצח") אחראי על תיאום הזרימה.
# task_orchestrator.py
class TaskCreationOrchestrator:
def __init__(self, task_service, notify_service, analytics_service, search_service):
self.task_service = task_service
self.notify = notify_service
self.analytics = analytics_service
self.search = search_service
def create_task(self, title: str, owner_id: int, owner_email: str):
# שלב 1
task = self.task_service.create(title, owner_id)
# שלב 2
self.notify.send_creation_email(owner_email, task)
# שלב 3
self.analytics.track_task_created(task)
# שלב 4
self.search.index(task)
return task
יתרון: קל לראות את הזרימה.
חסרון: ה-orchestrator הוא נקודת כשל ותלות מרכזית.
בפועל, מערכות רבות משלבות את שניהם.
Saga Pattern - דפוס הסאגה¶
הבעיה: בעסקאות מבוזרות, מה קורה אם שלב אחד נכשל?
דוגמה: הזמנת טיסה + מלון + רכב שכור. אם הזמנת הרכב נכשלת - צריך לבטל גם את הטיסה והמלון.
Saga היא רצף של transactions מקומיות, כאשר כל שלב שנכשל מפעיל "compensating transaction" שמבטל את הפעולות הקודמות.
# דוגמה: יצירת משימה עם notifications ו-billing
async def create_task_saga(title: str, owner_id: int):
task = None
notification_sent = False
try:
# שלב 1: יצירת משימה
task = task_service.create(title, owner_id)
# שלב 2: שליחת notification
notify_service.send(task)
notification_sent = True
# שלב 3: חיוב (נניח שזה API חיצוני)
billing_service.charge_for_task(owner_id)
except BillingError:
# Compensating transactions
if notification_sent:
notify_service.send_cancellation(task)
if task:
task_service.delete(task["id"])
raise
מתי להשתמש ב-EDA?¶
כן:
- כשרוצים decoupling חזק בין שירותים
- כשפעולה אחת צריכה להפעיל כמה תהליכים
- כשתהליכים יכולים לרוץ באופן א-סינכרוני
- כשצריכים audit trail של כל מה שקרה
לא:
- כשצריך תשובה מיידית (synchronous response)
- כשהמערכת קטנה ופשוטה - overhead לא שווה
- כשה-team לא מוכן לנהל message broker