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 là Đè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.
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.
# 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)
# 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 ra | Hành động |
|---|---|---|
| CLOSED (Bình thường) | Mọi thứ hoạt động | Theo dõi tỷ lệ lỗi |
| OPEN (Đã nhảy) | Dependency đang lỗi | Báo động on-call. Phục vụ fallback. |
| HALF-OPEN | Đang phục hồi | Theo 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.
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.
# 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ạ
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:
- Mọi lời gọi ra ngoài đều phải có timeout.
- Mọi lần retry phải có exponential backoff + jitter.
- Nếu dependency lỗi liên tục, mở circuit — fail nhanh.
- Rate limit theo user/IP, không chỉ giới hạn toàn cục.
Bài viết liên quan
-
Chứng chỉ API & Chuỗi niềm tin (Chain of Trust): Mô hình tư duy chuẩn chỉ
Dừng việc đoán mò với lỗi SSL. Hướng dẫn cấp độ chuyên gia về Chain of Trust, debug bằng openssl và cách chứng minh lỗi thuộc về ai.
-
Caching & Redis: Mô hình tư duy 'Tờ giấy nhớ'
Tại sao Redis làm mọi thứ nhanh hơn? Hướng dẫn chuyên sâu về cache invalidation (vấn đề khó nhất CS), các chiến lược eviction, và Redis data types.
-
Task Queue & Message Broker: Celery, RabbitMQ và Kafka — Phân biệt rõ ràng
Tại sao gửi email làm đứng API? Hướng dẫn chuyên sâu về xử lý bất đồng bộ: từ Celery/Django-Q (hàng đợi tác vụ) đến RabbitMQ (môi giới) và Kafka (luồng sự kiện).
-
REST vs. GraphQL vs. gRPC: Mô hình tư duy 'Thực đơn Nhà hàng'
Tại sao GraphQL lại ra đời nếu REST đã ổn? Hướng dẫn chuyên sâu về giao thức API, khi nào dùng cái nào, và tại sao gRPC thay đổi cuộc chơi cho các service nội bộ.