📊 Vận hành, observability và chi phí hạ tầng của Kafka so với BullMQ

Mục tiêu bài học

  • Liệt kê được gánh nặng vận hành thật sự của Kafka (broker, replication, rebalance) và của BullMQ (Redis, RAM, persistence).
  • Chỉ ra được các chỉ số phải giám sát trên từng hệ thống: consumer lag, queue depth, failure rate, stalled jobs.
  • Ước lượng được chi phí hạ tầng của hai lựa chọn bằng vài phép tính đơn giản.
  • Nhận diện được các cạm bẫy vận hành hay gặp và cách phòng tránh.

Ôn tập nhanh

Bài trước chốt một nguyên tắc: phân loại lỗi trước, retry sau. Lỗi transient thì retry kèm backoff, lỗi permanent thì đẩy thẳng vào DLQ. Và vì cả Kafka lẫn BullMQ đều giao hàng theo at-least-once, consumer bắt buộc phải idempotent.

Giờ sang câu hỏi đau ví hơn: nuôi hai thứ đó tốn bao nhiêu, và làm sao biết hệ thống đang khỏe hay đang ốm trước khi khách hàng phát hiện.

Lý thuyết

Gánh nặng vận hành

Nói thẳng: chọn Kafka là bạn nhận thêm một hệ thống phân tán phải nuôi. Bạn phải quản số broker, replication factor, partition count, và cả rebalance mỗi khi consumer join hoặc rời group. Rolling upgrade phải làm từng broker một, đủ chậm để không mất ISR. Disk đầy là sự cố P1, vì broker hết chỗ ghi sẽ ngừng nhận produce. Kafka không phải công cụ bạn bật lên rồi quên.

BullMQ thì gánh nặng dồn hết vào Redis. Nghe nhẹ hơn, nhưng RAM là tài nguyên khan hiếm và đắt: mọi job đang chờ đều nằm trong bộ nhớ. Không bật persistence thì Redis restart là mất sạch job. Đặt sai maxmemory-policy (ví dụ allkeys-lru) thì Redis sẽ tự xóa job khi hết RAM, âm thầm và không báo lỗi. Đúng cấu hình phải là noeviction, thà ghi lỗi còn hơn mất việc.

Observability: đo cái gì?

Nhóm chỉ số Kafka BullMQ
Tồn đọng Consumer lag Queue depth (waiting + delayed)
Sức khỏe hạ tầng Under-replicated partitions, ISR shrink, request latency Redis memory used, evicted keys, event loop lag
Lỗi Error rate của consumer, depth của DLQ topic failed count, stalled count
Thông lượng messages/s, bytes/s trên mỗi broker jobs/s ở trạng thái completed

Hai chỉ số đáng nhớ nhất:

  • Consumer lag của Kafka bằng log end offset trừ committed offset. Đây là chỉ số duy nhất trả lời được câu "consumer có theo kịp producer không".
  • Queue depth là bản tương đương bên BullMQ, vì không có offset để so. Cộng cả delayed vào, nếu không bạn sẽ bỏ sót hàng núi job đang chờ đến giờ mới chạy.

Failure rate thì đo ở tầng ứng dụng, không phải tầng hạ tầng. Tỷ lệ job failed trên tổng job (BullMQ), hoặc tỷ lệ message lỗi và độ sâu DLQ topic (Kafka). Broker khỏe mạnh hoàn toàn có thể đang âm thầm đẩy hàng nghìn message vào DLQ.

1. Kafka đo mức tụt hậu của consumer bằng {{c1::consumer lag}}, tức là log end offset trừ committed offset.
2. Với BullMQ, chỉ số gần tương đương lag là {{c2::queue depth}} (số job ở trạng thái waiting và delayed).
3. Redis dùng cho BullMQ phải đặt maxmemory-policy là {{c3::noeviction}} để không bị xóa job khi hết RAM.
4. Kafka production thường cần tối thiểu {{c4::3 broker}} để đảm bảo replication và chịu lỗi.
5. Thời gian trễ ước lượng bằng {{c5::lag chia cho throughput xử lý của consumer}}.

Chi phí hạ tầng

Hạng mục Kafka BullMQ
Số node tối thiểu 3 broker (cộng controller KRaft) 1 Redis, nên 2-3 nếu cần HA
Tài nguyên đắt nhất Disk và network RAM
Ai vận hành Thường là đội platform Thường là chính app team
Chi phí sàn Cao Thấp
Chi phí biên khi scale Tốt dần theo throughput Xấu dần vì job phải nằm trong RAM

Ví dụ từng bước

Ví dụ 1: đọc lag. Partition có log end offset là 1.000.000, committed offset của consumer group là 985.000. Lag = 1.000.000 - 985.000 = 15.000 message. Consumer xử lý 500 message/s, vậy độ trễ ước lượng là 15.000 / 500 = 30 giây. Con số này mới là thứ bạn đem đi alert, chứ lag thô không nói lên điều gì nếu không biết throughput.

Ví dụ 2: lag tăng dù consumer vẫn chạy. Producer vào 800 message/s, consumer ra 500 message/s. Lag tăng 300 message/s. Sau 10 phút, lag cộng thêm 180.000 message. Cách xử lý không phải là thêm consumer, vì số consumer hiệu dụng bị chặn bởi số partition. Muốn scale, phải tăng partition trước.

Ví dụ 3: chi phí RAM của BullMQ. Mỗi job payload khoảng 5 KB, cộng overhead của BullMQ và Redis khoảng 1,5 lần, thành ~8 KB/job. Đỉnh điểm có 200.000 job đang chờ: 200.000 x 8 KB = 1,6 GB. Cộng vài trăm MB cho BGSAVE và dữ liệu khác nếu dùng chung Redis, bạn cần node 4 GB. Nếu quên removeOnComplete, job đã xong vẫn nằm lại và con số này phình lên mỗi ngày.

