למה מערכות גדולות לא מדברות ישירות זו עם זו
AppsFlyer, חברת ה-attribution הישראלית, מעבדת 80 מיליארד events ביום. אם כל service הייתה קוראת ישירות לservice אחרת, API call אחד לשרת עמוס, timeout, קריסה, כל השרשרת הייתה נופלת. הפתרון שנבחר: Kafka. במקום API calls ישירים, כל service מפרסמת event לtopic, וכל מי שמעניין לו, קורא בקצב שלו, באופן עצמאי.
זה הרעיון של Event-Driven Architecture: decoupling. Service A לא יודעת שService B קיימת. היא רק מפרסמת "קרה משהו". Service B מקשיבה ומגיבה. אפשר להוסיף Service C מחר, בלי לשנות שורת קוד ב-A.
Kafka, RabbitMQ, SQS, ההבדלים שחשובים בפועל
| קריטריון | Kafka | RabbitMQ | AWS SQS |
|---|---|---|---|
| מודל | Log-based, pull | Queue-based, push | Queue-based, pull |
| Retention | ימים/שבועות, replay | עד שנצרך | עד 14 ימים |
| Throughput | מיליוני events/שנייה | עשרות אלפי msg/שנייה | מאות אלפי msg/שנייה |
| Routing | Topics + partitions | Exchanges מתוחכמים | SNS fanout נדרש |
| Multi-consumer | כל Consumer Group קורא הכל | Exchange + bindings | SNS + SQS |
| הכי מתאים ל | Event sourcing, analytics, replay | Background jobs, complex routing | Serverless, AWS stack פשוט |
עדכון חשוב, Kafka 4.0 (מרץ 2025): ZooKeeper הוסר לחלוטין. מי שמשתמש ב-Kafka 3.x עם ZooKeeper חייב לעבור ל-KRaft לפני שידרגו ל-4.0. ב-KRaft, recovery time מהיר פי 10. בנוסף, KIP-932 (Early Access) מביא Queue semantics ישירות לKafka, הבדל המסורתי בינה לRabbitMQ מיטשטש.
Consumer Groups, המנגנון שמאפשר scale ו-isolation
Consumer Group הוא אחד המושגים שהכי חשוב להבין ב-Kafka. הגדרה פשוטה: קבוצת consumers שחולקת ביניהם את ה-partitions. כל partition נצרכת בדיוק על-ידי consumer אחד בתוך ה-group. זה אומר שתי דברים:
- Scale-out: רוצים לעבד מהר יותר? מוסיפים consumers לgroup. עד מספר ה-partitions, תוספת לינארית.
- Isolation בין services: notification-service ו-fraud-detection הם groups שונות. אם fraud-detection לאגר, notification-service ממשיכה לרוץ בלי שום הפרעה. זה ה-decoupling בפועל.
דוגמה ישראלית: Wolt. כשorder מתקבל, topic אחד OrderPlaced מוזן על-ידי producer אחד. Consumer groups מקבילות: notifications-service (SMS ללקוח), dispatch-service (ניתוב לרידר), analytics-service (דשבורד real-time), fraud-service (בדיקת חריגות). כל אחת עצמאית, כל אחת בקצב שלה.
Delivery Semantics, כמה פעמים message אחת תגיע?
זו הבחירה שחייבים לקבל בכל system event-driven:
- At-most-once: message נשלחת פעם אחת לכל היותר. אם נכשלת, לא נשלחת שוב. אפשרי לאבד messages. מתאים ל-metrics שאין בעיה לאבד.
- At-least-once: message מובטחת להגיע, אבל אולי פעמיים. Consumer restart, network retry, גורמים לduplication. זה ה-default ב-Kafka ובSQS.
- Exactly-once: מורכב מאוד. Kafka תומך דרך Transactional Producers + Idempotent Consumers. AWS SQS FIFO קרוב לזה.
ב-payment processing, at-least-once פירושו סכנה של חיוב כפול. הפתרון: idempotency. כל event נושאת idempotency key (למשל orderId+paymentAttempt). Consumer בודק לפני עיבוד. הטריק: אל תשתמשו ב-SELECT ואז INSERT, זו race condition. השתמשו ב-INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT DO NOTHING, atomic, thread-safe.
הבעיות שפוגשים בproduction
Poison Pill Messages, message שגורמת ל-consumer לקרוש בכל פעם שמנסים לעבד אותה. בלי Dead Letter Queue (DLQ), consumer נתקע לנצח. כל message אחריה חסומה. DLQ לוקח messages שנכשלו N פעמים ומסיר אותן מה-main queue לqueue נפרדת לחקירה. זה לא optional, זה חובה בכל production system.
Ordering Assumptions לא נכונות, Kafka מבטיח ordering בתוך partition בלבד. אם צריך ordering של events לאותו user (OrderPlaced לפני PaymentProcessed לפני OrderShipped), כל events של אותו userId חייבים לנותב לאותה partition. partition key = userId. בלי זה, PaymentProcessed יכול להגיע לפני OrderPlaced.
Consumer Lag Monitoring חסר, Consumer lag הוא הפרש בין ה-offset האחרון ב-topic לoffset שה-consumer צרך. כשlag גדל, consumers לא מדביקים את ה-producers. זה ה-metric הקריטי. system שנראה בריא אבל עם consumer lag גדל הוא כבר בבעיה. הגדירו alert ב-Prometheus/Grafana על consumer lag מעל סף מוגדר.
Outbox Pattern, הפתרון לבעיה שכולם עוברים
בעיה קלאסית: שמרתם record ב-PostgreSQL, ניסיתם לשלוח event לKafka, הרשת נפלה. עכשיו ה-DB מעודכן אבל הevent לא יצאה. dual-write problem.
Outbox Pattern פותר זאת: כתבו ל-business table ול-outbox table באותה DB transaction. Process נפרד (Debezium CDC) קורא מה-outbox ושולח לKafka. אם Kafka לא זמינה, events מחכות ב-outbox. עם שתי ה-writes ב-transaction אחת, אי אפשר לקבל מצב שה-DB מעודכן בלי event, או event בלי DB update.
Debezium 2.5+ (2025) הוא ה-industry standard לconfiguration כזו. אבל שימו לב: "Stop overusing the outbox pattern", מוסיף complexity. אם ה-system שלכם לא מגיע ל-scale שבו dual-write בעיה אמיתית, אולי לא שווה את ה-overhead.
איך Claude עוזר לתכנן event-driven systems
Claude מצטיין בשאלות ארכיטקטורה שיש להן trade-offs. כשהוא מסביר שאלת Kafka, הוא לא רק נותן תשובה, הוא מסביר למה ומה ה-alternatives. פרומפטים שעובדים:
- "Design a [system] with [N] event types and [M] consumer services. Specify topics, partition keys, consumer groups, and retention. What's the failure mode if [service X] is slow?"
- "I need idempotency for payment processing with Kafka at-least-once delivery. Write PostgreSQL schema and consumer code with INSERT ON CONFLICT."
- "My service writes to PostgreSQL and publishes to Kafka. Network sometimes fails. Explain Transactional Outbox Pattern and write the schema and Debezium config."
- "Consumer lag on my billing-service grew from 0 to 50K in 2 hours. List 5 metrics to check and what each means."
נקודות מפתח לסיכום
- Event-driven architecture = decoupling. Services לא מכירות זו את זו, רק את ה-topics.
- Consumer Groups הן הכלי לscale-out ול-isolation בו-זמנית. כל service = consumer group עצמאית.
- At-least-once delivery (ה-default) דורש idempotency בצד ה-consumer. בלי זה, duplicate processing.
- Dead Letter Queue הוא לא optional. Poison pill messages יחסמו production בלעדיו.
- Partition key חייב להתאים ל-ordering requirements. events לאותו entity = אותה partition.
- Kafka 4.0 (מרץ 2025): ZooKeeper gone, KRaft only. מי שבונה היום, מתחיל ישר עם KRaft.
- Outbox Pattern הוא הפתרון לdual-write, אבל רק כשצריך אותו. אל תוסיפו complexity ללא סיבה.
