Partial Failure: Suy luận khi kết quả không chắc chắn
Suy luận về thao tác phân tán khi một component, message hoặc observer lỗi trong khi phần còn lại của hệ thống vẫn tiếp tục chạy.
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: 5 thg 11, 2026
Partial Failure: Suy luận khi kết quả không chắc chắn
Tóm tắt nhanh
Tình huống minh họa (giả định): Máy POS báo timeout khi thu tiền cà phê nhưng người mua đã nhận thông báo ghi nợ. Hai bên chỉ có quan sát cục bộ; timeout không chứng minh giao dịch chưa thực hiện và thông báo ghi nợ chưa đủ để khẳng định mọi bước đã hoàn tất. Trước khi gửi yêu cầu mới, ứng dụng phải đối soát với hệ thống có thẩm quyền.
Partial failure là tình huống một thành phần có thể hoàn tất thao tác nhưng thành phần khác không nhận được kết quả. Lỗi truyền thông không phải phán quyết về trạng thái đã commit.
💡 Quy tắc bỏ túi: Trong hệ thống phân tán, timeout chỉ là một quan sát cục bộ, không phải phán quyết cuối cùng. Tuyệt đối không bao giờ đồng nhất "chưa nhận được phản hồi" với "thao tác chưa xảy ra" đối với các tác vụ ghi dữ liệu.
- Lỗi cục bộ và kết quả qua mạng: Ngoại lệ trong cùng tiến trình có thể truyền đến handler theo runtime; nó không tự động làm dừng thread. Qua mạng, caller có thể mất phản hồi sau khi callee đã commit. Phải phân biệt RPC failure với trạng thái nghiệp vụ.
- Quan sát độc lập giữa bên gọi và bên nhận: Client và server đưa ra các kết luận cục bộ độc lập về cùng một RPC; bên gọi thấy
DEADLINE_EXCEEDEDnhưng bên nhận đã commit xong vào cơ sở dữ liệu. - Retry cần hợp đồng chống lặp: Với write chưa rõ kết quả, dùng định danh thao tác nghiệp vụ ổn định, idempotency được server thực thi, đối soát hoặc unique constraint/conditional update phù hợp. Header idempotency key không tự đảm bảo an toàn nếu bên nhận không tôn trọng nó.
- Khoanh vùng phạm vi ảnh hưởng (blast radius): Việc chờ đợi vô hạn các dependency chậm sẽ ngốn sạch worker thread, connection pool và bộ nhớ; hệ thống bắt buộc phải đặt hạn chót (deadline), truyền tín hiệu hủy (cancellation), và chủ động fail-fast, fallback hoặc suy giảm chức năng.
- Cạm bẫy chết người: Coi timeout là thao tác chưa thực thi. Giả định rằng gọi timeout nghĩa là server chưa làm gì rồi kích hoạt retry một thao tác ghi không có idempotency là nguyên nhân số một gây trừ tiền hai lần, tạo đơn trùng lặp và làm sai lệch nghiêm trọng dữ liệu sổ cái tài chính.
Khó khăn cốt lõi không chỉ là sự cố kỹ thuật. Đó là sự không chắc chắn về những gì đã thực sự diễn ra ở đầu bên kia.
Timeout là một quan sát, không phải phán quyết
Giả sử bên gọi chờ hai giây nhưng không nhận response. Nhiều thực tế đều có thể đúng:
- request chưa bao giờ tới server;
- server đã nhận nhưng chưa xử lý xong;
- server đã xử lý và commit nhưng response bị mất;
- response chỉ tới trễ sau deadline của caller;
- server crash trước hoặc sau khi đổi durable state.
Vì vậy timeout không chứng minh operation thất bại; kết quả vẫn có thể không rõ.
Với thao tác thay đổi state, coi DEADLINE_EXCEEDED là “không có gì xảy ra” là một giả định nguy hiểm.
Bên gọi và bên nhận có thể kết luận khác nhau
gRPC nêu rõ một thuộc tính quan trọng: client và server đưa ra kết luận cục bộ độc lập về cùng một RPC. Server có thể hoàn tất thành công trong khi client báo deadline exceeded vì response tới quá trễ.
Hai phía không mâu thuẫn về tính trung thực; họ chỉ có bằng chứng khác nhau.
Failure có nhiều hình dạng hơn “server down”
Thiết kế nên tính ít nhất các nhóm sau:
- process crash: một process biến mất trong khi peer vẫn khỏe;
- dependency chậm: service còn sống nhưng vượt latency budget;
- phân vùng mạng: một số node hoặc path không giao tiếp được, phần khác vẫn hoạt động;
- request bị mất: caller gửi nhưng callee không nhận;
- mất phản hồi: callee hoàn tất nhưng caller không biết;
- degradation một chiều: traffic chỉ lỗi theo một hướng hoặc một nhóm node.
Một health check báo “up” không phân biệt được hết các trạng thái này.
Retry có thể sửa uncertainty — hoặc nhân đôi side effect
Một lần retry là attempt mới, không phải quay ngược thời gian.
Nếu Charge(payment-42) đã commit trước khi response bị mất, thử lại bằng một lệnh Charge() mới có thể tạo giao dịch trùng và tác dụng phụ lặp.
Với operation có thể retry sau failure mơ hồ, hãy dùng định danh yêu cầu ổn định hoặc idempotency key. Server phải nhận ra nhiều transport attempt thuộc cùng một logical operation, thay vì coi mỗi retry là command mới.
Idempotency không làm network đáng tin cậy hơn; nó làm việc thử lại an toàn hơn.
Cancellation không phải rollback. gRPC Core Concepts nêu rõ thay đổi đã xảy ra trước khi cancel không được hoàn tác. Hướng dẫn gRPC Cancellation yêu cầu handler hợp tác kiểm tra tín hiệu hủy: thư viện không thể ngắt mọi đoạn code ứng dụng. Vì thế một lệnh ghi timeout vẫn cần đối soát với bên nhận.
Deadline giới hạn chờ; cancellation giới hạn công việc lãng phí
Mỗi remote dependency tiêu tốn thời gian và tài nguyên. Bên gọi nên truyền một hạn chót (deadline) end-to-end hợp lý thay vì để mỗi hop bắt đầu lại một timeout đầy đủ.
Khi caller không còn cần kết quả, hãy truyền cancellation để downstream dừng công việc nếu có thể. Nhưng cancellation không phải rollback: thay đổi đã commit trước lúc hủy có thể vẫn còn nguyên.
Điều này quan trọng khi request phía trên hết deadline, user bỏ thao tác, hoặc fan-out bỏ các nhánh quá chậm.
Partial failure tạo phạm vi ảnh hưởng qua chuỗi dependency
Một lỗi nhỏ có thể thành lỗi hệ thống nếu các synchronous dependency cứ chờ lẫn nhau.
Với mỗi dependency, hãy hỏi:
- Nó bắt buộc cho correctness hay chỉ hữu ích?
- Nên fail-fast, suy giảm chức năng, dùng fallback có giới hạn hay cô lập?
- Nó nhận deadline và retry budget bao nhiêu?
- Một dependency chậm có thể làm cạn thread, connection hoặc queue capacity của caller không?
Backend “không quan trọng” vẫn có thể trở thành critical nếu caller chờ nó vô hạn.
Làm uncertainty quan sát được
HTTP status một mình không đủ tái dựng partial failure. Hãy truyền mã tương quan (correlation ID) hoặc request ID qua boundary và giữ nó trong log/trace.
Các signal hữu ích gồm dependency latency, timeout rate, số retry attempt, duplicate-suppression hit, cancellation count, saturation và success/error rate theo dependency.
Với write có outcome mơ hồ, lưu logical operation ID cùng authoritative business state để operator trả lời được: operation này đã commit chưa?
Tình huống minh họa (giả định)
Checkout gọi payment provider bằng POST /charges. Provider đã commit charge nhưng response packet bị mất. Checkout timeout rồi tự retry bằng một request ID mới.
Hậu quả: khách hàng bị charge hai lần dù chỉ thực hiện một checkout. Operator thấy một timeout và một success nên sự cố ban đầu trông như dữ liệu mâu thuẫn.
Nguyên nhân cốt lõi: caller coi timeout là bằng chứng operation thất bại rồi retry side effect không idempotent mà không có logical request identity ổn định. Bên gọi và bên nhận có hai quan sát cục bộ khác nhau về attempt đầu tiên.
Cách khắc phục chuẩn: tạo payment operation ID ổn định trước attempt đầu; gửi cùng idempotency/request key cho mọi retry; đặt deadline end-to-end và retry policy có giới hạn; propagate cancellation khi hữu ích; persist đủ state để reconcile unknown outcome; và trace mọi attempt bằng cùng correlation ID để phân biệt một logical operation với nhiều transport attempt.
Tự kiểm tra: payment call timeout thì có thể kết luận chắc chắn chưa charge không?
Không. Request có thể đã commit và chỉ response bị mất. Hãy coi outcome là chưa rõ cho tới khi query/reconcile authoritative state hoặc retry an toàn bằng request identity ổn định và idempotent semantics.
Checklist production
- Remote timeout được mô hình hóa như một outcome có thể mơ hồ.
- Retry cho state-changing operation dùng lại request identity hoặc idempotency key ổn định.
- Retry budget có giới hạn và được đặt có chủ đích trong dependency chain.
- Deadline end-to-end được truyền xuống thay vì reset ở mỗi hop.
- Cancellation dừng downstream work không còn cần thiết khi protocol hỗ trợ.
- Code không giả định cancellation rollback thay đổi đã commit.
- Dependency critical và noncritical đều có hành vi fail-fast/degrade/fallback/isolation rõ.
- Blast radius tính cả saturation của connection, worker, thread và queue.
- Correlation ID nối caller, callee, retry và bằng chứng business state.
- Metric theo dependency thể hiện latency, timeout, retry, cancellation và error.
- Write có outcome mơ hồ có đường reconciliation.
- Failure test gồm case chậm, blackhole, partition và lost response, không chỉ clean HTTP 500.
Agent rule
Khi remote call thất bại, đừng biến “tôi không nhận được success response” thành “operation không xảy ra”. Hãy liệt kê các remote state có thể có, giữ logical request identity qua retry, giới hạn waiting/retry amplification và thiết kế đường bằng chứng để reconcile outcome mơ hồ.
Nguồn
- gRPC — Core concepts: RPC termination, deadlines, and cancellation
- gRPC — Deadlines
- Amazon Builders' Library — Timeouts, retries, and backoff with jitter
- Amazon Builders' Library — Making retries safe with idempotent APIs
- Stripe — Idempotent requests
- Google SRE — Addressing Cascading Failures
Architecture Decision Records: Giữ lại lý do
Vận hành ADR bằng cách ghi lại quyết định kiến trúc quan trọng, bối cảnh, phương án, hệ quả, ownership, bằng chứng, review và supersession mà không viết lại lịch sử.
Timeout: Vận hành với ngân sách thời gian rõ ràng
Đặt, truyền, quan sát và tinh chỉnh timeout để dependency chậm chỉ giữ tài nguyên trong giới hạn mà không tạo cảm giác chắc chắn giả.