לדלג לתוכן

3.3 ארכיטקטורה מונעת אירועים הרצאה

ארכיטקטורה מונעת אירועים - Event-Driven Architecture

בקורס צד השרת למדנו על Kafka ותורי הודעות. עכשיו נסתכל על הנושא מזווית ארכיטקטונית - ארכיטקטורה מונעת אירועים (EDA) היא סגנון עיצוב שלם, לא רק כלי טכני.


בקשה לעומת אירוע

ישנם שני דפוסי תקשורת עיקריים:

תקשורת מבוססת בקשה - Request-Driven

Service A  →  "תעשה X"  →  Service B
Service A  ←  "בוצע X"  ←  Service B

A שולח בקשה ומחכה לתשובה. A יודע ש-B קיים. A תלוי ב-B.

תקשורת מבוססת אירועים - Event-Driven

Service A  →  "X קרה"  →  Event Bus
                             |
                    +--------+--------+
                    v        v        v
                Service B  Service C  Service D

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