Ôn nhanh BullMQ và bức tranh tổng quan Kafka so với BullMQ
Mục tiêu bài học
- Nhắc lại chính xác mô hình BullMQ: Queue, Job, Worker và vai trò của Redis.
- Mô tả được bức tranh lớn: BullMQ là hàng đợi công việc, Kafka là log sự kiện.
- Nêu được các khác biệt cốt lõi giữa hai công cụ: đơn vị dữ liệu, vòng đời dữ liệu, cách giao việc, khả năng replay.
- Nhận diện nhanh tình huống nào BullMQ là đủ, tình huống nào cần tới Kafka.
Ôn nhanh (khoảng 5 phút)
BullMQ là thư viện hàng đợi cho Node.js, chạy trên Redis. Ba khái niệm bạn đã biết:
- Queue: cái "kệ" chứa job. Bạn
add()job vào queue kèmname,datavàoptions. - Job: một đơn vị công việc cụ thể. Mỗi job có
attempts,backoff,delay,priority,jobId. - Worker: tiến trình pull job ra và chạy hàm xử lý. Worker có tham số
concurrency.
Vòng đời job: waiting → active → completed hoặc failed. Job xong thì bị dọn khỏi Redis (theo removeOnComplete), job fail hết lượt thì nằm lại ở failed set, đó là dead letter queue của bạn.
[!NOTE] Câu thần chú cần nhớ: BullMQ giao mỗi job cho đúng một worker. Đó là toàn bộ sức mạnh và cũng là giới hạn của nó.
Lý thuyết
1. BullMQ: hàng đợi công việc (task queue)
BullMQ trả lời câu hỏi: "Việc này cần được làm". Bạn chỉ đích danh công việc, giao cho một worker, chờ nó xong. Job có thể được hẹn giờ (delay), lặp lại (repeat), ưu tiên (priority), giới hạn tốc độ (rate limit). Khi job xong, nó biến mất. Muốn chạy lại thì phải add() lại từ đầu.
2. Kafka: log sự kiện (distributed log)
Kafka trả lời câu hỏi: "Chuyện này đã xảy ra". Producer ghi record vào một topic, topic được chia thành nhiều partition, mỗi record nằm ở một offset tăng dần. Consumer đọc record theo offset và commit offset để đánh dấu đã đọc tới đâu. Record không bị xóa khi đọc, nó chỉ bị xóa theo thời gian hoặc dung lượng (retention). Nhiều consumer group khác nhau đọc cùng một topic một cách độc lập.
3. Bốn khác biệt cốt lõi
| Khía cạnh | BullMQ | Kafka |
|---|---|---|
| Đơn vị dữ liệu | Job (việc cần làm) | Record (sự kiện đã xảy ra) |
| Vòng đời | Xong là xóa | Giữ theo retention, đọc lại được |
| Cách giao việc | Mỗi job đúng một worker | Mỗi partition giao cho một consumer trong group |
| Nhiều bên cùng quan tâm | Muốn fan-out phải tự nhân bản hoặc tạo nhiều queue | Nhiều consumer group đọc chung một log |
| Replay | Không (job đã xóa là mất) | Có (seek về offset cũ) |
| Hạ tầng | Một Redis | Cluster Kafka (KRaft hoặc Zookeeper) |
flowchart LR
subgraph bull["BullMQ: hàng đợi công việc"]
P1["📥<br/>Producer add job"] --> Q1["🗂️<br/>Queue trong Redis"]
Q1 --> W1["⚙️<br/>Worker A"]
Q1 --> W2["⚙️<br/>Worker B"]
W1 --> D1["🗑️<br/>Job xong thì bị xóa"]
end
subgraph kaf["Kafka: log sự kiện"]
P2["📥<br/>Producer send record"] --> T1["📜<br/>Topic orders"]
T1 --> C1["📧<br/>Consumer group email"]
T1 --> C2["📊<br/>Consumer group analytics"]
T1 --> R1["💾<br/>Record giữ theo retention"]
end
Ví dụ có lời giải
Ví dụ 1: Gửi email xác nhận đơn hàng bằng BullMQ
- Tạo queue:
const emailQueue = new Queue('email', { connection }). - Khi đơn hàng được tạo, đẩy job vào:
await emailQueue.add('send-confirmation', { orderId, email }, {
jobId: `order-${orderId}-email`, // chống add trùng
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: 1000,
removeOnFail: 5000,
});
- Worker xử lý:
new Worker('email', handler, { connection, concurrency: 10 }). - Diễn biến: job
waiting→ worker pull → gửi mail thành công →completed→ bị dọn khỏi Redis (giữ lại 1000 job gần nhất). - Nếu SMTP lỗi: job retry sau 2s, rồi 4s, rồi 8s. Hết 3 lượt, job nằm ở
failed. Bạn đọcqueue.getFailed()để xử lý thủ công.
Điểm đáng chú ý ở bước 2: jobId trùng thì BullMQ bỏ qua job mới. Đây là cách chống trùng đơn giản nhất, nhưng nó chỉ hiệu lực trong khoảng thời gian job còn tồn tại.
Ví dụ 2: Cùng nghiệp vụ đó trên Kafka
- Producer gửi record
{ type: 'OrderPlaced', orderId, email }vào topicorders, dùngorderIdlàm key. - Topic có 3 partition. Consumer group
email-servicecó 3 consumer, mỗi consumer nhận đúng một partition và đọc record theo offset. - Email service gửi mail rồi commit offset.
- Sáu tháng sau, team loyalty muốn đếm điểm khách hàng. Họ tạo consumer group
loyalty-servicevà đọc topicorderstừ offset 0. Không cần sửa producer, không cần copy dữ liệu. - Nếu một consumer của
email-servicechết giữa lúc gửi mail nhưng chưa commit offset, group sẽ rebalance và record đó được đọc lại → email gửi trùng. Phải chống trùng bằngorderIdở tầng nghiệp vụ.
Ví dụ 3: Tính nhanh năng lực hai bên
Giả sử mỗi job mất 50ms. Một worker BullMQ với concurrency: 10 cho khoảng 10 / 0.05 = 200 job/giây. Cần 500 job/giây thì tăng concurrency hoặc thêm worker, khá linh hoạt.
Với Kafka, topic 3 partition thì một consumer group chỉ chạy song song tối đa 3 consumer. Muốn gấp đôi throughput, bạn phải tăng số partition, và không giảm lại được. Đây chính là chủ đề bài sau sẽ mổ xẻ.
Thảo luận và suy ngẫm
- Hệ thống của bạn đang có job gửi hóa đơn lúc 2 giờ sáng. Nếu đẩy sang Kafka, bạn sẽ làm phần hẹn giờ ở đâu?
- Có hai service cùng cần dữ liệu
OrderPlaced. Nếu dùng BullMQ, bạn sẽ nhân bản job hay tạo hai queue? Đánh đổi là gì? - Nếu Kafka giữ record 7 ngày, chuyện gì xảy ra với consumer bị tắt 10 ngày?
Góc chuyên gia
- Cạm bẫy 1: chọn Kafka vì "nghe mạnh hơn". Kafka không có
delay,priority,repeat, rate limit theo queue. Cron 5 phút một lần thì BullMQ thắng tuyệt đối về chi phí và độ đơn giản. - Cạm bẫy 2: tưởng Kafka là queue nhiều consumer. Thực tế trong một consumer group, mỗi record chỉ được một consumer xử lý. Muốn nhiều bên cùng nhận thì phải tạo nhiều group.
- Cạm bẫy 3: tưởng BullMQ là exactly-once. Job bị
stalledvẫn được đưa lại cho worker khác. Cả hai công cụ mặc định đều at-least-once, nên handler luôn phải idempotent. - Cạm bẫy 4: để
removeOnComplete: false. Redis sẽ phình dần cho tới khi bạn hết RAM. Luôn đặt giới hạn số job giữ lại. - Quy tắc ngón tay cái: việc cần được làm, có đích danh, có deadline, có retry ngắn hạn → BullMQ. Sự kiện cần được lan truyền, nhiều hệ thống quan tâm, cần đọc lại lịch sử → Kafka.
Tóm tắt và bước tiếp theo
BullMQ là hàng đợi công việc trên Redis: job được giao cho đúng một worker và bị xóa khi xong. Kafka là log sự kiện phân tán: record được giữ lại, nhiều consumer group đọc độc lập, đọc lại được. Cùng là "hàng đợi" nhưng một bên quản lý công việc, một bên quản lý lịch sử sự kiện.
Kiểm tra nhanh trí nhớ:
BullMQ lưu toàn bộ job trong {{c1::Redis}}.
Sau khi worker xử lý xong, job của BullMQ thường bị {{c2::xóa}} khỏi queue.
Kafka lưu record trong một {{c3::log}} append-only và chỉ xóa theo chính sách {{c4::retention}}.
Trong BullMQ, mỗi job chỉ được giao cho đúng {{c5::một}} worker.
Nhiều {{c6::consumer group}} có thể đọc cùng một topic Kafka mà không ảnh hưởng lẫn nhau.
Ôn lại bằng bộ flashcard dưới đây trước khi sang bài sau:
[
{ "front": "BullMQ lưu trữ dữ liệu hàng đợi ở đâu?", "back": "Trong Redis." },
{ "front": "Ba thành phần chính của BullMQ là gì?", "back": "Queue, Job và Worker." },
{ "front": "Chuyện gì xảy ra với job BullMQ sau khi xử lý xong?", "back": "Job bị dọn khỏi Redis theo cấu hình removeOnComplete." },
{ "front": "Đơn vị dữ liệu trong Kafka là gì?", "back": "Record (event) nằm trong một partition của topic, tại một offset." },
{ "front": "Kafka xóa record khi nào?", "back": "Theo chính sách retention (thời gian hoặc dung lượng), không phải khi đọc xong." },
{ "front": "Trong một consumer group Kafka, một record được giao cho bao nhiêu consumer?", "back": "Đúng một consumer trong group đó." },
{ "front": "Vì sao nhiều hệ thống khác nhau có thể đọc chung một topic Kafka?", "back": "Vì log được giữ lại; mỗi consumer group đọc độc lập theo offset riêng." },
{ "front": "BullMQ hỗ trợ retry bằng cách nào?", "back": "Cấu hình attempts và backoff trên từng job." },
{ "front": "BullMQ có replay lại job đã hoàn thành không?", "back": "Không. Job đã bị xóa thì phải add lại từ đầu." },
{ "front": "Hai công cụ đảm bảo delivery semantics nào theo mặc định?", "back": "At-least-once, nên consumer cần idempotency." },
{ "front": "Thứ tự record trong Kafka được đảm bảo ở phạm vi nào?", "back": "Chỉ trong một partition; muốn cùng thứ tự theo key thì dùng key làm khóa partition." },
{ "front": "Điểm khác biệt cốt lõi giữa BullMQ và Kafka trong một câu?", "back": "BullMQ quản lý công việc cần làm và xóa khi xong; Kafka giữ lịch sử sự kiện để nhiều bên đọc lại." },
{ "front": "Kafka có sẵn tính năng delay job hoặc priority queue không?", "back": "Không có sẵn; BullMQ có delay, priority, repeat và rate limit." }
]
Bài tiếp theo sẽ mổ xẻ Kafka ở tầng chi tiết: topic, partition, offset và consumer group, kèm ví dụ từng bước để bạn thấy chính xác điều gì xảy ra khi một consumer join hoặc rời group.