יישום תבנית Saga לעסקאות מבוזרות
לעתים קרובות אנו נתקלים במצבים שבהם עסקאות מבוזרות שוברות את עקביות הנתונים. במונולית עם מסד נתונים יחיד, עסקאות אטומיות אפשריות: או שהכול מתבצע או שכלום. בארכיטקטורת מיקרוסרוויסים, לכל שירות יש אחסון משלו, ופעולות אטומיות על פני מספר שירותים בלתי אפשריות. שני שירותים, שני מסדי נתונים, שתי קבוצות נפרדות של עסקאות — במקרה של כשל, הנתונים מתפצלים.
Saga היא דרך לנהל את המורכבות הזו. זוהי רצף של עסקאות מקומיות, שכל אחת מהן מפרסמת אירוע לשלב הבא. במקרה של שגיאה, עסקאות מפצות מבטלות שלבים שכבר הושלמו. מרטין פאולר מתאר את Saga כתבנית אמינה לשמירה על עקביות במיקרוסרוויסים. התשתית חייבת להיות בנויה כראוי; אחרת, Saga הופכת למקור לבאגים שקשה לתפוס עם אירועים שאבדו ומצב לא עקבי. העלות הממוצעת של השבתה במהלך כשל Saga היא 10,000 דולר לשעה, כך שיישום איכותי משתלם במהירות.
כיצד פועלת תבנית Saga?
Saga אינה פתרון קסם. ללא תשתית מתאימה, עסקאות מבוזרות הופכות ל"סיוטים מבוזרים". בפועל, כ-30% מה-Sagas מסתיימות בפיצוי, ו-latency של אחוזון 99 בשרשרת יכול להגיע ל-2.5 שניות. לאחר אופטימיזציה של התשתית, ה-latency יורד ל-0.8 שניות, ומספר הפיצויים ל-8%.
כוריאוגרפיה מול אורקסטרציה: הבחירה משפיעה על התשתית
- כוריאוגרפיה: כל שירות מפרסם אירועים ומגיב אליהם. אין מתאם מרכזי.
- אורקסטרציה: מתאם Saga מרכזי מנהל את התהליך, וקורא לכל שירות ישירות דרך HTTP/gRPC.
לכוריאוגרפיה, יש צורך בתיווך הודעות אמין עם הבטחת מסירה. לאורקסטרציה, יש צורך במצב מתמשך (מסד נתונים או Redis). בפועל, קל יותר לנפות באגים ולנטר אורקסטרציה — כל גרף הביצוע נראה לעין. Temporal מהירה פי 3 מ-Kafka בעת ניפוי באגים בתרחישי שחזור מכשל.
| פרמטר | כוריאוגרפיה | אורקסטרציה |
|---|---|---|
| מורכבות תשתית | נמוכה (רק broker) | בינונית (broker + אחסון מצב) |
| ניפוי באגים | קשה (לוגים מפוזרים) | קל (זרימת עבודה אחת) |
| ניטור | מדדים לכל שלב | מעקב מלא |
| אידמפוטנטיות | קריטית בכל מקום | מובנית חלקית |
Kafka כתחבורה לכוריאוגרפיה של Saga
Kafka עם acks=all ו-min.insync.replicas=2 מבטיחה: הודעה לא תיאבד לאחר אישור. תצורת topic טיפוסית:
תצורת KafkaTopic
---
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaTopic
metadata:
name: order-saga-events
spec:
partitions: 12
replicas: 3
config:
retention.ms: "604800000"
min.insync.replicas: "2"
cleanup.policy: compact
היצרן חייב לכלול apiVersion: kafka.strimzi.io/v1beta2 kind: KafkaTopic metadata: name: order-saga-events spec: partitions: 12 replicas: 3 config: retention.ms: "604800000" min.insync.replicas: "2" cleanup.policy: compact — Kafka מבטיחה כתיבה בדיוק פעם אחת בניסיון חוזר. זה קריטי עבור Saga, שבה אירועים כפולים מובילים לביצוע חוזר של שלבים.
כיצד ליישם אידמפוטנטיות של handler?
Kafka ב-at-least-once פירושו: אירוע עשוי להגיע פעמיים. כל handler ב-Saga חייב להיות אידמפוטנטי — ביצוע חוזר עם אותו מזהה אירוע לא משנה את המצב. תבנית: בדוק אם האירוע כבר עובד → בצע עסקה מקומית → פרסם את האירוע הבא.
async def handle_order_created(event: OrderCreatedEvent):
if await redis.get(f"processed:payment:{event.saga_id}"):
logger.info(f"Event {event.saga_id} already processed")
return
async with db.transaction():
payment = await payment_service.charge(
customer_id=event.customer_id,
amount=event.total_amount,
idempotency_key=event.saga_id
)
await db.execute(
"INSERT INTO saga_steps (saga_id, step, status) VALUES ($1, $2, $3)",
event.saga_id, "payment", "completed"
)
await redis.setex(f"processed:payment:{event.saga_id}", 86400, "1")
await kafka_producer.produce("payment.completed", PaymentCompletedEvent(...))אם שלב N נכשל, יש לבטל את השלבים 1..N-1. עסקאות מפצות חייבות להיות גם אידמפוטנטיות — השתמש ב-enable.idempotence=True עם קידומת refund-.
מדוע אורקסטרציה עדיפה?
Temporal היא מנוע זרימת עבודה עם מצב מתמשך, ניסיון חוזר אוטומטי, והיסטוריה מלאה של כל ביצוע. כאשר עובד קורס, התהליך מתחדש מנקודת העצירה. תשתית: Temporal Server (Docker או Kubernetes), PostgreSQL להתמדה.
@workflow.defn
class OrderSagaWorkflow:
@workflow.run
async def run(self, order: OrderRequest) -> OrderResult:
try:
payment = await workflow.execute_activity(
process_payment,
order.payment_details,
start_to_close_timeout=timedelta(seconds=30))
except ActivityError:
raise
try:
reservation = await workflow.execute_activity(
reserve_inventory,
order.items,
start_to_close_timeout=timedelta(seconds=30))
except ActivityError:
await workflow.execute_activity(refund_payment, payment.payment_id)
raise
try:
shipment = await workflow.execute_activity(
create_shipment,
order,
start_to_close_timeout=timedelta(seconds=60))
except ActivityError:
await workflow.execute_activity(cancel_reservation, reservation.id)
await workflow.execute_activity(refund_payment, payment.payment_id)
raise
return OrderResult(order_id=order.id, payment_id=payment.payment_id, shipment_id=shipment.id) ניטור Saga: מה לעקוב
| מדד | משמעות |
|---|---|
| שיעור השלמת Saga | אחוז ה-Sagas שהושלמו בהצלחה |
| שיעור פיצויים | באיזו תדירות מופעלים פיצויים |
| Latency של שלב p99 | היכן נמצא הצוואר בקבוק בשרשרת |
| גודל תור הודעות מתות | כמה אירועים לא ניתן היה לעבד |
| מספר Sagas פעילים | כמה Sagas מבוצעות כעת |
DLQ (תור הודעות מתות) הוא קריטי. אירועים שלא עובדו לאחר 3 ניסיונות חוזרים עוברים ל-DLQ. יש לטפל בהם אוטומטית או ידנית.
כיצד ליישם את תבנית Saga: מדריך שלב אחר שלב
- ביקורת ארכיטקטורה: זהה נקודות כשל במיקרוסרוויסים הנוכחיים.
- עיצוב Saga: תאר שלבים, פיצויים, סכמת אירועים.
- פריסת תשתית: הגדר אשכול Kafka/Temporal, topics, התמדה.
- יישום handler: יישם אידמפוטנטיות, עסקאות מפצות, DLQ.
- ניטור והתראות: הגדר לוחות מחוונים של Prometheus/Grafana, התראות על פיצויים ו-DLQ.
מה כלול בעבודה שלנו
- ביקורת ארכיטקטורה: ניתוח מיקרוסרוויסים נוכחיים ונקודות כשל.
- עיצוב Saga: הגדרת שלבים, פיצויים, סכמת אירועים.
- פריסת תשתית: אשכול Kafka/Temporal, תצורת topic, הגדרת התמדה.
- יישום handler: אידמפוטנטיות, עסקאות מפצות, DLQ.
- ניטור והתראות: לוחות מחוונים של Prometheus/Grafana, התראות על פיצויים ו-DLQ.
- תיעוד והדרכה: runbook, תבניות postmortem, הדרכת צוות.
תהליך היישום
| שלב | משך |
|---|---|
| עיצוב: שלבי saga, פיצויים, סכמת אירועים | יום אחד |
| תשתית: הגדרת Kafka/Temporal | יום אחד |
| יישום handler עם אידמפוטנטיות | 1.5 ימים |
| DLQ + ניטור + התראות | 0.5 יום |
| בדיקת תרחישי כשל | יום אחד |
העלות מחושבת באופן אישי לאחר ביקורת ארכיטקטורת מיקרוסרוויסים. צור קשר להערכה של הפרויקט שלך.
הזמן יישום Saga במפתח — קבל מערכת מבוזרת יציבה.







