Featured image of post Rate Limiting & Circuit Breaker: Mô hình tư duy 'Đèn Giao thông & Hộp Cầu dao'

Rate Limiting & Circuit Breaker: Mô hình tư duy 'Đèn Giao thông & Hộp Cầu dao'

Làm sao ngăn một client xấu làm sập toàn bộ API của bạn? Hướng dẫn chuyên sâu về chiến lược rate limiting, circuit breaker và các pattern tăng cường độ bền.

Một client gửi 10,000 request mỗi giây đến API của bạn. Server bị quá tải. Mọi client khác đều nhận “503 Service Unavailable.”

Một service upstream bị sập. App của bạn vẫn cứ gọi đến nó. Các thread xếp hàng chờ. Toàn bộ app trở nên chậm chạp với mọi người.

Đây không phải giả thuyết. Nó xảy ra trong mọi hệ thống production. Rate Limiting và Circuit Breaker là hàng phòng thủ của bạn.


Phần 1: Nền tảng (Mô hình tư duy)

Rate Limiting = Đèn Giao thông

Rate LimiterĐèn Giao thông tại một ngã tư bận rộn.

  • Nó không chặn tất cả xe cộ. Nó điều tiết luồng.
  • Đèn đỏ: “Bạn gửi quá nhiều request rồi. Chờ 60 giây.” (HTTP 429 Too Many Requests).
  • Đèn xanh: “Bạn vẫn trong giới hạn. Đi qua.”

Nó bảo vệ ai: Service của bạn khỏi bị áp đảo bởi bất kỳ một client nào. Cũng bảo vệ khỏi DDoS và data scraping.

Circuit Breaker = Hộp Cầu dao

Circuit Breaker bảo vệ app của bạn khỏi dependency đang lỗi (API bên ngoài, database).

Hãy nghĩ về hộp cầu dao điện. Nếu xảy ra chập điện (quá nhiều dòng điện), cầu dao nhảy (ngắt mạch). Điện lập tức ngừng chảy. Nhà bạn không bị cháy.

Các trạng thái:

  • CLOSED (Bình thường): Request đi qua đến dependency.
  • OPEN (Đã nhảy): Dependency thất bại quá nhiều lần. Ngừng gọi. Trả về fallback ngay lập tức.
  • HALF-OPEN (Đang thử): Sau một khoảng timeout, cho phép một request thử nghiệm. Thành công → CLOSED. Thất bại → OPEN lại.
1
2
CLOSED → (quá nhiều lỗi) → OPEN → (timeout) → HALF-OPEN → (thành công) → CLOSED
                                                            → (thất bại) → OPEN

Phần 2: Điều tra (Các thuật toán Rate Limiting)

1. Fixed Window (Cửa sổ cố định)

Đơn giản nhất. Cho phép N request mỗi cửa sổ thời gian (ví dụ: 100/phút).

  • Vấn đề: Một client gửi 100 request lúc 12:59. Rồi 100 cái nữa lúc 13:00. Thực tế là họ đánh bạn 200 request trong 2 giây ngay tại ranh giới.

2. Sliding Window Log (Cửa sổ trượt)

Theo dõi timestamp của mọi request. Đếm xem có bao nhiêu cái trong 60 giây qua.

  • Ưu: Chính xác. Không có vấn đề bùng nổ ranh giới.
  • Nhược: Tốn bộ nhớ. Phải lưu từng timestamp.

3. Token Bucket (Tốt nhất cho API)

Một cái thùng chứa token, được nạp đầy đều đặn (ví dụ: 10 token/giây, tối đa 100). Mỗi request tiêu tốn 1 token. Nếu thùng rỗng: 429.

  • Ưu: Cho phép bùng phát ngắn (tối đa bằng dung tích thùng). Tốc độ dài hạn ổn định.
  • Nhược: Phức tạp hơn một chút.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Token Bucket với Redis (atomic, hoạt động với nhiều server)
import redis, time

r = redis.Redis()

