cat blog/.md
Retry và Circuit Breaker: xử lý lỗi tạm thời trong hệ thống phân tán
Service A gọi Service B. Service B đang deploy lại, mất khoảng 10 giây. Trong 10 giây đó, Service A nhận liên tục Connection refused. Nếu không xử lý gì, toàn bộ request đến Service A cũng fail theo — dù logic phía A hoàn toàn không có vấn đề.
Tệ hơn: 50 instance của Service A đồng loạt retry vào Service B vừa lúc nó khởi động xong. Service B chưa kịp warm up đã bị đập 5.000 requests/giây. Lại chết.
Đây là cascading failure — lỗi lan từ service này sang service khác như hiệu ứng domino. Và hai pattern cơ bản nhất để chống lại nó là retry và circuit breaker.
Retry: thử lại, nhưng phải thông minh
Ý tưởng đơn giản: request fail → đợi một chút → thử lại. Nhiều lỗi trong hệ thống phân tán là tạm thời (transient) — network blip, service đang restart, database connection pool đầy trong tích tắc. Thử lại là đủ.
Nhưng retry ngây thơ sẽ giết hệ thống.
Đừng retry ngay lập tức
# ❌ Sai: retry ngay, không delay
for i in range(3):
try:
response = await call_service_b()
break
except ConnectionError:
continue # retry ngay → tạo spike traffic
Service B đang quá tải mà bạn gửi thêm request liên tục? Nó càng không thể recover.
Exponential backoff + jitter
Pattern chuẩn: mỗi lần retry, tăng delay theo cấp số nhân và thêm random jitter để tránh nhiều client retry đồng thời.
import asyncio
import random
import httpx
async def call_with_retry(
url: str,
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
):
for attempt in range(max_retries + 1):
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(url)
response.raise_for_status()
return response.json()
except (httpx.ConnectError, httpx.TimeoutException, httpx.HTTPStatusError) as e:
if attempt == max_retries:
raise
# Exponential backoff: 1s, 2s, 4s, ...
delay = min(base_delay * (2 ** attempt), max_delay)
# Full jitter: random từ 0 đến delay
jitter = random.uniform(0, delay)
print(f"Attempt {attempt + 1} failed: {e}. Retrying in {jitter:.1f}s...")
await asyncio.sleep(jitter)
Tại sao jitter quan trọng? Hình dung 100 client đều fail cùng lúc. Không có jitter, tất cả đều retry sau đúng 1 giây, rồi 2 giây, rồi 4 giây — tạo ra thundering herd. Jitter phân tán các retry ra khắp timeline, giảm đáng kể spike.
AWS SDK dùng pattern này mặc định và gọi nó là “full jitter” — đã được chứng minh hiệu quả trong production scale lớn.
Chỉ retry lỗi tạm thời
Không phải lỗi nào cũng nên retry:
| Status code | Nên retry? | Lý do |
|---|---|---|
| 500, 502, 503 | ✅ Có | Server error, có thể tạm thời |
| 429 | ✅ Có | Rate limited, đợi rồi thử lại |
| 408 | ✅ Có | Timeout, có thể do network |
| 400, 401, 403, 404 | ❌ Không | Client error, retry cũng vậy |
| 409 | ⚠️ Tùy | Conflict, cần check idempotency |
Retry một request 400 Bad Request 3 lần không thay đổi kết quả — chỉ tốn resource.
Idempotency: retry an toàn không?
Câu hỏi quan trọng nhất trước khi retry: gọi lại có tạo side effect không?
GET /orders/123→ retry thoải mái, idempotentPOST /orders→ nguy hiểm! Retry có thể tạo 2 đơn hàngPUT /orders/123→ thường an toàn (replace toàn bộ)DELETE /orders/123→ thường an toàn (xóa rồi xóa lại = vẫn xóa)
Với non-idempotent operations, bạn cần idempotency key:
# Client gửi kèm idempotency key
headers = {"Idempotency-Key": "order-abc-123-attempt-1"}
response = await client.post("/orders", json=data, headers=headers)
# Server side: check key trước khi xử lý
async def create_order(request):
key = request.headers.get("Idempotency-Key")
existing = await cache.get(f"idempotency:{key}")
if existing:
return existing # trả về kết quả cũ, không tạo order mới
result = await process_order(request.json())
await cache.set(f"idempotency:{key}", result, ttl=86400)
return result
Stripe, PayPal, và hầu hết payment API đều dùng pattern này. Nếu bạn build API cho người khác gọi, hãy support idempotency key.
Circuit Breaker: ngắt mạch khi service chết
Retry xử lý lỗi tạm thời. Nhưng nếu Service B chết hẳn 5 phút? Retry 3 lần, mỗi lần timeout 5 giây = 15 giây chờ vô ích cho mỗi request. Nhân với hàng nghìn request, Service A bị nghẽn hoàn toàn.
Circuit breaker giải quyết bằng cách ngừng gọi service đã chết, thay vì cứ cố retry.
3 trạng thái
Giống cầu dao điện trong nhà bạn:
failure threshold
┌──────┐ exceeded ┌──────┐
│CLOSED│ ───────────────────→ │ OPEN │
│ │ │ │
└──┬───┘ └──┬───┘
↑ │
│ success │ timeout
│ │ expires
│ ┌─────────┐ │
└──────── │HALF-OPEN│ ←───────┘
│ │
└─────────┘
failure → back to OPEN
- Closed (bình thường): request đi qua, đếm lỗi. Nếu lỗi vượt ngưỡng → chuyển sang Open.
- Open (ngắt mạch): chặn toàn bộ request, trả lỗi ngay lập tức. Không timeout, không chờ. Sau một khoảng thời gian → chuyển sang Half-Open.
- Half-Open (thử lại): cho một vài request đi qua. Nếu thành công → Closed. Nếu fail → quay lại Open.
Implementation
Đây là circuit breaker đơn giản bằng TypeScript:
type State = "closed" | "open" | "half-open";
class CircuitBreaker {
private state: State = "closed";
private failureCount = 0;
private lastFailureTime = 0;
private readonly failureThreshold: number;
private readonly resetTimeout: number; // ms
constructor(options: { failureThreshold?: number; resetTimeout?: number } = {}) {
this.failureThreshold = options.failureThreshold ?? 5;
this.resetTimeout = options.resetTimeout ?? 30_000; // 30s
}
async call<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {
if (this.state === "open") {
// Check nếu đã hết timeout → chuyển sang half-open
if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
this.state = "half-open";
} else {
if (fallback) return fallback();
throw new Error("Circuit breaker is OPEN — request blocked");
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
if (fallback) return fallback();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = "closed";
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = "open";
console.warn(
`Circuit OPEN after ${this.failureCount} failures. ` +
`Will retry in ${this.resetTimeout / 1000}s.`
);
}
}
}
Sử dụng:
const breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30_000 });
// Gọi với fallback
const data = await breaker.call(
() => fetch("https://service-b/api/data").then(r => r.json()),
() => ({ items: [], fromCache: true }) // fallback khi circuit open
);
Khi circuit open, request fail ngay lập tức — không timeout, không tốn resource. Service A vẫn hoạt động (dù degraded), user vẫn nhận được response.
Kết hợp Retry + Circuit Breaker
Hai pattern này bổ trợ nhau hoàn hảo:
- Retry (inner): xử lý lỗi tạm thời — network blip, 503 thoáng qua
- Circuit breaker (outer): xử lý lỗi kéo dài — service chết, dependency sập
Thứ tự quan trọng: circuit breaker bọc bên ngoài retry.
Request → [Circuit Breaker] → [Retry với backoff] → Service B
Nếu ngược lại (retry bọc circuit breaker), mỗi retry đều check circuit breaker — vẫn ổn. Nhưng pattern phổ biến hơn là circuit breaker đếm số lần retry cuối cùng vẫn fail, không đếm từng attempt riêng lẻ.
const breaker = new CircuitBreaker({ failureThreshold: 3, resetTimeout: 60_000 });
async function callServiceB(path: string) {
return breaker.call(async () => {
// Retry logic bên trong
return callWithRetry(`https://service-b${path}`, {
maxRetries: 2,
baseDelay: 500,
});
});
}
Logic: retry 2 lần trước khi báo fail cho circuit breaker. Circuit breaker đếm 3 lần “retry đã hết mà vẫn fail” → open circuit.
Bài học từ production
Sau khi triển khai ở vài dự án, đây là những điều mình đúc kết:
1. Log đầy đủ state transitions. Khi circuit breaker chuyển từ closed → open, đó là signal quan trọng. Gắn alert vào đây — nó cho bạn biết dependency có vấn đề trước khi user report.
2. Đừng set threshold quá thấp. failureThreshold: 2 nghĩa là 2 request fail liên tiếp là ngắt mạch. Trong hệ thống real-world, 2 lỗi liên tiếp xảy ra thường xuyên hơn bạn tưởng. Mình thường bắt đầu với 5-10.
3. Reset timeout nên tăng dần. Lần đầu open: đợi 30s. Nếu half-open vẫn fail, lần sau đợi 60s, rồi 120s. Tránh liên tục probe service đang chết.
4. Fallback phải hữu ích. Trả về cached data, default value, hoặc response từ service backup. Đừng chỉ throw error — hãy degrade gracefully.
5. Đừng tự build cho production. Ví dụ trên là để hiểu concept. Khi dùng thật, hãy dùng thư viện đã battle-tested:
- Python:
tenacity(retry),pybreaker(circuit breaker) - Node.js:
cockatiel,opossum - Java: Resilience4j
- Infra level: Istio, Envoy proxy có built-in circuit breaker
Tóm lại
| Pattern | Xử lý | Khi nào dùng |
|---|---|---|
| Retry + backoff | Lỗi tạm thời | Network blip, 503 thoáng qua |
| Circuit breaker | Lỗi kéo dài | Service chết, dependency sập |
| Kết hợp cả hai | Mọi trường hợp | Production system nghiêm túc |
Hai pattern này không fancy, không trendy, nhưng là nền tảng của bất kỳ hệ thống phân tán nào muốn chạy ổn định. Nếu hệ thống của bạn có nhiều hơn 2 services gọi nhau, hãy triển khai ngay — trước khi 3 giờ sáng bị gọi dậy vì cascading failure.
cat comments.log