Bối cảnh & Tại sao cần Circuit Breaker trong Microservices
Giả sử hệ thống e-commerce của bạn gồm 5 microservices gọi chéo nhau. Đột nhiên, cổng thanh toán bên thứ ba gặp sự cố. Thời gian phản hồi bình thường chỉ khoảng 150ms bỗng vọt lên 30 giây trước khi timeout.
Lúc này, hàng nghìn request từ người dùng ồ ạt đổ về. Do mỗi request bị giữ lại chờ phản hồi, Node.js nhanh chóng cạn kiệt socket pool và chạm trần file descriptor (lỗi EMFILE). Bộ nhớ RAM tăng vọt, các request mới bắt đầu bị từ chối hàng loạt. Cuối cùng, service Order, Cart và Auth lần lượt sập theo như hiệu ứng domino. Đây chính là thảm họa Cascading Failure (sụp đổ dây chuyền).
Vấn đề cốt lõi là mạng phân tán luôn tiềm ẩn rủi ro chập chờn. Khi một downstream service đang quá tải, dồn thêm traffic chỉ khiến nó chết nhanh hơn và kéo sập luôn caller service. Hạ timeout xuống 2 giây chỉ giúp giải phóng request nhanh hơn đôi chút. Nó không ngăn được việc hàng nghìn request vô nghĩa liên tục tiêu tốn tài nguyên mạng và CPU.
Để xử lý triệt để, chúng ta áp dụng Circuit Breaker Pattern (mô hình cầu dao điện). Tương tự aptomat trong nhà tự ngắt khi chập điện để bảo vệ mạng lưới, Circuit Breaker trong phần mềm chặn đứng các lệnh gọi tới dịch vụ đang hỏng. Cơ chế này vận hành qua 3 trạng thái:
- Closed (Đóng mạch): Trạng thái bình thường. Mọi request đều được gửi thẳng tới đích. Khi tỷ lệ request thất bại vượt ngưỡng (ví dụ trên 50%), cầu dao tự động ngắt sang Open.
- Open (Ngắt mạch): Hệ thống chặn hoàn toàn lời gọi ra ngoài. Request gửi đến lập tức trả về lỗi ngay (fail fast) hoặc kích hoạt hàm xử lý dự phòng (fallback) trong vòng dưới 1ms, hoàn toàn không tốn tài nguyên chờ đợi socket.
- Half-Open (Nửa mở): Sau một khoảng thời gian nghỉ định trước (resetTimeout, thường là 10–30 giây), mạch cho phép vài request thăm dò đi qua. Nếu các request này xử lý thành công, mạch đóng lại (Closed) và hệ thống trở lại bình thường. Ngược lại, nếu vẫn lỗi, mạch lập tức ngắt tiếp (Open).
Trong thực tế vận hành, việc cấu hình thông số cầu dao cần dựa trên metrics traffic thực tế thay vì đoán mò. Một bộ thông số sai lệch có thể vô tình ngắt luôn các service vẫn đang hoạt động ổn định.
Cài đặt Opossum cho dự án Node.js
Trong hệ sinh thái Node.js, opossum là thư viện Circuit Breaker hoàn thiện và phổ biến nhất hiện nay. Thư viện hỗ trợ Promise/async-await native, kiến trúc event-driven nhẹ nhàng và tích hợp rất mượt với Prometheus metrics.
Khởi tạo project và cài đặt opossum kèm axios:
mkdir node-circuit-breaker-demo
cd node-circuit-breaker-demo
npm init -y
npm install opossum axios
Cấu hình chi tiết Opossum Circuit Breaker
Để cầu dao hoạt động đúng kỳ vọng, bạn cần hiểu rõ từng tham số. Cấu hình quá nhạy sẽ gây ngắt mạch giả khi mạng chỉ vừa trễ nhẹ vài mili-giây. Ngược lại, cấu hình quá lỏng lẻo sẽ khiến cầu dao phản ứng quá chậm khi service đích đã sập.
Dưới đây là module bọc lời gọi API thanh toán thực tế:
// paymentClient.js
const axios = require('axios');
const CircuitBreaker = require('opossum');
// Hàm thực hiện lời gọi API thực tế
async function callPaymentGateway(orderData) {
const response = await axios.post('https://api.payment-mock.com/charge', orderData, {
timeout: 2500 // Axios timeout cục bộ cho từng request
});
return response.data;
}
// Cấu hình Circuit Breaker
const breakerOptions = {
timeout: 3000, // Quá 3000ms coi như thất bại
errorThresholdPercentage: 50, // Mở mạch khi >= 50% request thất bại
resetTimeout: 10000, // Đợi 10 giây ở trạng thái Open trước khi thử lại
rollingCountTimeout: 10000, // Cửa sổ trượt 10 giây để tính tỷ lệ lỗi
rollingCountBuckets: 10, // Chia cửa sổ thành 10 bucket (1 giây/bucket)
volumeThreshold: 5 // Cần tối thiểu 5 request trong cửa sổ trượt mới xét ngắt mạch
};
const paymentBreaker = new CircuitBreaker(callPaymentGateway, breakerOptions);
// Fallback: Trả về kết quả dự phòng khi mạch Open hoặc request bị lỗi
paymentBreaker.fallback((orderData, error) => {
return {
status: 'DEFERRED',
message: 'Hệ thống thanh toán đang bận, đơn hàng đã được đưa vào hàng đợi xử lý ngầm.',
orderId: orderData.orderId,
fallback: true
};
});
module.exports = paymentBreaker;
Ý nghĩa các thông số then chốt:
- volumeThreshold: Tránh mở mạch sai khi lưu lượng thấp. Giả sử vào 3 giờ sáng chỉ có đúng 1 request bị timeout, tỷ lệ lỗi là 100% (1/1). Nếu không có ngưỡng volume tối thiểu, cầu dao sẽ ngắt mạch oan uổng.
- rollingCountTimeout: Thống kê tỷ lệ lỗi theo cửa sổ trượt thời gian thực thay vì cộng dồn từ khi khởi động app. Nhờ đó, một vài lỗi từ 30 phút trước sẽ không làm ảnh hưởng đến quyết định của hiện tại.
- fallback: Thực hiện graceful degradation. Thay vì quẳng lỗi 500 vào mặt người dùng, hệ thống trả về phản hồi lịch sự hoặc đẩy job vào RabbitMQ/Kafka để thử lại sau.
Kiểm tra & Monitoring trạng thái Circuit Breaker
Để kiểm chứng cầu dao vận hành chuẩn xác, chúng ta đăng ký lắng nghe các lifecycle events của Opossum và mô phỏng endpoint đích bị sự cố.
// server.js
const paymentBreaker = require('./paymentClient');
// Lắng nghe các event để giám sát trạng thái
paymentBreaker.on('open', () => {
console.warn('⚠️ [CIRCUIT BREAKER] Mạch đã MỞ (OPEN)! Chặn gọi ra ngoài, kích hoạt Fallback ngay.');
});
paymentBreaker.on('halfOpen', () => {
console.info('🔄 [CIRCUIT BREAKER] Mạch chuyển sang NỬA MỞ (HALF-OPEN). Đang gửi request thăm dò...');
});
paymentBreaker.on('close', () => {
console.info('✅ [CIRCUIT BREAKER] Mạch đã ĐÓNG (CLOSED). Service đích đã phục hồi hoàn toàn.');
});
paymentBreaker.on('fallback', (result) => {
console.log('🛡️ [FALLBACK TRIGGERED] Đã trả về kết quả dự phòng:', result.orderId);
});
// Giả lập 15 request liên tiếp với endpoint không phản hồi
async function simulateRequests() {
for (let i = 1; i <= 15; i++) {
try {
console.log(`\n--- Gửi Request #${i} ---`);
const result = await paymentBreaker.fire({ orderId: `ORDER_${i}`, amount: 100 });
console.log('Kết quả:', result);
} catch (err) {
console.error('Request thất bại:', err.message);
}
// Giãn cách 800ms giữa các lượt gọi
await new Promise(resolve => setTimeout(resolve, 800));
}
}
simulateRequests();
Chạy thử nghiệm:
node server.js
Khi chạy script với domain mock không tồn tại, bạn sẽ quan sát thấy chu trình xử lý rất rõ ràng:
- Từ request 1 đến 5, axios timeout liên tục sau 2.5 giây.
- Đến request thứ 5, điều kiện
volumeThresholdđược thỏa mãn và tỷ lệ lỗi đạt 100%. Eventopenlập tức kích hoạt. - Từ request thứ 6 trở đi, Opossum chặn đứng toàn bộ traffic mạng. Dữ liệu fallback trả về tức thì với độ trễ gần như 0ms, giải phóng hoàn toàn socket và bộ nhớ cho Node.js.
- Sau đúng 10 giây (resetTimeout), mạch tự chuyển sang
halfOpenđể gửi request thăm dò sức khỏe downstream service.
Khi đưa lên production, bạn nên xuất các metrics của Opossum ra Prometheus thông qua prom-client. Kết hợp cùng dashboard Grafana và cảnh báo Alertmanager qua Slack hoặc Telegram, team SRE sẽ nắm bắt chính xác thời điểm downstream service gặp sự cố trước cả khi user kịp phàn nàn.