Ví dụ 4: chi phí disk của Kafka. Ingress 5 MB/s, một ngày là 5 x 86.400 = 432.000 MB, tức ~432 GB. Retention 7 ngày: ~3 TB. Replication factor 3: ~9 TB tổng, chia 3 broker thành ~3 TB/broker, nên mua disk 4 TB. Đổi retention lên 30 ngày thì nhân 4,3 lần, thành ~13 TB/broker, vượt sức một node. Retention chính là nút vặn hóa đơn lớn nhất của Kafka.

Thảo luận và phản tư

  1. Hệ thống của bạn đang alert trên chỉ số nào? Nếu chỉ alert "broker còn sống" hoặc "Redis ping OK", bạn đang bỏ sót loại sự cố nào?
  2. Nếu queue depth tăng gấp đôi trong 10 phút, bạn phân biệt được producer bùng nổ với consumer bị chậm bằng cách nào?
  3. Với lưu lượng hiện tại của bạn, chi phí Kafka gấp mấy lần BullMQ, và phần chênh đó mua được gì?

Góc chuyên gia

  • Cạm bẫy 1: chọn Kafka vì nghe oai. Kafka trả giá bằng vận hành. Nếu bạn chỉ có vài nghìn job/phút, cần delay, priority, scheduling thì BullMQ thắng rõ ràng.
  • Cạm bẫy 2: quên stalled của BullMQ. Worker nhận job rồi crash, job đó không failed cũng không completed. Không monitor stalled thì job treo im lặng.
  • Cạm bẫy 3: partition skew. 10 partition nhưng mọi message dùng cùng key thì 9 partition rỗng, 1 partition quá tải. Lag trung bình trông đẹp, nhưng độ trễ thật thì thảm.
  • Cạm bẫy 4: dùng chung Redis với cache. Cache bị evict có thể kéo theo job bị evict. Tách instance.
  • Rule of thumb: cần replay, nhiều consumer group độc lập đọc cùng dữ liệu, retention dài, throughput lớn: chọn Kafka. Cần delay, priority, throughput vừa phải, muốn vận hành nhẹ: chọn BullMQ.

Tóm tắt và bước tiếp theo

Kafka đổi lấy khả năng mở rộng và replay bằng gánh nặng vận hành cùng chi phí disk, network, và tối thiểu 3 broker. BullMQ nhẹ và rẻ ở quy mô nhỏ, nhưng chi phí biên xấu dần vì mọi job phải nằm trong RAM Redis. Observability của Kafka xoay quanh consumer lag; của BullMQ xoay quanh queue depth, stalled count và bộ nhớ Redis. Cả hai đều phải đo failure rate ở tầng ứng dụng, không phải tầng hạ tầng.

Thẻ ôn tập

[
  { "front": "Chỉ số quan trọng nhất để biết consumer Kafka có theo kịp producer không?", "back": "Consumer lag." },
  { "front": "Consumer lag được tính thế nào?", "back": "log end offset trừ committed offset." },
  { "front": "BullMQ dùng chỉ số nào thay cho consumer lag?", "back": "Queue depth, tức số job ở trạng thái waiting cộng delayed." },
  { "front": "Cấu hình Redis nào bắt buộc cho BullMQ để tránh mất job?", "back": "maxmemory-policy noeviction, kèm bật persistence (AOF hoặc RDB)." },
  { "front": "Trạng thái stalled trong BullMQ nghĩa là gì?", "back": "Job đã được worker nhận nhưng worker chết, job không hoàn thành và cần được đưa lại hàng đợi." },
  { "front": "Kafka production cần tối thiểu bao nhiêu broker?", "back": "3 broker, để đảm bảo replication và chịu lỗi." },
  { "front": "Under-replicated partitions báo hiệu điều gì?", "back": "Có partition chưa đồng bộ đủ bản sao trong ISR, thường do broker, disk hoặc network có vấn đề." },
  { "front": "Chi phí lớn nhất của Kafka thường đến từ đâu?", "back": "Disk cho retention nhân với replication factor, cộng network và số broker tối thiểu." },
  { "front": "Chi phí lớn nhất của BullMQ thường đến từ đâu?", "back": "RAM của Redis, vì mọi job đang chờ đều nằm trong bộ nhớ." },
  { "front": "Vì sao phải dùng removeOnComplete trong BullMQ?", "back": "Để job đã xong không tích tụ trong Redis và làm phình RAM." },
  { "front": "Công cụ nào xem giao diện queue của BullMQ?", "back": "Bull Board." },
  { "front": "Failure rate nên đo ở tầng nào?", "back": "Tầng ứng dụng: tỷ lệ job failed trên BullMQ, hoặc tỷ lệ message lỗi và depth của DLQ topic trên Kafka." },
  { "front": "Điều gì xảy ra khi consumer Kafka chậm hơn producer?", "back": "Lag tăng liên tục; dữ liệu vẫn còn trong log cho tới khi hết retention." },
  { "front": "Vì sao không nên dùng chung Redis cho cache và BullMQ?", "back": "Cache bị evict có thể kéo theo job bị evict, gây mất việc âm thầm." }
]

Bài sau chúng ta gộp tất cả lại: một case study chọn công cụ và thiết kế kiến trúc hàng đợi hoàn chỉnh, từ mô hình topic/queue tới retry, DLQ, và các chỉ số giám sát đi kèm.