Đừng để Worker "đốt" CPU: Giải phẫu cơ chế Dequeue và ma thuật BRPOP/BLPOP trong Redis
Chào anh em cộng đồng Viblo
Chúng ta thường tung hô Message Queue (như Redis, RabbitMQ) là thần dược cứu rỗi mọi hệ thống cao. Ai cũng biết cách đẩy (Push/Enqueue) một cái Job và Queue. Nhưng ở đầu bên kia chiến tuyến, nơi các Queue Worker đang thầm lặng làm việc, một cuộc chiến khốc liệt về tài nguyên CPU đang diễn ra.
Nếu bạn viết một con Worker để lấy dữ liệu ra (Dequeue) mà không hiểu rõ cách hệ điều hành và Redis giao tiếp, khả năng cao là con Worker đó sẽ ngốn sạch 100% CPU của Server chỉ để... đứng chơi xơi nước.
Hôm nay chúng ta sẽ bóc trần cơ chế Dequeue, nguyên lý "từng cái một", và tuyệt chiêu tối thượng mang tên BRPOP/BLPOP mà các framework lớn đang âm thầm sử dụng. Lên xe thôi
1. Cơ chế Dequeue (Lấy ra) là gì?
Nếu Enqueue là hành động ném một túi hồ sơ vào khay giấy in, thì Dequeue là hành động anh nhân viên bốc túi hồ sơ đó ra để xử lý.
Về mặt kỹ thuật, Worker là một tiến trình (Process) chạy nền liên tục. Nó mở một kết nối (Connection) dài hạn tới Queue Broker (Redis, RabbitMQ, hoặc SQL Database) và liên tục gửi yêu cầu: "Ê, có việc gì mới cho tôi làm không?".
Trong các hệ thống thực tế như xử lý giao dịch vé tàu điện (AFC Gate) hay đồng bộ dữ liệu tài chính, tốc độ Dequeue quyết định trực tiếp đến độ trễ (Latency) của toàn bộ hệ thống.
2. Nguyên lý "Từng cái một" (One by One)
Khi nhìn vào một Queue đang bị ùn ứ hàng triệu task, nhiều anh em fresher sẽ nảy ra ý tưởng: "Tại sao Worker không lấy luôn một lúc 100 task ra xử lý cho nhanh?"
Thực tế, các Queue Worker xịn xò đều được thiết kế với triết lý: Lấy từng cái một, xử lý xong, xóa khỏi Queue, rồi mới lấy cái tiếp theo.
Tại sao lại phải bảo thủ như vậy?
Đó là để đảm bảo Tính nguyên tử (Atomicity) và Khả năng phục hồi (Resilience).
Giả sử Worker bốc ra 100 giao dịch cần đồng bộ. Xử lý được 50 cái thì... mất điện (Server crash).
50 giao dịch còn lại đang nằm trên RAM của Worker sẽ bốc hơi vĩnh viễn, trong khi Redis thì tưởng Worker đã nhận hàng rồi nên xóa luôn trong Queue.
Bằng cách lấy "Từng cái một", nếu Worker chết đột tử, cùng lắm chúng ta chỉ bị rớt đúng 1 task đang chạy dở. Các task khác vẫn nằm an toàn trong Redis chờ một Worker khác (hoặc chính nó sau khi khởi động lại) bốc ra xử lý. An toàn dữ liệu luôn là số 1!
3. Nỗi đau thực tế: Vòng lặp "Đốt" CPU (Polling)
Bây giờ, hãy thử đóng vai một gã thợ hàn tự tay viết một con Worker bằng PHP để bốc data từ Redis ra thông qua lệnh LPOP (Lấy phần tử đầu tiên của List).
Code "Ngây thơ" (Polling - Hỏi liên tục):
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
echo "Worker đang chạy...\n";
while (true) {
// LPOP: Lấy ra 1 task. Nếu queue rỗng, trả về null.
$task = $redis->lPop('afc_transactions_queue');
if ($task) {
// Có việc! Xử lý ngay.
processTransaction($task);
} else {
// KHÔNG CÓ VIỆC?
// Bỏ qua, vòng lặp while lập tức quay lại hỏi tiếp!
}
}
Thảm họa xảy ra:
Vì vòng lặp while (true) không có điểm dừng, nếu Queue rỗng (đêm khuya không có khách), con Worker này sẽ hỏi Redis hàng chục ngàn lần MỖI GIÂY: "Có việc chưa? Có việc chưa? Có việc chưa?".
Kết quả: CPU của máy chạy Worker vọt lên 100%, và CPU của máy chủ Redis cũng sùi bọt mép vì phải trả lời "Chưa" hàng chục ngàn lần.
Nhiều người "chữa cháy" bằng cách thêm sleep(1) vào hàm else. Cách này giảm CPU, nhưng lại tạo ra độ trễ (Latency). Task mới vừa ném vào phải đợi tối đa 1 giây sau Worker mới thức dậy để xử lý.
4. Cứu tinh xuất hiện: Ma thuật BRPOP / BLPOP
Những kỹ sư tạo ra Redis hiểu rất rõ nỗi đau này. Và họ sinh ra một lệnh tối thượng: BLPOP (Block Left Pop) và BRPOP (Block Right Pop).
Chữ Block (Chặn) ở đây mang ý nghĩa phép màu: "Nếu Queue đang có hàng, lấy ra ngay lập tức. Nhưng nếu Queue rỗng, hãy ĐÓNG BĂNG (Block) cái connection này lại, Worker cứ đi ngủ đi, KHÔNG TỐN MỘT TÍ CPU NÀO. Bao giờ có người ném task mới vào Queue, Redis sẽ CHỦ ĐỘNG ĐÁNH THỨC Worker dậy làm việc."
Code "Thực chiến" bằng BLPOP:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1); // Tắt timeout của PHP socket
echo "Worker xịn xò đang chạy...\n";
while (true) {
// BLPOP: Đợi ở queue 'afc_transactions_queue'.
// Số 0 ở cuối nghĩa là: Đợi Mãi Mãi (Vô tận) cho đến khi có hàng.
$result = $redis->blPop(['afc_transactions_queue'], 0);
// Khi chạy đến dòng này, code sẽ DỪNG LẠI HOÀN TOÀN nếu queue rỗng.
// Ngay khi có ai đó push data vào queue, dòng này lập tức trả về kết quả!
$task = $result[1]; // BLPOP trả về mảng [tên_queue, giá_trị]
processTransaction($task);
}
Bằng cách sử dụng BLPOP, CPU của Worker sẽ giảm xuống gần bằng 0% khi rảnh rỗi. Và ngay khi có giao dịch mới (VD: khách quẹt thẻ), Worker sẽ thức dậy và xử lý trong chưa tới 1 mili-giây. Tuyệt đối không có độ trễ (Zero Latency)!
Đây chính xác là cách mà phần lõi của các framework đình đám như Laravel Queue (file RedisQueue.php), hay Sidekiq (của Ruby) đang hoạt động bên dưới lớp vỏ bọc hào nhoáng.
5. Kinh nghiệm "Hạng Nặng" cho Senior
Tuyệt chiêu: Xử lý đa hàng đợi với Độ ưu tiên (Priority) Một điểm bá đạo nữa của lệnh BLPOP là nó có thể lắng nghe nhiều Queue cùng một lúc. Redis sẽ kiểm tra theo thứ tự từ trái sang phải.
Giả sử bạn có 2 loại giao dịch: Giao dịch nạp tiền (Cần xử lý gấp - High Priority) và Giao dịch đồng bộ log (Xử lý lúc nào cũng được - Low Priority).
Bạn chỉ cần ra lệnh:
$result = $redis->blPop(['high_priority_queue', 'low_priority_queue'], 0);
Redis sẽ luôn luôn quét high_priority_queue trước. Kể cả khi low_priority_queue đang có 1 triệu task chờ, mà high_priority_queue có 1 task chen ngang vào, Redis lập tức đánh thức Worker và nhả cái task High Priority ra cho bạn xử lý.
Việc thiết lập hệ thống ưu tiên Queue (Priority Queue) trở nên dễ dàng và chuẩn xác tuyệt đối ở cấp độ engine C++ của Redis, thay vì phải viết logic if-else lằng nhằng trên tầng Application PHP.
Lời kết
Việc hiểu sâu xuống tầng giao thức mạng và các tập lệnh gốc (Native Commands) của các hệ thống như Redis sẽ cho bạn một "siêu năng lực" khi tối ưu hệ thống. Khi bạn biết BLPOP hoạt động thế nào, bạn sẽ hiểu tại sao chạy Queue bằng Redis lại có độ trễ thấp hơn rất nhiều so với dùng SQL Database (phải liên tục SELECT / Polling).
Lần tới, khi setup một cụm Worker để "nhai" hàng triệu data, hãy an tâm rằng đằng sau nó là những cơ chế Block thần thánh đang miệt mài bảo vệ CPU cho server của bạn.
Chúc anh em code mượt mà, server lúc nào cũng mát mẻ!
All rights reserved