Thiết kế logic thử lại API an toàn cho production
Cuộc gọi API thanh toán thất bại lúc 2 giờ sáng. Nguyên nhân là sự cố mạng tạm thời, giới hạn tốc độ hay máy chủ ngừng hoạt động? Câu trả lời quyết định việc thử lại có cứu được giao dịch hay khiến khách hàng bị tính phí hai lần.
Thử lại là mẫu hình phục hồi phổ biến trong hệ thống phân tán, nhưng cũng rất dễ triển khai sai. Một vòng lặp quanh cuộc gọi HTTP có thể biến sự cố 30 giây thành sự cố 30 phút nếu hàng nghìn client cùng gửi lại yêu cầu đến máy chủ đang quá tải.
Một chiến lược thử lại đáng tin cậy cần có:
- Lọc mã trạng thái có thể thử lại.
- Exponential backoff với full jitter.
- Tôn trọng tiêu đề
Retry-After. - Idempotency key cho các thao tác ghi.
- Retry budget và circuit breaker.
- Kiểm thử với máy chủ giả lập trả về lỗi.
Vì sao thử lại đơn giản có thể làm sự cố tệ hơn?
Giả sử một dịch vụ xử lý 1.000 yêu cầu/giây và ngừng hoạt động trong 5 giây. Nếu mỗi client thử lại ngay lập tức ba lần, tải có thể tăng từ 1.000 lên 4.000 yêu cầu/giây. Máy chủ vốn đã quá tải sẽ càng khó phục hồi.
Đây là retry storm. Khi máy chủ hoạt động trở lại và tất cả client gửi lại cùng lúc, hiện tượng này được gọi là thundering herd. Sách SRE của Google mô tả việc retry không có backoff có thể khuếch đại lỗi dây chuyền và kéo dài thời gian ngừng hoạt động.
Hai lỗi thiết kế phổ biến nhất:
- Không có độ trễ: client retry ngay trong thời điểm hệ thống chịu tải nặng nhất.
- Độ trễ cố định: mọi client đợi đúng một khoảng thời gian rồi retry đồng bộ.
Giải pháp không phải là “không bao giờ thử lại”, mà là retry có chọn lọc, có backoff ngẫu nhiên và có giới hạn tải bổ sung.
Mã nào nên thử lại?
Trước khi tính backoff, hãy xác định rõ bảng quyết định.
Nên thử lại
| Tín hiệu | Ý nghĩa |
|---|---|
429 Too Many Requests |
Đã đạt giới hạn tốc độ; cần chậm lại. |
502 Bad Gateway |
Gateway hoặc proxy nhận phản hồi không hợp lệ; thường là lỗi tạm thời. |
503 Service Unavailable |
Máy chủ quá tải, đang khởi động lại hoặc bảo trì. |
504 Gateway Timeout |
Một dependency phía trên phản hồi quá chậm. |
| Connection reset, lỗi DNS, socket timeout | Yêu cầu có thể chưa đến máy chủ. |
Lỗi 504 cần được xử lý cẩn thận: máy chủ gốc có thể đã hoàn tất thao tác dù gateway đã ngừng chờ. Đây là lý do idempotency rất quan trọng.
Không nên thử lại
| Tín hiệu | Ý nghĩa |
|---|---|
400 Bad Request |
Dữ liệu gửi đi sai định dạng; retry không sửa được dữ liệu. |
401 Unauthorized |
Token sai hoặc hết hạn; cần làm mới thông tin xác thực. |
403 Forbidden |
Client không có quyền; retry không cấp thêm quyền. |
422 Unprocessable Entity |
Dữ liệu không vượt qua kiểm tra xác thực. |
Quy tắc thực tế: retry khi lỗi liên quan đến trạng thái máy chủ hoặc mạng; fail fast khi lỗi nằm ở request.
429 nằm ở giữa: có thể retry, nhưng cũng cho thấy tốc độ gửi tổng thể cần được cải thiện bằng rate limiting, throttling hoặc client-side caching. Xem thêm cách triển khai giới hạn tốc độ API.
Exponential backoff và full jitter
Exponential backoff tăng thời gian chờ sau mỗi lần thất bại:
delay = base * 2^retry_count
Với base = 500 ms, các lần chờ sẽ là:
0.5 giây, 1 giây, 2 giây, 4 giây, 8 giây
Luôn đặt giới hạn trên:
delay = min(cap, base * 2^retry_count)
Ví dụ, cap = 30 giây sẽ ngăn thời gian chờ tăng lên vài phút.
Tuy nhiên, exponential backoff thuần túy vẫn đồng bộ hóa client. Nếu 5.000 client cùng thất bại, chúng có thể cùng retry ở các mốc 0.5, 1 và 2 giây.
Full jitter phá vỡ sự đồng bộ bằng cách chọn ngẫu nhiên thời gian chờ trong khoảng từ 0 đến giới hạn:
delay = random_between(0, min(cap, base * 2^retry_count))
Phân tích exponential backoff và jitter của AWS cho thấy full jitter giảm các đỉnh request và thường đạt thời gian hoàn tất ngắn hơn backoff không có jitter. Equal jitter và decorrelated jitter cũng là các lựa chọn hợp lệ, nhưng full jitter đơn giản và phù hợp làm mặc định.
Tôn trọng Retry-After
Backoff là dự đoán của client. Retry-After cho phép máy chủ chỉ dẫn chính xác hơn:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Theo tài liệu Retry-After, giá trị có thể là số giây hoặc một HTTP date.
Khi tiêu đề này xuất hiện:
- Parse giá trị.
- Chờ ít nhất khoảng thời gian máy chủ yêu cầu.
- Vẫn áp dụng
capvà số lần retry tối đa.
Không nên để một giá trị lỗi như Retry-After: 86400 làm treo tiến trình cả ngày.
Idempotency: điều kiện tiên quyết khi retry POST
GET, PUT và DELETE được định nghĩa là idempotent theo hợp đồng HTTP. POST thì không.
Nếu POST /v1/payments timeout sau khi máy chủ đã xử lý thành công, retry không có kiểm soát có thể tạo thêm một khoản thanh toán. Bạn vừa xây dựng một cỗ máy tính phí hai lần.
Giải pháp là idempotency key — một ID duy nhất do client tạo, thường là UUID. Client gửi key trong mỗi lần retry; máy chủ lưu key cùng phản hồi đầu tiên và trả lại phản hồi đó cho các request trùng key. Stripe idempotent requests sử dụng mẫu hình này.
Hai quy tắc quan trọng:
- Một thao tác logic dùng một key: mọi retry của cùng một thanh toán phải tái sử dụng key; hành động mới của người dùng phải tạo key mới.
- Tạo key trước vòng lặp: không tạo key bên trong vòng retry, nếu không mỗi lần thử sẽ bị xem là một thao tác mới.
Nếu API không hỗ trợ idempotency key, không nên tự động retry các thao tác ghi không idempotent. Hãy trả lỗi để con người hoặc tác vụ đối soát xử lý.
Xem thêm hướng dẫn về idempotency key.
Retry budget và circuit breaker
Backoff kiểm soát thời điểm retry, nhưng không kiểm soát tổng số request bổ sung. Retry lồng nhau còn có thể nhân tải:
- Gateway retry 3 lần.
- Service client retry 3 lần.
- Một request của người dùng tạo ra tối đa 9 request.
Retry budget
Thay vì đặt quy tắc “3 retry cho mỗi request”, hãy đặt quy tắc:
Retry chỉ được phép tạo thêm tối đa 10% lưu lượng trong một cửa sổ trượt.
Khi ngân sách cạn, lỗi phải được trả về ngay. Cách này giới hạn mức khuếch đại dù nhiều request thất bại đồng thời. Linkerd và Envoy đều hỗ trợ retry budget ở cấp cấu hình.
Circuit breaker
Circuit breaker theo dõi tỷ lệ lỗi của từng dependency. Khi tỷ lệ vượt ngưỡng:
- Open: fail fast, không gửi request ra mạng.
- Cooldown: chờ một khoảng thời gian.
- Half-open: gửi một số request thăm dò.
- Closed: cho phép lưu lượng bình thường nếu dependency đã phục hồi.
Backoff làm giảm tốc độ retry; circuit breaker ngăn retry hoàn toàn khi dependency rõ ràng đang gặp sự cố. Hệ thống production nên kết hợp cả hai.
Ví dụ Python sẵn sàng cho production
Ví dụ dưới đây kết hợp:
- Danh sách mã trạng thái có thể retry.
- Full jitter.
- Hỗ trợ
Retry-After. - Idempotency key.
- Giới hạn retry cứng.
import random
import time
import uuid
import requests
RETRYABLE = {429, 502, 503, 504}
BASE = 0.5 # giây
CAP = 30.0 # giới hạn cho bất kỳ độ trễ nào
MAX_RETRIES = 5
def create_payment(payload):
idempotency_key = str(uuid.uuid4()) # một khóa cho mỗi thanh toán logic
headers = {"Idempotency-Key": idempotency_key}
for retry_count in range(MAX_RETRIES + 1):
try:
resp = requests.post(
"https://api.acmepay.com/v1/payments",
json=payload, headers=headers, timeout=10,
)
if resp.status_code < 400:
return resp.json()
if resp.status_code not in RETRYABLE:
resp.raise_for_status() # 400/401/403/422: thất bại nhanh chóng
retry_after = resp.headers.get("Retry-After")
except (requests.ConnectionError, requests.Timeout):
retry_after = None # lỗi mạng: chuyển sang backoff
if retry_count == MAX_RETRIES:
raise RuntimeError("payment failed after all retries") # thanh toán thất bại sau tất cả các lần thử lại
if retry_after and retry_after.isdigit():
delay = min(CAP, float(retry_after))
else:
delay = random.uniform(0, min(CAP, BASE * 2 ** retry_count))
time.sleep(delay)
Điểm cần kiểm tra:
- Key được tạo một lần, bên ngoài vòng lặp.
-
Retry-Afterđược ưu tiên nhưng vẫn chịuCAP. - Lỗi không retryable gây lỗi ngay.
-
MAX_RETRIESbao gồm cả lần gọi đầu tiên thông quarange(MAX_RETRIES + 1).
Với JavaScript, axios-retry cung cấp cấu trúc tương tự qua retryCondition và retryDelay. Bảng quyết định mã trạng thái vẫn không thay đổi.
Kiểm thử retry trước khi production gặp lỗi
Nhiều nhóm chỉ kiểm thử đường dẫn thành công. Nhánh 503, timeout hoặc 429 lần đầu được chạy trong một sự cố thật — đó là quá muộn.
Mô phỏng lỗi bằng mock server
Smart mock của Apidog cho phép định nghĩa endpoint như /v1/payments và điều khiển phản hồi:
- Trả
503cho hai cuộc gọi đầu,200cho cuộc gọi thứ ba. - Trả
429kèmRetry-After: 5. - Thêm độ trễ 15 giây để kích hoạt timeout.
- Tạo các chuỗi lỗi khác nhau cho cùng một endpoint.
Trỏ client đến mock URL và kiểm tra từng kịch bản mà không cần tạo sự cố production. Bạn có thể tham khảo thêm logic retry cho API fintech.
Kiểm tra bằng test scenarios
Dùng test scenarios để kết hợp request, assertion và kiểm tra thời gian:
- Cuộc gọi cuối cùng phải thành công.
- Tổng thời gian chờ phải nằm trong khoảng backoff dự kiến.
- Chỉ một tài nguyên được tạo.
- Các request retry phải dùng cùng idempotency key.
Kết nối scenario vào CI để logic retry được chạy trên mỗi commit, thay vì chỉ được kiểm chứng trong sự cố thực tế.
Bạn cũng có thể tải Apidog để thiết lập mock server và kiểm tra client trong vài phút.
Checklist triển khai
Trước khi đưa client vào production, hãy xác nhận:
- [ ] Chỉ retry lỗi tạm thời và lỗi mạng.
- [ ] Sử dụng exponential backoff.
- [ ] Thêm full jitter.
- [ ] Tôn trọng
Retry-After. - [ ] Có
capcho thời gian chờ. - [ ] Có giới hạn số lần retry.
- [ ] POST ghi dữ liệu dùng idempotency key.
- [ ] Có retry budget cấp hệ thống.
- [ ] Có circuit breaker cho dependency quan trọng.
- [ ] Có test cho
429,503,504, timeout và connection error. - [ ] Đo số request retry, độ trễ và tỷ lệ retry thành công.
Câu hỏi thường gặp
Có nên retry lỗi 429 không?
Có, nhưng hãy đọc Retry-After và chờ ít nhất khoảng thời gian đó. Nếu tiêu đề bị thiếu, dùng exponential backoff với jitter.
Nếu 429 xuất hiện thường xuyên, đó là dấu hiệu cần điều chỉnh tốc độ request bằng throttling hoặc caching, không phải trạng thái hoạt động bình thường.
Full jitter là gì?
Full jitter chọn ngẫu nhiên mỗi độ trễ trong khoảng:
random(0, min(cap, base * 2^n))
Cách này ngăn nhiều client retry đồng bộ. Trong các mô phỏng của AWS, full jitter vượt trội hơn backoff thông thường và equal jitter về tổng số request cũng như thời gian hoàn tất.
Có an toàn khi retry POST không?
Chỉ khi request thực sự idempotent trên máy chủ. Với POST, điều đó thường yêu cầu idempotency key để máy chủ chống tạo bản ghi trùng lặp.
Nếu không có key, timeout có thể khiến thanh toán, đơn hàng hoặc bản ghi bị nhân đôi. Điều này cũng áp dụng cho các AI agent thường xuyên gọi API ghi dữ liệu: cần keyed writes, retry có giới hạn và circuit breaker.
Nên retry bao nhiêu lần?
Ba đến năm lần thường đủ để xử lý phần lớn lỗi tạm thời. Sau đó, tỷ lệ thành công thường chững lại trong khi tải và độ trễ tiếp tục tăng.
Hãy kết hợp giới hạn retry cho từng request với retry budget toàn cầu, chẳng hạn retry chỉ được thêm tối đa 10% lưu lượng. Nếu dependency vẫn lỗi sau lần retry cuối, hãy chuyển sang circuit breaker thay vì tiếp tục retry.
Top comments (1)
Hello Sebastian, I am Kane Lim from Hong Kong. I have over 10 years of development experience. I am writing this because your post was interesting.
The strongest part here is recognizing that retry is a distributed systems control mechanism, not simply an HTTP loop. I would push the design further by making retry policy an explicit resilience layer with centralized telemetry and adaptive limits.
One issue I would additionally address is retry multiplication across service boundaries. If five downstream dependencies each apply local retries, the effective request amplification can become difficult to predict. Propagating a retry budget through request context gives every layer visibility into remaining retry capacity.
For payment workflows, I would also separate transport uncertainty from business outcome uncertainty. A timeout after request transmission means the client cannot safely conclude failure. The correct architecture is idempotency plus durable operation state and asynchronous reconciliation, rather than simply increasing MAX_RETRIES.
I would instrument retry_attempt, retry_reason, dependency, idempotency_key_hash, backoff_delay, circuit_state and final_outcome as structured metrics. Then resilience becomes measurable through retry amplification, recovery probability, tail latency and dependency saturation.
Combining exponential backoff, jitter, circuit breaking, retry budgets and reconciliation creates a much stronger failure containment model than any individual technique.
Excellent practical treatment of a problem that is often underestimated. I would enjoy exchanging ideas around adaptive resilience and distributed transaction recovery.