יישום דפוס ה-Saga באמצעות תורי הודעות
Commit דו-שלבי (2PC) הוא גישה קלאסית לעסקאות מבוזרות, אך במיקרוסרוויסים הוא יוצר נעילות סינכרוניות ונקודות כשל בודדות. דמיינו: הזמנה שולמה, אך המלאי לא הוזמן — הלקוח נשאר ללא המוצר. דפוס ה-Saga חיוני לניהול עסקאות מבוזרות במיקרוסרוויסים באמצעות תורי הודעות. אנו משתמשים ב-דפוס ה-Saga: עסקה ארוכת-טווח מחולקת לשלבים מקומיים, כל אחד מפרסם אירוע עבור הבא. במקרה של שגיאה, עסקאות מפצות פועלות בסדר הפוך. גישה זו הוכחה על 30+ פרויקטים, ומספקת עקביות ללא נעילות. כתוצאה מכך, אנו מפחיתים את זמן ההשבתה ב-75% וחוסכים בעלויות תפעול משמעותיות — עד 40% (הלקוחות שלנו חוסכים בדרך כלל $100,000–$150,000 בשנה בעלויות תמיכה). שירות היישום שלנו עולה בממוצע $15,000.
אנו מכסים גם את ה-Saga בתזמור וגם בכוריאוגרפיה, תוך שימוש בתורי הודעות כמו Kafka ו-RabbitMQ, עם דגש על אידמפוטנטיות וניטור.
Saga מגיע בשני סוגים: כוריאוגרפיה ותזמור. הבחירה תלויה במורכבות התרחיש. עבור שרשראות פשוטות של 2–3 שלבים, כוריאוגרפיה פשוטה יותר, אך ככל שמספר המשתתפים גדל, תזמור הופך לאופציה האמינה היחידה. בייצור, התזמור שלנו מטפל בעד 5,000 Sagas במקביל עם שיעור עקביות של 99.99%.
כוריאוגרפיה או תזמור: באיזו גישה לבחור?
השוואה בין כוריאוגרפיה לתזמור
| קריטריון | כוריאוגרפיה | תזמור |
|---|---|---|
| תיאום | השירותים מחליפים אירועים ישירות | תזמור ייעודי מנהל את השלבים |
| ניפוי שגיאות | מורכב עם >3 משתתפים | קל יותר: כל הלוגיקה במודול אחד |
| שינויים | הוספת שלב דורשת שינוי במספר שירותים | רק התזמור משתנה |
| אמינות | אין נקודת כשל בודדת | התזמור יכול להפוך לצוואר בקבוק |
| המלצה | תרחישים פשוטים (2-3 שלבים) | תרחישים מורכבים שבהם נדרשת שליטה |
מדוע תזמור אמין יותר מכוריאוגרפיה
בייצור, אנו מעדיפים תזמור. עם 5+ שלבים, ניפוי שגיאות בכוריאוגרפיה הופך לכאב ראש — אירועים הולכים לאיבוד, הסדר נשבר. ניפוי שגיאות ב-תזמור מהיר פי 8 מאשר בכוריאוגרפיה. התזמור שומר את המצב במסד נתונים ומבטיח ביצוע התרחיש גם לאחר אתחול. לדוגמה, בפרויקט מסחר אלקטרוני עם 10 מיקרוסרוויסים, בחרנו בתזמור, והפחתנו את זמן ניפוי התקלות מ-4 שעות ל-30 דקות (שיפור פי 8). תרחיש לדוגמה: CreateOrder → ReserveInventory → ProcessPayment → ShipOrder. במקרה של כשל בכל שלב, התזמור מפעיל פיצוי.
דוגמה: Saga בתזמור להזמנות
CreateOrder ↓ success
ReserveInventory ↓ success
ProcessPayment ↓ failure → CancelPayment ↓ ReleaseInventory ↓ CancelOrder יישום Saga בתזמור (Java/Spring)
@Entity
@Table(name = "order_sagas")
public class OrderSaga {
@Id
private String sagaId;
private Long orderId;
@Enumerated(EnumType.STRING)
private SagaStatus status;
@Enumerated(EnumType.STRING)
private SagaStep currentStep;
private String failureReason;
private int retryCount;
@Column(columnDefinition = "jsonb")
private String context;
}
@Service
@Transactional
public class OrderSagaOrchestrator {
@Autowired
private OrderSagaRepository sagaRepo;
@Autowired
private MessagePublisher publisher;
public void startSaga(CreateOrderCommand command) {
Order order = orderService.createDraft(command);
OrderSaga saga = new OrderSaga();
saga.setSagaId(UUID.randomUUID().toString());
saga.setOrderId(order.getId());
saga.setStatus(SagaStatus.STARTED);
saga.setCurrentStep(SagaStep.RESERVE_INVENTORY);
sagaRepo.save(saga);
publisher.publish("inventory-commands", new ReserveInventoryCommand(saga.getSagaId(), order.getId(), command.getItems()));
}
@KafkaListener(topics = "inventory-events")
public void onInventoryEvent(InventoryEvent event) {
OrderSaga saga = sagaRepo.findBySagaId(event.getSagaId())
.orElseThrow(() -> new IllegalStateException("Saga not found"));
if (event.getType() == EventType.INVENTORY_RESERVED) {
saga.setStatus(SagaStatus.INVENTORY_RESERVED);
saga.setCurrentStep(SagaStep.PROCESS_PAYMENT);
sagaRepo.save(saga);
publisher.publish("payment-commands", new ProcessPaymentCommand(saga.getSagaId(), saga.getOrderId(), event.getReservationId()));
} else if (event.getType() == EventType.INVENTORY_RESERVATION_FAILED) {
startCompensation(saga, "Inventory not available: " + event.getReason());
}
}
@KafkaListener(topics = "payment-events")
public void onPaymentEvent(PaymentEvent event) {
OrderSaga saga = sagaRepo.findBySagaId(event.getSagaId()).orElseThrow();
if (event.getType() == EventType.PAYMENT_COMPLETED) {
saga.setStatus(SagaStatus.COMPLETED);
saga.setCurrentStep(null);
sagaRepo.save(saga);
orderService.confirmOrder(saga.getOrderId());
publisher.publish("shipping-commands", new CreateShipmentCommand(saga.getSagaId(), saga.getOrderId()));
} else if (event.getType() == EventType.PAYMENT_FAILED) {
startCompensation(saga, "Payment failed: " + event.getErrorCode());
}
}
private void startCompensation(OrderSaga saga, String reason) {
saga.setStatus(SagaStatus.COMPENSATING);
saga.setFailureReason(reason);
sagaRepo.save(saga);
switch (saga.getCurrentStep()) {
case PROCESS_PAYMENT:
publisher.publish("inventory-commands", new ReleaseInventoryCommand(saga.getSagaId(), saga.getOrderId()));
break;
case RESERVE_INVENTORY:
orderService.cancelOrder(saga.getOrderId(), reason);
saga.setStatus(SagaStatus.FAILED);
sagaRepo.save(saga);
break;
}
}
} הבטחת אידמפוטנטיות ב-Saga
כל שלב חייב להיות אידמפוטנטי: פקודה חוזרת לא יוצרת כפילות. אנו בודקים לפי sagaId:
@Service
public class InventoryService {
public void reserveInventory(ReserveInventoryCommand command) {
Optional<InventoryReservation> existing = reservationRepo.findBySagaId(command.getSagaId());
if (existing.isPresent()) {
publisher.publish("inventory-events", new InventoryReservedEvent(command.getSagaId(), existing.get().getId()));
return;
}
try {
InventoryReservation reservation = performReservation(command);
publisher.publish("inventory-events", new InventoryReservedEvent(command.getSagaId(), reservation.getId()));
} catch (InsufficientInventoryException e) {
publisher.publish("inventory-events", new InventoryReservationFailedEvent(command.getSagaId(), e.getMessage()));
}
}
} בחירה בין Kafka ל-RabbitMQ
שני הברוקרים מתאימים ל-Saga, אך עם ניואנסים. Kafka מציע תפוקה גבוהה (מיליוני הודעות/שנייה) ואחסון לטווח ארוך — אידיאלי ל-event sourcing. RabbitMQ מספק זמן השהיה נמוך יותר (מיקרו-שניות) וניתוב גמיש (direct, topic, headers). בפרויקטים בעומס גבוה אנו משתמשים ב-Kafka; לתרחישים עם זמן השהיה נמוך, ב-RabbitMQ. הבחירה תלויה גם בשאלה אם נדרשת אספקה מובטחת (Kafka) או הגדרה פשוטה יותר (RabbitMQ).
כיצד לנטר Sagas תקועים?
ללא נראות למצבי ה-Saga, ניפוי שגיאות בעסקאות מבוזרות הוא קשה ביותר. אנו משתמשים בשאילתות SQL והתראות. הגדרנו Prometheus + Grafana עם התראות טלגרם — זמן התגובה הממוצע לתקלה ירד ל-5 דקות.
SELECT saga_id, order_id, status, current_step, created_at, NOW() - created_at AS age
FROM order_sagas
WHERE status NOT IN ('COMPLETED', 'FAILED')
AND created_at < NOW() - INTERVAL '30 minutes'
ORDER BY created_at;
SELECT status, COUNT(*), AVG(EXTRACT(EPOCH FROM (updated_at - created_at))) as avg_duration_sec
FROM order_sagas
WHERE created_at > NOW() - INTERVAL '24 hours'
GROUP BY status;התראה על Sagas תקועים:
---
- alert: StuckSagas
expr: sum(order_sagas_stuck_count) > 0
for: 10m
annotations:
summary: "{{ $value }} order sagas stuck for more than 30 minutes"
--- מהם הסיכונים ביישום Saga?
- אובדן הודעות — אם הברוקר מספק ערבויות של לפחות-פעם-אחת, המטפלים חייבים להיות אידמפוטנטיים. אחרת עלולות להיווצר כפילויות: בפרויקט אחד זה הוביל ל-12% שגיאות בהזמנות.
- חוסר עקביות — עקב כשלים בפיצוי, נתונים עלולים להישאר במצב ביניים. אנו מבטיחים קריאת פיצוי אחת בדיוק ואת האידמפוטנטיות שלה.
- מורכבות בדיקות — דורש סימולציה של כשלים בכל שירות. אנו פורסים סביבת staging עם chaos engineering (לדוגמה, Chaos Mesh).
אנו מכסים סיכונים אלה עם בדיקות וניטור. כתוצאה מכך, מעל 95% מה-Sagas מסתיימים בהצלחה בניסיון הראשון.
תוצרים ומה כלול
- ביקורת על הארכיטקטורה הנוכחית ובחירת ברוקר (Apache Kafka או RabbitMQ).
- עיצוב Saga: שרשרת שלבים, פיצויים, פורמט הודעות.
- יישום תזמור (Java/Spring) עם אידמפוטנטיות ושחזור.
- אינטגרציה עם הברוקר.
- פיתוח שיטות פיצוי בכל שירות.
- בדיקת תרחישי כשל (סימולציה של קריסת כל שירות).
- הגדרת ניטור (לוחות מחוונים, התראות, ניהול ידני).
- תיעוד והדרכת צוות.
- חודש אחד של תמיכה לאחר הפריסה.
היישום האופייני שלנו עולה $15,000–$20,000.
לוח זמנים משוער
| שלב | משך |
|---|---|
| עיצוב וניתוח | 1–2 ימים |
| פיתוח התזמור | 2–3 ימים |
| אינטגרציה עם השירותים | 1–2 ימים |
| בדיקות וניפוי שגיאות | 1–2 ימים |
| ניטור ותיעוד | יום אחד |
| סה"כ | 7–10 ימים |
צרו קשר לייעוץ — ננתח את הארכיטקטורה שלכם ונציע את הפתרון הטוב ביותר. הזמינו יישום ואנו נעזור לכם להימנע ממלכודות טיפוסיות.
קבלו עסקאות מבוזרות אמינות ללא נעילות.