def is_allowed(user_id: str, rate: int = 10, burst: int = 100) -> bool:
    now = time.time()
    key = f"bucket:{user_id}"
    
    pipe = r.pipeline()
    pipe.hgetall(key)
    pipe.expire(key, 60)
    result = pipe.execute()
    
    bucket = result[0]
    tokens = float(bucket.get(b"tokens", burst))
    last_refill = float(bucket.get(b"last_refill", now))
    
    # Nạp token theo thời gian đã trôi qua
    elapsed = now - last_refill
    tokens = min(burst, tokens + elapsed * rate)
    
    if tokens < 1:
        return False  # 429
    
    # Tiêu tốn 1 token
    r.hset(key, mapping={"tokens": tokens - 1, "last_refill": now})
    return True

Phần 3: Chẩn đoán (Trạng thái Circuit Breaker)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Dùng thư viện 'circuitbreaker'
from circuitbreaker import circuit

@circuit(
    failure_threshold=5,    # Mở sau 5 lỗi liên tiếp
    recovery_timeout=30,    # Chờ 30 giây trước khi thử HALF-OPEN
    expected_exception=Exception
)
def call_payment_api(order_id: str) -> dict:
    response = requests.post("https://payment-service/charge", json={"order": order_id})
    response.raise_for_status()
    return response.json()


def process_order(order_id: str):
    try:
        result = call_payment_api(order_id)
    except CircuitBreakerError:
        # Mạch đang OPEN — không thử nữa. Fail nhanh với fallback.
        return {"status": "pending", "message": "Payment service đang sập. Sẽ thử lại sau."}
    except Exception as e:
        return {"status": "error", "message": str(e)}

Theo dõi trạng thái Circuit Breaker

Trạng tháiĐiều đang xảy raHành động
CLOSED (Bình thường)Mọi thứ hoạt độngTheo dõi tỷ lệ lỗi
OPEN (Đã nhảy)Dependency đang lỗiBáo động on-call. Phục vụ fallback.
HALF-OPENĐang phục hồiTheo dõi chặt request thử nghiệm

Phần 4: Giải pháp (Pattern thực tế)

1. Retry với Exponential Backoff

Đừng thử lại ngay. Hãy chờ, rồi chờ lâu hơn.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import time, random

def call_with_retry(fn, max_retries=3):
    for attempt in range(max_retries):
        try:
            return fn()
        except Exception as e:
            if attempt == max_retries - 1:
                raise  # Lần thử cuối: ném lại lỗi
            wait = (2 ** attempt) + random.uniform(0, 1)  # Exponential + Jitter
            time.sleep(wait)

Tại sao cần Jitter? Không có jitter, tất cả client thất bại đều thử lại cùng một lúc → đám đông sấm sét → service đang phục hồi lại bị sập ngay lập tức.

2. Bulkhead Pattern (Vách ngăn)

Cô lập các phần khác nhau của hệ thống để một lỗi không lan sang phần khác.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Không có Bulkhead: tất cả thread dùng chung một pool
# Một endpoint chậm làm cạn kiệt thread của tất cả endpoint khác

# Có Bulkhead: mỗi service quan trọng có pool thread riêng
from concurrent.futures import ThreadPoolExecutor

payment_executor = ThreadPoolExecutor(max_workers=5, thread_name_prefix="payment")
notification_executor = ThreadPoolExecutor(max_workers=10, thread_name_prefix="notif")

# Payment chậm không ảnh hưởng đến notification
future = payment_executor.submit(call_payment_api, order_id)

Mô hình tư duy chốt hạ

1
2
3
4
5
6
7
Rate Limiter    -> Đèn Giao thông. Kiểm soát luồng vào. Bảo vệ BẠN khỏi client.
Circuit Breaker -> Hộp Cầu dao. Kiểm soát luồng ra. Bảo vệ BẠN khỏi dependency.

Token Bucket    -> Luồng đều đặn, cho phép bùng phát ngắn.
CLOSED          -> Mọi thứ bình thường. Cho traffic đi qua.
OPEN            -> Dependency lỗi. Dừng gọi. Trả về cache/fallback.
Exponential Backoff -> "Chờ 1s, 2s, 4s, 8s trước khi thử lại." Tránh đám đông sấm sét.

Quy tắc độ bền:

  1. Mọi lời gọi ra ngoài đều phải có timeout.
  2. Mọi lần retry phải có exponential backoff + jitter.
  3. Nếu dependency lỗi liên tục, mở circuit — fail nhanh.
  4. Rate limit theo user/IP, không chỉ giới hạn toàn cục.
Được tạo với sự lười biếng tình yêu 🦥

Subscribe to My Newsletter