Tám ngộ nhận về hệ phân tán: nhìn từ một request timeout Cơ bản
Thời gian đọc ước tính: 6 phút
Vấn đề
Phần tiêu đề “Vấn đề”Client gửi POST /payments. Service đã ghi kết quả vào database, nhưng response đến chậm hơn deadline của client. Client thấy timeout và retry. Nếu mỗi request tạo một thanh toán mới, người dùng bị tính tiền hai lần.
Từ log phía client không thể biết request thất bại ở đâu. Đây là partial failure: một bên không biết chắc trạng thái của bên kia dù cả hai vẫn sống.
Nguồn gốc danh sách
Phần tiêu đề “Nguồn gốc danh sách”Danh sách bắt đầu năm 1994 với bảy mục do L. Peter Deutsch (Sun Microsystems) ghi lại. Theo Wikipedia, bốn mục trong đó lấy từ danh sách “The Fallacies of Networked Computing” trước đó của Bill Joy và một đồng nghiệp ở Sun.
Khoảng năm 1997, James Gosling thêm mục thứ tám, “mạng đồng nhất”. Bài này dùng danh sách tám mục theo bài của Oracle.
Tám ngộ nhận trong một bảng
Phần tiêu đề “Tám ngộ nhận trong một bảng”| Ngộ nhận | Phản ví dụ gần backend | Câu hỏi thiết kế |
|---|---|---|
| Mạng đáng tin cậy | TCP đã kết nối vẫn đứt giữa request và response | Sau timeout, client biết gì? Retry có an toàn không? |
| Latency bằng 0 | Mỗi RPC thêm chờ và tail latency | Một request đi qua bao nhiêu hop đồng bộ? |
| Bandwidth vô hạn | Payload lớn và fan-out nhân tổng byte | Có gửi dữ liệu thừa hoặc fetch nhiều lần không? |
| Mạng an toàn | Mạng nội bộ vẫn có máy bị xâm nhập, credential lộ | Xác thực, phân quyền, TLS, secret ở đâu? |
| Topology không đổi | Pod restart, leader failover, đổi IP | Client có discovery và reconnect không? |
| Chỉ một người quản trị | Nhiều team sở hữu gateway, service, DB, DNS | Ai xử lý sự cố; log/trace nối các bên ra sao? |
| Vận chuyển miễn phí | Serialization, TLS, egress, hop đều tốn tiền | Gộp request, cache, hay đổi ranh giới service? |
| Mạng đồng nhất | Mobile, WAN, VPN, cluster khác nhau | Timeout và protocol có hợp mọi đường đi không? |
Bốn điểm một RPC có thể hỏng
Phần tiêu đề “Bốn điểm một RPC có thể hỏng”Hàm local charge() trả kết quả hoặc throw ngay. RPC charge() có bốn điểm lỗi, và client chỉ thấy cùng một TimeoutError cho cả bốn.
Đang tải diagram…
| Điểm | Server đã làm gì | Retry cùng ý định |
|---|---|---|
| 1. Request mất | Chưa biết gì | Cần, an toàn |
| 2. Nhận, chưa làm | Chưa thay đổi gì | Cần, an toàn |
| 3. Đã commit | Side effect đã xảy ra | Phải trả kết quả cũ, không làm lại |
| 4. Response mất | Side effect đã xảy ra | Giống điểm 3 |
Vì không phân biệt được điểm 1-2 với 3-4, thao tác có side effect phải có idempotency key: server nhận cùng key thì trả kết quả cũ thay vì làm lại. Cách thiết kế key, lưu bền và xử lý hai request đồng thời nằm ở idempotent consumer.
Cách hoạt động
Phần tiêu đề “Cách hoạt động”POST /paymentsClient gửi yêu cầu tạo thanh toán. Kết nối hoạt động lúc gửi không bảo đảm response sẽ đến đúng hạn.
Timeout của client không huỷ công việc đang chạy ở server. Retry với key mới sẽ tạo payment thứ hai; retry cùng key thì server tra kết quả cũ.
Demo local (không cần Docker) dựng HTTP server trên 127.0.0.1; client có deadline 20 ms, server chờ 80 ms rồi commit. Output thực tế:
client timeout: TimeoutErrorserver committed: payment-1retry result: payment-1 committed count: 1Demo dùng Map trong RAM nên chỉ minh hoạ retry sau khi request đầu xong. Production cần storage bền và unique key.
Latency và availability nhân lên theo số hop
Phần tiêu đề “Latency và availability nhân lên theo số hop”Availability của một chuỗi gọi đồng bộ là tích availability từng phần:
| Số dependency đồng bộ, mỗi cái 99,9% | Availability chuỗi | Downtime mỗi tháng (30 ngày) |
|---|---|---|
| 1 | 99,9% | khoảng 43 phút |
| 10 | 99,0% | khoảng 7,2 giờ |
| 20 | 98,0% | khoảng 14,3 giờ |
(0,999^10 ≈ 0,990, 0,999^20 ≈ 0,980.) Ngay cả khi mỗi hop p95 nhỏ, 20 RPC tuần tự cộng dồn latency và nhân rủi ro.
Giải pháp không nhất thiết là thêm service:
- gộp API theo use case;
- chạy song song phần độc lập;
- đặt dữ liệu cùng ranh giới;
- hoặc trả kết quả bất đồng bộ.
Phân bổ ngân sách thời gian từ ngoài vào trong (timeout budget).
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Checklist khi thêm một RPC đồng bộ
Phần tiêu đề “Checklist khi thêm một RPC đồng bộ”- Thao tác có side effect nhận idempotency key; retry dùng cùng key, không sinh key mới.
- Client có deadline, và có cách tra trạng thái (
pending,paid,failed) khi kết quả chưa rõ. - Retry chỉ cho lỗi phù hợp, có giới hạn và backoff (retry, backoff, jitter).
- Đếm số hop đồng bộ trên đường găng và nhân availability; cân nhắc gộp API, chạy song song hoặc trả kết quả bất đồng bộ.
- Gọi nội bộ vẫn có xác thực, phân quyền và TLS.
- Client có discovery và reconnect khi topology đổi (Pod restart, failover, đổi IP).
- Event phát qua transactional outbox, không phát từ RAM.
Trade-offs
Phần tiêu đề “Trade-offs”Thiết kế với giả định thực tế
- Timeout, idempotency và truy vấn trạng thái cho client cách xử lý kết quả chưa rõ
- Giới hạn số hop và payload giúp latency và chi phí dễ dự đoán
- Ownership, log và trace xuyên service rút ngắn thời gian tìm lỗi một phần
Chi phí phải chấp nhận
- Idempotency cần storage bền và chính sách hết hạn
- Retry và backoff tăng tải nếu thiếu budget (retry)
- Bảo mật, discovery, observability thêm việc vận hành; chia service chỉ đáng khi lợi ích vượt chi phí
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Tóm tắt
Phần tiêu đề “Tóm tắt”Tự kiểm tra: (1) Client timeout sau POST /payments: kết luận được gì về DB của server? (2) Retry key mới khác retry cùng key thế nào? (3) 15 RPC tuần tự mỗi cái 99,9% cho availability khoảng bao nhiêu?