Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 4 — Retry và Backoff: chịu lỗi tạm thời

Đã đọc 0/11 mục · còn khoảng 4 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Hệ thống ngoài sẽ lỗi — timeout, 503, mất mạng chớp nhoáng. Phần lớn lỗi là tạm thời (transient) và sẽ hết nếu thử lại sau vài giây. Retry đúng cách là kỹ năng cốt lõi của orchestration. Nhưng retry sai cách còn tệ hơn không retry: bạn có thể trừ tiền khách hai lần, hoặc "đánh sập" chính hệ thống đang gặp sự cố.

Phân loại lỗi trước khi retry

Không phải lỗi nào cũng nên thử lại:

  • Transient (nên retry): timeout, 502/503/504, connection reset, rate-limited 429.
  • Permanent (KHÔNG retry): 400 (payload sai), 401/403 (sai key/quyền), 422 (nghiệp vụ từ chối, ví dụ "hết hàng").
  • Không rõ (retry thận trọng): 500 chung chung — retry vài lần rồi bỏ.
Retry một lỗi permanent chỉ tốn thời gian và làm đơn kẹt lâu hơn.

Exponential backoff + jitter

Đừng retry ngay tức khắc và đều đặn. Dùng backoff lũy thừa cộng jitter (nhiễu ngẫu nhiên) để tránh "thundering herd" — hàng loạt client cùng retry một lúc.

import random

def backoff_delay(attempt, base=1.0, cap=60.0):
    # attempt: 1,2,3...
    exp = min(cap, base * (2 ** (attempt - 1)))
    return exp / 2 + random.uniform(0, exp / 2)  # jitter

# attempt 1 -> ~0.5-1s, 2 -> ~1-2s, 3 -> ~2-4s ...

Giới hạn số lần và đường chết

Luôn có trần. Khi vượt trần, đơn không biến mất — nó đi vào Dead Letter Queue (DLQ) hoặc trạng thái FAILED để người thật xử lý.

def run_step(row, adapter):
    try:
        result = adapter.do(row.context())
        row.apply_success(result)
    except PermanentError:
        row.state = "FAILED"        # không retry
    except TransientError:
        row.attempt_count += 1
        if row.attempt_count >= MAX_ATTEMPTS:
            row.state = "FAILED"    # -> DLQ, cần bù trừ
            alert_ops(row.order_id)
        else:
            queue.push(row.order_id,
                       delay=backoff_delay(row.attempt_count))
    db.save(row)

Ghi attempt_count vào state bền, không đếm trong RAM — nếu không, restart sẽ reset bộ đếm và retry vô hạn.

Circuit breaker

Nếu một hệ thống ngoài đang chết hàng loạt, đừng cứ nện request vào nó. Circuit breaker mở mạch sau N lỗi liên tiếp, tạm dừng gọi trong T giây, rồi thử "nửa mở" một request thăm dò. Việc này bảo vệ cả bạn lẫn đối tác.

Checklist retry an toàn

  • [ ] Đã phân loại transient vs permanent bằng mã lỗi cụ thể.
  • [ ] Backoff lũy thừa có jitter, có cap trần.
  • [ ] attempt_count lưu bền, không nằm trong bộ nhớ.
  • [ ] Có trần retry và có DLQ/FAILED khi vượt.
  • [ ] Mỗi retry dùng cùng idempotency key (xem bài 5).
  • [ ] Cảnh báo ops khi một đơn vào FAILED.

Các bước triển khai

  • Lập bảng ánh xạ mã lỗi → transient/permanent cho từng adapter.
  • Thêm hàm backoff_delay và dùng nó khi đẩy job lại.
  • Lưu và tăng attempt_count trong cùng transaction với state.
  • Đặt MAX_ATTEMPTS và tuyến FAILED/alert.
  • Test bằng cách mock adapter trả 503 vài lần rồi 200 — xác nhận đơn cuối cùng thành công.

Tôn trọng tín hiệu Retry-After

Khi hệ thống ngoài trả 429 (rate limited) hoặc 503, chúng thường kèm header Retry-After cho biết nên chờ bao lâu. Hãy ưu tiên giá trị này thay vì backoff tự tính — đối tác biết rõ khi nào họ sẵn sàng hơn bạn. Bỏ qua Retry-After và nện request sớm chỉ khiến bạn bị chặn lâu hơn.

def next_delay(resp, attempt):
    ra = resp.headers.get("Retry-After")
    if ra:
        return float(ra)          # tôn trọng đối tác
    return backoff_delay(attempt)  # fallback tự tính

Timeout: đừng chờ vô hạn

Retry chỉ có ý nghĩa khi mỗi lần gọi có timeout rõ ràng. Nếu một lời gọi treo mãi (không timeout), worker của bạn kẹt và bước không bao giờ được đánh dấu để retry. Đặt hai loại timeout: connect timeout (thời gian bắt tay kết nối) và read timeout (thời gian chờ phản hồi). Timeout nên ngắn hơn nhiều so với chu kỳ backoff để một request chậm không làm nghẽn cả pipeline.

Khung tư duy: retry ở đâu trong quy trình

Retry nên nằm ở tầng orchestrator (đẩy lại job vào queue với delay), không phải một vòng for chặn ngay trong lời gọi HTTP. Retry trong vòng lặp đồng bộ giữ worker bận suốt thời gian chờ backoff — lãng phí. Retry qua queue giải phóng worker làm việc khác, và giúp attempt_count được lưu bền giữa các lần thử.

Sai lầm thường gặp

  • Retry lỗi permanent: thử lại 400 mãi mãi, đơn kẹt, tốn tài nguyên.
  • Retry đều nhịp không jitter: mọi worker cùng đập vào service vừa hồi phục, đánh sập lần nữa.
  • Đếm attempt trong RAM: restart reset về 0, retry vô hạn.
  • Retry mà không kèm idempotency key: mỗi lần thử tạo một khoản charge mới — khách bị trừ nhiều lần.
Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi