Sagas: Điều phối workflow phân tán bằng bù trừ
Suy luận về Saga như chuỗi local transaction bền vững với bù trừ ngữ nghĩa, pivot, retry, các kiểu điều phối, kiểm soát trạng thái trung gian và recovery.
Bản đồ học tập phát triển phần mềm bởi Tran Trong Thuc · Về dự án Atlas · Cập nhật lần cuối: 22 thg 9, 2026
Sagas: Điều phối workflow phân tán bằng bù trừ
Bạn đang xây dựng luồng đặt combo du lịch: dịch vụ chuyến bay đã trừ tiền trên thẻ tín dụng và xuất vé máy bay thành công, nhưng dịch vụ phòng khách sạn ngay sau đó lại báo lỗi vì hết phòng vào phút chót. Trong kiến trúc nguyên khối với một cơ sở dữ liệu duy nhất, một lệnh rollback ACID sẽ âm thầm xóa sạch mọi thay đổi để đưa dữ liệu về vạch xuất phát ban đầu. Nhưng trong thế giới microservices với các database độc lập và cổng thanh toán bên thứ ba, giao thức Two-Phase Commit (2PC) vừa nặng nề, vừa dễ bế tắc, và bạn cũng không thể ép dòng thời gian quay ngược để "xóa sổ" một khoản tiền đã trừ trên thẻ khách. Làm thế nào để xử lý chuỗi thao tác dở dang này một cách an toàn? Câu trả lời nằm ở mô hình Saga và cơ chế bồi hoàn ngữ nghĩa (semantic compensation).
Saga là một chuỗi local transaction cùng nhau triển khai một business workflow dài hạn qua các service hoặc data store có quyền sở hữu độc lập. Mỗi bước commit cục bộ trên database của mình, và khi có sự cố, workflow sẽ kích hoạt các hành động bồi hoàn tương ứng.
💡 Quy tắc bỏ túi: Hệ thống phân tán không thể quay ngược thời gian bằng ACID rollback. Hãy cấu trúc workflow quanh một điểm pivot rõ ràng: bồi hoàn lùi (backward compensation) trước điểm không quay lại, và kiên trì retry tiến về phía trước (forward recovery) sau khi điểm pivot đã hoàn tất.
Tóm tắt
- Saga thay thế 2PC phân tán bằng một chuỗi local transaction: Thay vì giữ các distributed lock qua mạng nhiều giây hoặc nhiều phút gây tắc nghẽn, mỗi service tự commit transaction cục bộ trên database của mình, để lộ trạng thái trung gian nghiệp vụ có chủ đích và đẩy workflow tiến lên.
- Bồi hoàn là đảo ngược ngữ nghĩa, không phải database rollback: Giao dịch bồi hoàn (compensating transaction) không thể xóa bỏ lịch sử; chúng là các transaction mới đã commit (như hoàn lại tiền, giải phóng giữ chỗ tồn kho) nhằm vô hiệu hóa về mặt nghiệp vụ cho các bước đã hoàn thành trước đó theo chiều ngược lại.
- Bước Pivot là ranh giới giữa bồi hoàn lùi và phục hồi tiến: Trước bước giao dịch pivot (điểm không quay lại), lỗi sẽ kích hoạt bồi hoàn lùi; một khi bước pivot đã commit thành công, workflow bắt buộc phải bảo đảm phục hồi tiến bằng các bước thử lại bất biến (idempotent retry) và cảnh báo vận hành.
- Lựa chọn điều phối dựa trên độ phức tạp của luồng nghiệp vụ: Điều phối tập trung (Orchestration) dùng một coordinator quản lý tường minh state machine và timeout; Phối hợp phi tập trung (Choreography) dựa trên các domain event bắn qua lại giữa các service, giảm phụ thuộc trung tâm nhưng khó truy vết luồng chạy khi hệ thống lớn dần.
- Cạm bẫy chết người: Xem timeout qua mạng như một lỗi thất bại tuyệt đối rồi vội vã kích hoạt bù trừ. Nếu lệnh thanh toán bị timeout do rớt gói tin nhưng phía ngân hàng thực tế đã trừ tiền thành công, việc gửi ngay lệnh hoàn tiền mà không đối soát trạng thái sẽ dẫn đến thảm họa lệch sổ sách hoặc hoàn tiền kép.
Local commit làm bài toán thay đổi
Một database transaction thông thường có thể giữ nhiều write nguyên tử vì một transaction manager kiểm soát dữ liệu liên quan. Workflow xuyên service thường không có cùng boundary đó nếu không dùng giao thức distributed transaction như 2PC.
Saga chủ động thu hẹp atomicity. Mỗi participant commit local transaction của mình, giải phóng lock cục bộ rồi truyền ý định tiếp theo. Cách này phù hợp với long-running workflow kéo dài vài giây, vài phút, vài giờ hoặc lâu hơn mà không giữ một global transaction mở suốt thời gian đó.
Đổi lại, trạng thái trung gian trở nên quan sát được. Sau khi inventory đã reserve nhưng trước khi payment được authorize, actor khác có thể thấy hệ thống không còn ở trạng thái ban đầu nhưng cũng chưa đạt trạng thái cuối. Vì vậy thiết kế Saga cần các business state tường minh như PENDING_PAYMENT, RESERVED, COMPENSATING và COMPLETED thay vì giả định workflow vô hình cho tới cuối.
Bù trừ là semantic undo, không phải rollback
Database rollback xóa phần việc chưa commit bên trong một transaction. Saga compensation là một transaction mới đã commit có ý nghĩa nghiệp vụ đối nghịch hoặc bù lại một bước đã commit trước đó.
Ví dụ:
| Hành động tiến | Bù trừ có thể dùng | Điều không thể xóa khỏi lịch sử |
|---|---|---|
| Reserve inventory | Release reservation | Reservation đã từng tồn tại và có thể ảnh hưởng availability |
| Authorize payment | Void authorization | Request cùng audit history ở provider |
| Capture payment | Refund payment | Phí, timing, notification, settlement history |
| Gửi email | Gửi email đính chính | Người nhận có thể đã đọc email đầu |
Compensation nên idempotent khi có thể. Replay ReleaseReservation(sagaId, reservationId) không được giải phóng hai lần. Compensation cũng cần trạng thái bền vững riêng; “đã thử refund” không đồng nghĩa với “refund đã thành công”.
Phân loại bước: compensable, pivot và retryable
Nhiều thiết kế Saga thực tế trở nên dễ suy luận hơn khi chia các bước quanh một pivot.
Trước pivot, các bước đã hoàn thành cần có compensation path đáng tin. Pivot là điểm workflow đã cam kết với một kết quả nghiệp vụ không nên bị hủy bỏ tùy tiện nữa. Sau pivot, các bước phía sau thường nên được thiết kế retryable tới khi hoàn tất thay vì tiếp tục cố unwind kết quả đã cam kết.
Pivot là quyết định nghiệp vụ, không phải feature của framework. Ở workflow này nó có thể là “capture payment”; ở workflow khác có thể là “phát hành entitlement không hoàn tiền” hoặc “bàn kiện hàng cho carrier bên ngoài”.
Unknown outcome phải được xử lý trước compensation
Distributed call có thể timeout sau khi phía remote đã commit. Giả sử Payment service nhận AuthorizePayment(op-42), commit authorization nhưng response bị mất. Saga chỉ nhìn thấy timeout.
Bù trừ ngay lúc đó là không an toàn vì coordinator chưa biết có gì để bù trừ hay không. Trình tự an toàn thường là:
- Giữ bước ở trạng thái
UNKNOWNhoặcAWAITING_CONFIRMATION. - Retry hoặc query bằng cùng stable operation ID.
- Reconcile kết quả ở remote.
- Chỉ sau đó mới quyết định tiếp tục tiến hay bù trừ.
Đây chính là quy tắc partial failure khiến idempotency trở nên bắt buộc. Timeout là bằng chứng rằng kết quả không chắc chắn, không phải bằng chứng operation đã thất bại.
Choreography và orchestration là hai kiểu điều phối
Saga semantics không bắt buộc một topology duy nhất. Hai kiểu phổ biến là choreography và orchestration.
Choreography để các participant phản ứng với event rồi phát event mới. Nó loại bỏ central coordinator khỏi happy path, nhưng workflow graph dễ trở nên ẩn trong subscription. Khi số participant tăng, cycle, hidden dependency, ownership của compensation và end-to-end debugging trở nên khó hơn.
Orchestration dùng một coordinator bền vững có tri thức tường minh về workflow state machine. Participant nhận command và trả outcome. Control flow dễ quan sát, thay đổi và recovery hơn, nhưng orchestrator trở thành production component quan trọng cần được thiết kế kỹ về state, availability, versioning và recovery.
Chọn theo độ phức tạp của workflow và ownership. “Không có central service” không tự động đồng nghĩa với loose coupling nếu mỗi participant vẫn phải biết event nào kích hoạt participant nào khác.
Lưu trạng thái workflow, không chỉ lưu message
Một Saga production cần durable state model. Tối thiểu nên lưu:
- Saga ID hoặc workflow ID ổn định;
- trạng thái workflow hiện tại và trạng thái từng bước đã hoàn thành;
- command/event identity và correlation ID hay mã tương quan;
- số lần thử, timestamp và metadata timeout/deadline;
- trạng thái compensation tách riêng trạng thái forward step;
- đủ input hoặc reference để resume tất định sau restart.
Một promise chain chỉ tồn tại trong process không phải Saga bền vững. Nếu coordinator crash sau khi payment thành công, process thay thế phải tái dựng được chuyện gì đã xảy ra và bước nào tiếp theo là an toàn.
Messaging durability là boundary riêng. Transactional outbox có thể atomically lưu local state change của participant cùng command/event đẩy Saga tiến tiếp. Inbox hoặc processed-message record giúp consumer duplicate-safe. Outbox giải reliable publication; Saga giải multi-step business coordination. Hai pattern thường đi cùng nhau nhưng không thay thế nhau.
Isolation vẫn cần thiết kế nghiệp vụ
Saga không cung cấp isolation cho toàn workflow. Request khác có thể đọc hoặc sửa dữ liệu trong khi Saga chưa kết thúc.
Các countermeasure phổ biến gồm:
- state
PENDINGhoặcRESERVEDtường minh để chặn transition không hợp lệ; - reservation có expiry thay vì chiếm tài nguyên vĩnh viễn;
- semantic lock như “order đang được hủy” thay vì giữ database lock nhiều phút;
- version check hoặc optimistic concurrency trước khi áp bước tiếp theo;
- dùng operation có tính giao hoán khi phù hợp;
- business rule chấp nhận trạng thái trung gian đã biết rồi reconcile sau.
Câu hỏi đúng không phải “làm sao che mọi trạng thái trung gian?” mà là “invariant nào phải luôn đúng trong lúc workflow chưa hoàn tất?”.
Compensation cũng có thể lỗi
Backward recovery bản thân nó cũng là distributed workflow. Refund API có thể timeout. Inventory release có thể gặp database failure tạm thời. Participant có thể unavailable nhiều giờ.
Vì vậy compensation cần cùng kỷ luật kỹ thuật như forward work: stable identity, idempotency, bounded retry, trạng thái bền vững, backoff, dead-letter/quarantine cho deterministic failure và reconciliation có thể nhìn thấy bởi operator.
Không được mark Saga là COMPENSATED chỉ vì compensation đã được schedule. Chỉ kết luận khi các outcome bù trừ bắt buộc đã được xác nhận.
Production micro-scenario: fulfillment lỗi sau payment
Checkout tạo order o-731 dưới Saga saga-8841.
- Inventory reserve đơn vị hàng cuối cùng với reservation
r-91. - Payment authorize
$149bằng idempotency keysaga-8841:authorize. - Fulfillment từ chối địa chỉ vì carrier không phục vụ khu vực đó.
- Saga chuyển sang bù trừ: void payment rồi release inventory.
- Call void payment timeout sau khi provider có thể đã chấp nhận request.
Hậu quả: Đơn hàng mắc kẹt vô thời hạn ở trạng thái COMPENSATING. Kho hàng không thể mở bán lại sản phẩm, đội ngũ hỗ trợ thấy tiền vẫn bị phong tỏa trên cổng thanh toán, còn thao tác retry ngây thơ có nguy cơ gửi lệnh hoàn tiền lần hai hoặc giải phóng tài nguyên khi chưa rõ kết quả trừ tiền thực tế.
Nguyên nhân cốt lõi: Workflow ngộ nhận cơ chế bù trừ (compensation) giống như một lệnh rollback đồng bộ của database và vội vã kết luận timeout đồng nghĩa với thất bại. Hệ thống không lưu trạng thái bồi hoàn bền vững và không tái sử dụng mã định danh thao tác ổn định (idempotency key) để đối soát.
Cách khắc phục chuẩn: Lưu bền vững toàn bộ tiến trình Saga cùng kết quả của từng bước. Giữ bước bồi hoàn thanh toán ở trạng thái chưa rõ kết quả (unknown), truy vấn đối soát hoặc thử lại bằng chính idempotency key đó cho đến khi có phản hồi chắc chắn từ phía cổng thanh toán, rồi mới tiến hành giải phóng tồn kho theo đúng thứ tự bồi hoàn đã định. Thiết lập cảnh báo theo tuổi thọ Saga (Saga age) và tỷ lệ lỗi bồi hoàn để chuyển các ca ngoại lệ tới kỹ sư vận hành kịp thời.
Xem giải thích chi tiết
Cả bước thanh toán tiến và bước bồi hoàn lùi đều phải đi qua cùng một ranh giới mạng không đáng tin cậy. Cả hai đều có thể đã commit thành công ở phía remote trong khi response phản hồi bị rơi rụng trên đường truyền. Vì vậy mô hình an toàn phải có tính đối xứng: timeout tạo ra sự không chắc chắn (uncertainty), mã định danh ổn định (stable identity) giúp retry và đối soát an toàn, và trạng thái workflow bền vững ngăn việc tiến trình crash làm xóa sạch những việc còn dang dở.
Observability phải trả lời được “Saga nào đang bị kẹt?”
Telemetry hữu ích cho Saga gồm:
- số Saga theo từng state;
- tuổi của Saga chưa terminal lâu nhất;
- per-step latency và retry rate;
- compensation rate cùng compensation failure count;
- thời gian nằm trong
UNKNOWNhoặc reconciliation state; - số transition sang manual intervention hoặc quarantine;
- correlation từ Saga ID tới participant log, message và trace.
Global error rate đơn thuần là chưa đủ. Mười workflow bị kẹt ba giờ có thể cấp bách hơn hàng nghìn workflow hoàn tất bình thường với retry lẻ tẻ.
Saga so với các pattern lân cận
Saga so với 2PC/distributed transaction. 2PC điều phối một commit decision chung giữa các transactional resource tham gia. Saga cho phép từng local transaction commit độc lập rồi recovery bằng forward progress hoặc bù trừ. Chọn theo atomicity thật sự cần và loại hệ thống tham gia, không theo độ nổi tiếng của pattern.
Saga so với transactional outbox. Outbox đóng local dual-write gap giữa database và message. Nó không định nghĩa multi-step business workflow, thứ tự compensation hay pivot. Saga thường dùng outbox ở participant boundary.
Saga so với workflow engine. Workflow engine có thể cung cấp durable timer, retry, state persistence và primitive cho orchestration. Những capability đó giúp triển khai Saga, nhưng business semantics của compensation, pivot và invariant vẫn thuộc application design.
Khi nào không nên dùng Saga
Không nên đưa Saga vào khi một local ACID transaction đã sở hữu đầy đủ invariant. Saga sẽ thêm state, retry, compensation logic và operational work không cần thiết.
Hãy thận trọng khi operation không có compensation đáng tin và cũng không thể làm retryable an toàn sau pivot. Khi đó kiến trúc có thể cần boundary ownership khác, reservation/preauthorization model, coordination chặt hơn hoặc human approval tường minh trước bước không thể đảo ngược.
Checklist review theo failure
- Boundary: Workflow có thật sự đi qua nhiều resource commit độc lập, hay một local transaction sẽ đơn giản hơn?
- State: Saga ID và từng forward/compensation step có được lưu bền vững không?
- Unknown outcome: Timeout có đưa workflow vào reconciliation thay vì bị coi là failure chắc chắn không?
- Idempotency: Mọi forward step có retry và compensation có replay an toàn dưới stable operation ID không?
- Compensation: Mỗi bước compensable có inverse hợp lệ về ngữ nghĩa chưa, kể cả side effect không thể xóa khỏi lịch sử?
- Pivot: Điểm không quay lại có tường minh không, và sau đó có ưu tiên forward recovery không?
- Isolation: Trạng thái trung gian có được bảo vệ bằng reservation, semantic lock, version check hoặc business invariant không?
- Delivery: Local state change và Saga message có được nối an toàn, ví dụ bằng outbox/inbox boundary không?
- Observability: Operator có tìm nhanh Saga bị kẹt, unknown state quá lâu và compensation lỗi không?
- Manual recovery: Có reconciliation path được tài liệu hóa cho case automation không giải quyết được không?
Quy tắc cho agent
- Không mô tả Saga compensation như thể đó là database rollback.
- Persist workflow progress trước khi phụ thuộc process khác tiếp tục nó.
- Xem timeout là unknown outcome và reconcile bằng stable identity trước khi bù trừ.
- Làm forward retry và compensation idempotent ở mọi nơi downstream cho phép.
- Làm pivot cùng irreversible side effect tường minh trong workflow design.
- Không giả định Saga cung cấp isolation; hãy thiết kế valid intermediate state có chủ đích.
- Dùng outbox/inbox để bảo vệ message boundary nhưng không nhầm chúng với Saga semantics.
- Alert theo tuổi Saga và compensation failure, không chỉ request-level error rate.
Tài liệu chính
- Hector Garcia-Molina và Kenneth Salem, Sagas, ACM SIGMOD 1987: https://dl.acm.org/doi/10.1145/38714.38742
- AWS Prescriptive Guidance, Saga patterns: https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-patterns.html
- Microsoft Azure Architecture Center, Saga distributed transactions pattern: https://learn.microsoft.com/en-us/azure/architecture/patterns/saga
Transactional Outbox: Phát sự kiện mà không tạo khoảng trống dual-write
Vận hành transactional outbox bằng cách commit trạng thái nghiệp vụ và ý định phát sự kiện một cách nguyên tử, rồi relay với duplicate-safe delivery, ordering, recovery và observability.
Cloud Networking: Lần theo khả năng kết nối qua Route, NAT và Policy
Vận hành cloud networking bằng cách lần theo đường đi của gói tin qua CIDR, subnet, route, gateway, NAT, network policy, DNS, private endpoint và các mạng kết nối.