series Bee Queue Bài 4: Xử Lý Stalled Jobs (Job Bị Kẹt) Và Tuyệt Chiêu Tối Ưu Bộ Nhớ Redis
Hãy tưởng tượng một kịch bản ác mộng: Đội DevOps tiến hành deploy code mới và ép các Worker cũ phải tắt ngay lập tức bằng lệnh kill -9, hoặc server của bạn đột ngột bị mất điện. Ngay tại thời điểm đó, Worker đang xử lý dở một Job cực kỳ quan trọng (ví dụ: trừ tiền trong tài khoản hoặc xuất hóa đơn).
Vì Worker chết đột ngột, nó không kịp báo về cho Redis là Job này thất bại hay thành công. Trên hệ thống, Job đó sẽ mãi mãi nằm ở trạng thái "Đang xử lý" (Active). Đây chính là Stalled Job (Job bị thối/kẹt).
Nếu không có cơ chế giải cứu, đoạn code đó sẽ bị đóng băng vĩnh viễn, dữ liệu của khách hàng sẽ bị treo.
1. Cơ Chế "Nhịp Đập Trái Tim" (Heartbeat) Để Phát Hiện Job Kẹt
Để giải quyết bài toán trên, Bee-Queue sử dụng một cơ chế rất thông minh gọi là Heartbeat (Nhịp đập trái tim).
Khi một Worker nhận một Job về làm, nó phải liên tục gửi tín hiệu "Tôi còn sống" về cho Redis sau những khoảng thời gian cố định. Nếu quá thời gian quy định mà Redis không nhận được tín hiệu từ Worker đó, Bee-Queue sẽ hiểu rằng: "Worker này đã chết lâm sàng rồi, hãy giải cứu cái Job này ngay!".
Ngay lập tức, Bee-Queue sẽ thu hồi Job đó, chuyển trạng thái từ Active về lại Failed hoặc ném ngược lại vào hàng đợi để một Worker khác khỏe mạnh hơn vào cấu rỉa và xử lý tiếp.
Cấu hình giải cứu Job kẹt tại Worker:
const Queue = require('bee-queue');
const emailQueue = new Queue('email-queue', {
redis: { host: '127.0.0.1', port: 6379 },
// 1. Khoảng thời gian Worker phải gửi tín hiệu "còn sống" về Redis (mặc định 5000ms)
stallInterval: 5000,
});
// Kích hoạt tính năng chủ động lùng sục và check xem có Job nào bị kẹt không
emailQueue.checkStalledJobs(10000).then((numStalled) => {
if (numStalled > 0) {
console.log(`[SYSTEM] Phát hiện và giải cứu thành công ${numStalled} Job bị kẹt!`);
}
});
Lời khuyên từ thực tế: Bạn nên gọi hàm checkStalledJobs bằng một hàm setInterval chạy định kỳ (ví dụ cứ mỗi 30 giây hoặc 1 phút check một lần) để đảm bảo không một Job nào bị bỏ lại phía sau quá lâu sau một sự cố sập server.
2. Sát Thủ Thầm Lặng: Cạn Kiệt RAM Redis (Out Of Memory)
Mặc định, khi một Job được Worker xử lý thành công hoặc thất bại, Bee-Queue vẫn giữ nguyên dữ liệu của Job đó trong Redis để bạn có thể vào tra cứu lịch sử.
Bản chất của Redis là lưu dữ liệu hoàn toàn trên RAM. Nếu hệ thống của bạn High-Traffic, mỗi ngày bắn ra 1 triệu Job, thì chỉ sau vài tuần, đống "lịch sử" này sẽ phình to ra hàng Gigabyte, nuốt sạch RAM của server và khiến Redis kích hoạt cơ chế tự hủy (OOM và từ chối mọi lệnh ghi mới).
Để không biến Redis thành một bãi rác công nghệ, bạn bắt buộc phải cấu hình chiến lược tự động dọn dẹp nhà cửa ngay khi tạo hàng đợi.
Cấu hình xóa sạch dấu vết tại Producer/Worker:
const emailQueue = new Queue('email-queue', {
redis: { host: '127.0.0.1', port: 6379 },
// TỰ ĐỘNG DỌN DẸP BỘ NHỚ
removeOnSuccess: true, // Nếu Job chạy thành công, xóa sạch dữ liệu khỏi Redis ngay lập tức
removeOnFailure: false // Nếu Job thất bại, GIỮ LẠI để Admin xem lỗi và tìm cách fix
});
- removeOnSuccess: true: Đây là cấu hình bắt buộc phải có trên Production đối với các tác vụ thông thường. Bạn không cần giữ lại lịch sử các gói tin gửi mail thành công làm gì, hãy để RAM trống cho các tác vụ khác.
- removeOnFailure: false: Khuyên dùng để giữ lại. Vì khi Job lỗi, ta cần biết lý do tại sao lỗi để debug. Tuy nhiên, định kỳ cuối tuần hoặc cuối tháng, bạn nên dùng script để clear dứt điểm các Job lỗi quá cũ này.
3. Tuyệt Chiêu Hủy Hàng Đợi An Toàn (Graceful Shutdown)
Để giảm thiểu tối đa việc sinh ra các Stalled Jobs ngay từ đầu, khi bạn cần bảo trì hệ thống hoặc deploy code mới, đừng bao giờ dùng lệnh tắt thô bạo. Hãy hướng dẫn Worker của bạn cách "Hạ cánh an toàn".
Nghĩa là: Khi nhận được tín hiệu tắt từ hệ thống, Worker sẽ ngừng nhận Job mới, cố gắng giải quyết cho xong các Job đang làm dở, rồi mới thong thả đóng kết nối và tắt hẳn.
Hãy chèn đoạn code này vào cuối file worker.js của bạn:
// Lắng nghe tín hiệu tắt từ hệ điều hành (khi bấm Ctrl+C hoặc khi deploy)
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
async function gracefulShutdown() {
console.log('[SHUTDOWN] Đang tiến hành đóng Worker an toàn...');
try {
// Lệnh .close() sẽ đợi tất cả các Job đang active chạy xong, dừng nhận Job mới, rồi ngắt kết nối Redis
await emailQueue.close();
console.log('[SHUTDOWN] Toàn bộ hệ thống Worker đã đóng êm đẹp. Tạm biệt!');
process.exit(0);
} catch (error) {
console.error('[SHUTDOWN] Có lỗi khi đóng hệ thống:', error);
process.exit(1);
}
}
Tóm lại là...
Một hệ thống Background Job mạnh mẽ không chỉ nằm ở tốc độ, mà nằm ở độ bền bỉ. Bằng cách kết hợp cơ chế giải cứu Stalled Jobs, bật cấu hình tự dọn dẹp bộ nhớ removeOnSuccess và viết lệnh Graceful Shutdown, bạn đã nâng cấp hệ thống Bee-Queue của mình lên mức độ chín muồi nhất, sẵn sàng đương đầu với mọi sự cố phần cứng hay những đợt bảo trì hệ thống mà không sợ mất mát dữ liệu.
Bài học tiếp theo cũng là bài học cuối cùng để khép lại series Bee-Queue này: Xây dựng Dashboard trực quan bằng Arena để giám sát hàng đợi theo thời gian thực và tổng hợp các Best Practices tối thượng khi vận hành Job Queue.
All rights reserved