Phần lớn các sự cố sập server diện rộng (Cascading Failure) không bắt nguồn từ mã nguồn nội bộ, mà đến từ các dịch vụ bên thứ ba: cổng gửi SMS OTP, API tra cứu mã số thuế hoặc dịch vụ tính phí vận chuyển bỗng nhiên phản hồi chậm mất 10–20 giây.
Khi API ngoài bị treo, mọi luồng xử lý (worker thread) và connection pool của backend sẽ bị giữ chặt để chờ đợi. Chỉ sau vài phút có traffic thực tế, toàn bộ tài nguyên máy chủ cạn kiệt, kéo sập luôn cả các tính năng hoàn toàn không liên quan.
Dưới đây là cách triển khai Circuit Breaker Pattern (Cơ chế ngắt mạch tự động) giúp hệ thống tự cô lập lỗi và duy trì khả năng phục vụ ngay cả khi đối tác ngừng hoạt động.
1. Mô hình 3 trạng thái của Circuit Breaker
Giống hệt chiếc aptomat điện trong gia đình tự nhảy khi quá tải để chống cháy nhà, Circuit Breaker trong phần mềm hoạt động qua 3 trạng thái liên hoàn:
┌────────────────────────────────────────────────────────┐
│ │
▼ │
[ Trạng thái CLOSED ] ──(Tỷ lệ lỗi > Ngưỡng)──► [ Trạng thái OPEN ]
(Cho request đi qua) (Chặn đứng, trả Fallback ngay)
▲ │
│ │ (Hết thời gian chờ Reset)
│ ▼
└──────(Thử nghiệm thành công)────── [ Trạng thái HALF-OPEN ]
(Cho 1 vài request đi thử)
- CLOSED (Đóng mạch): Trạng thái bình thường. Mọi request được gửi thẳng tới API đối tác. Bộ đếm theo dõi số lần thành công/thất bại trong một khung thời gian trượt.
- OPEN (Ngắt mạch): Khi tỷ lệ lỗi hoặc timeout vượt ngưỡng (ví dụ: lỗi > 50% trong 10 giây qua), mạch lập tức bung ra. Toàn bộ request tiếp theo bị chặn đứng ngay tại chỗ, trả về phản hồi dự phòng (Fallback) trong dưới 1ms mà không thèm gọi ra ngoài.
- HALF-OPEN (Thử nghiệm): Sau một khoảng thời gian nghỉ (ví dụ: 30 giây), mạch hé mở cho một vài request mẫu đi qua. Nếu các request này thành công, mạch đóng lại bình thường (CLOSED); nếu vẫn lỗi, mạch tiếp tục bung ra (OPEN) thêm 30 giây nữa.
2. Triển khai Circuit Breaker gọn nhẹ trong TypeScript / Node.js
Thay vì phải cài các thư viện cồng kềnh, bạn hoàn toàn có thể tự đóng gói một Circuit Breaker class nguyên khối để bọc quanh các lệnh gọi fetch:
type BreakerState = "CLOSED" | "OPEN" | "HALF_OPEN";
export class CircuitBreaker {
private state: BreakerState = "CLOSED";
private failureCount = 0;
private successCount = 0;
private lastFailureTime = 0;
constructor(
private readonly failureThreshold = 5, // Chạm 5 lỗi liên tiếp -> Bật OPEN
private readonly recoveryTimeout = 30000, // Thời gian chờ phục hồi: 30s
private readonly halfOpenSuccessThreshold = 2 // 2 request thử nghiệm thành công -> Đóng mạch
) {}
async execute<T>(action: () => Promise<T>, fallback: () => T): Promise<T> {
const now = Date.now();
// 1. Kiểm tra chuyển từ OPEN sang HALF_OPEN khi hết timeout
if (this.state === "OPEN") {
if (now - this.lastFailureTime > this.recoveryTimeout) {
this.state = "HALF_OPEN";
this.successCount = 0;
} else {
// Mạch đang mở: Không gọi ngoài, trả ngay fallback
return fallback();
}
}
try {
// 2. Thực thi lệnh gọi thực tế với Timeout cứng 3 giây
const result = await Promise.race([
action(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 3000)
)
]);
return this.onSuccess(result);
} catch (error) {
return this.onFailure(fallback);
}
}
private onSuccess<T>(result: T): T {
this.failureCount = 0;
if (this.state === "HALF_OPEN") {
this.successCount++;
if (this.successCount >= this.halfOpenSuccessThreshold) {
this.state = "CLOSED";
}
}
return result;
}
private onFailure<T>(fallback: () => T): T {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === "HALF_OPEN" || this.failureCount >= this.failureThreshold) {
this.state = "OPEN";
}
return fallback();
}
}
3. Thiết kế kịch bản Fallback thông minh: Đừng để khách thấy trang trắng
Khi mạch đã nhảy (OPEN), trải nghiệm người dùng phụ thuộc hoàn toàn vào cách bạn thiết kế hàm fallback():
- Tính cước vận chuyển chậm: Thay vì báo lỗi, trả về mức phí tạm tính trung bình (ví dụ: mặc định 30.000đ) kèm dòng chữ: “Phí vận chuyển thực tế sẽ được điều chỉnh nhẹ khi giao hàng”.
- Cổng tra cứu mã số thuế/tồn kho ngoài bị sập: Đọc dữ liệu từ bản sao lưu gần nhất trong Redis cache (Stale-While-Revalidate). Thông tin có thể cũ 1–2 tiếng nhưng vẫn giúp khách hàng tiếp tục hoàn tất đơn hàng.
- Gửi SMS OTP thất bại: Tự động điều hướng dự phòng sang kênh gửi mã qua Email hoặc Zalo ZNS.
4. Ba điểm lưu ý cốt tử khi vận hành
- Bắt buộc phải có Hard Timeout per Call: Nếu không đặt giới hạn thời gian (ví dụ: tối đa 3 giây) cho mỗi request gọi ra ngoài, cơ chế Circuit Breaker sẽ hoàn toàn vô dụng vì worker vẫn bị treo vĩnh viễn trước khi kịp tính là lỗi.
- Không chia sẻ trạng thái tập trung nếu không cần thiết: Với hệ thống vừa và nhỏ, để Circuit Breaker tự quản lý trạng thái trong bộ nhớ từng instance Node.js là đủ nhanh và an toàn. Chỉ dùng Redis để đồng bộ trạng thái mạch khi có hàng chục microservice cùng gọi chung một đối tác.
- Bắn cảnh báo ngay khi mạch nhảy sang OPEN: Khi mạch chuyển sang trạng thái OPEN, hệ thống phải bắn ngay alert vào Telegram/Slack nội bộ để đội kỹ thuật liên hệ đối tác kiểm tra, thay vì đợi người dùng khiếu nại.
Trong kiến trúc hệ thống hiện đại, việc một dịch vụ phụ thuộc bị lỗi không phải là xác suất “nếu xảy ra” mà là “khi nào xảy ra”. Thiết kế sẵn cơ chế Circuit Breaker cùng kịch bản dự phòng mềm dẻo chính là chìa khóa giúp ứng dụng của bạn duy trì độ tin cậy 99.9% ngay cả trong những thời điểm giông bão của các nhà cung cấp bên ngoài.


