Kafka Consumer Groups: Chia việc, Offset và Rebalancing
“Simplicity is a prerequisite for reliability.” – Edsger W. Dijkstra
Nội dung bài viết
Hi các bạn 👋. Mình là Hùng Anh.
Trong bài Message Broker mình đã giới thiệu tổng quan về Kafka. Hôm nay đi sâu vào một mảnh ghép mà theo mình là quan trọng nhất để vận hành Kafka đúng cách: consumer group.
Thử một câu hỏi kiểm tra nhanh: topic orders có 4 partition, bạn bật 5 consumer cùng một group để "xử lý cho nhanh". Kết quả? Một consumer ngồi chơi hoàn toàn — và nếu không hiểu vì sao, sớm muộn bạn cũng sẽ gặp những chuyện khó chịu hơn: message xử lý trùng, group đứng hình vài giây mỗi lần deploy, hay lag tăng vọt không rõ lý do.
Bắt đầu thôi.
Consumer group là gì?
Một consumer group là một tập consumer cùng chia nhau đọc một topic, nhận diện bằng group.id. Kafka áp một luật duy nhất nhưng quyết định tất cả:
Trong một group, mỗi partition thuộc về đúng một consumer. Một consumer có thể giữ nhiều partition, nhưng một partition không bao giờ bị hai consumer trong cùng group đọc song song.
Luật này cho bạn hai chế độ tiêu thụ chỉ bằng cách đặt group.id:
- Chia việc (work queue): các instance của cùng một service dùng chung một
group.id→ mỗi message chỉ một instance xử lý. Thêm instance = chia việc nhỏ hơn = nhanh hơn. - Phát cho tất cả (pub/sub): các service khác nhau dùng
group.idkhác nhau → mỗi service đều nhận đủ toàn bộ message. Service billing và service notification cùng đọcorders, không giẫm chân nhau.
Và hệ quả của luật partition-một-chủ: số consumer hữu ích tối đa = số partition. 4 partition thì con thứ 5 thất nghiệp — đó là câu trả lời cho câu hỏi đầu bài. Muốn scale ngang sâu hơn, phải tăng partition trước, tăng consumer sau.
Offset: cuốn sổ đánh dấu "đọc đến đâu"
Mỗi message trong một partition có một số thứ tự tăng dần gọi là offset. Consumer đọc tới đâu thì commit offset tới đó vào một topic nội bộ của Kafka (__consumer_offsets) — như kẹp bookmark vào sách. Consumer chết đi sống lại, hoặc partition được giao cho consumer khác, thì cứ mở bookmark ra đọc tiếp.
Điểm bẫy nằm ở thời điểm commit:
# mặc định: tự commit mỗi 5 giây
enable.auto.commit=true
auto.commit.interval.ms=5000
Auto-commit tiện nhưng mơ hồ: nó commit theo đồng hồ, không theo tiến độ xử lý thật. Hai tai nạn đối xứng nhau:
- Commit trước khi xử lý xong → consumer crash giữa chừng → message coi như đã đọc → mất message.
- Xử lý xong nhưng chưa kịp commit → crash → consumer mới đọc lại từ offset cũ → xử lý trùng.
Thực tế, Kafka mặc định cho bạn ngưỡng an toàn at-least-once (không mất, có thể trùng) nếu bạn commit sau khi xử lý xong:
while (true) {
ConsumerRecords<String, Order> records = consumer.poll(Duration.ofMillis(200));
for (ConsumerRecord<String, Order> r : records) {
process(r.value()); // 1. xử lý xong trước
}
consumer.commitSync(); // 2. rồi mới commit
}
Vế còn lại — "có thể trùng" — thì xử ở tầng nghiệp vụ: làm cho việc xử lý idempotent (khoá duy nhất theo order_id, INSERT ... ON DUPLICATE KEY, kiểm tra trạng thái trước khi cập nhật…). Công thức mình luôn dùng: commit-sau-xử-lý + consumer idempotent — đơn giản, đúng cho 95% hệ thống.
Rebalancing: chia lại bài khi có người vào/ra
Khi thành viên của group thay đổi — một consumer mới join, một con crash, hay chỉ là deploy lại service — Kafka phải chia lại partition cho các thành viên còn lại. Quá trình đó gọi là rebalance.
Rebalance là tính năng sống còn (không có nó thì consumer chết là partition mồ côi), nhưng cũng là nguồn đau đầu vận hành số một, vì kiểu rebalance cổ điển (eager) bắt toàn bộ group ngừng đọc trong lúc chia lại — người ta gọi là "stop the world". Group to, deploy rolling 20 instance = 20 lần cả thế giới khựng lại.
Ba điều đáng biết để bớt đau:
- Cooperative rebalancing: từ Kafka 2.4, đặt
partition.assignment.strategy=CooperativeStickyAssignor— chỉ những partition thực sự đổi chủ mới tạm dừng, phần còn lại đọc tiếp bình thường. Gần như luôn nên bật cho consumer mới viết. - Static membership: đặt
group.instance.idcố định cho mỗi instance — restart nhanh (trongsession.timeout.ms) sẽ không kích hoạt rebalance, cực hữu ích cho rolling deploy. - Đừng để bị đá oan: Kafka đá consumer khỏi group nếu quá
max.poll.interval.ms(mặc định 5 phút) mà không gọipoll(). Batch xử lý quá lâu → bị đá → rebalance → message chia lại cho con khác → lại xử lý trùng. Chỉnhmax.poll.recordsnhỏ lại hoặc tăngmax.poll.interval.mscho khớp thời gian xử lý thật.
Consumer lag: chỉ số sức khoẻ số một
Lag của một partition = offset mới nhất − offset đã commit — tức số message đang xếp hàng chờ. Lag tăng đều nghĩa là consumer xử lý không kịp producer; lag nhảy vọt sau deploy thường là dấu hiệu rebalance có vấn đề.
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group billing
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# orders 0 51200 51230 30
# orders 1 48100 52400 4300 ← con này có chuyện
Lag lệch hẳn ở một partition thường chỉ về hai thủ phạm: partition key bị lệch (một key quá nóng dồn hết vào một partition) hoặc một consumer cụ thể đang ốm. Đây là metric đáng đặt cảnh báo nhất trong toàn bộ hệ Kafka của bạn.
Kết luận
Gói lại những điều đáng nhớ:
- Partition là đơn vị song song — số consumer hữu ích tối đa bằng số partition; scale partition trước, consumer sau.
- Chung group = chia việc, khác group = mỗi bên nhận đủ — một cái
group.idquyết định cả mô hình tiêu thụ. - Commit sau xử lý + idempotent — công thức at-least-once thực dụng cho đa số hệ thống.
- Rebalance là bạn, "stop the world" là kẻ thù — CooperativeStickyAssignor + static membership +
max.poll.interval.mschỉnh đúng. - Theo dõi lag — chỉ số sớm nhất báo cho bạn biết pipeline đang tụt.
Hẹn gặp lại các bạn ở những bài viết sau. Nếu thấy bài viết hữu ích, đừng quên chia sẻ và để lại bình luận nhé 👇.
Tài liệu tham khảo
- Apache Kafka Documentation — Consumer Configs
- Apache Kafka Documentation — Design: The Consumer
- KIP-429: Kafka Consumer Incremental Rebalance Protocol
- Đọc thêm: loạt bài Kafka trên blog.algomaster.io và chuyên mục message queue của xiaolincoding.com
Bài viết liên quan
Transaction Isolation Levels: 4 nấc thang cô lập trong MySQL
Hai transaction cùng đọc–ghi một dòng dữ liệu thì chuyện gì xảy ra? Tuỳ vào isolation level bạn chọn, cùng một câu SELECT có thể trả về những kết quả rất khác nhau. Bài này mình giải thích 3 hiện tượng đọc "bẩn", 4 mức cô lập của SQL, và cách InnoDB hiện thực chúng bằng MVCC — kèm ví dụ chạy được để bạn tự kiểm chứng.
Cache với Redis: Cache-Aside và 3 sự cố kinh điển
Thêm một lớp cache Redis trước database nghe thì đơn giản: có thì trả, không có thì xuống DB lấy rồi cất lại. Nhưng đằng sau pattern quen thuộc đó là ba cái bẫy đã đánh sập không ít hệ thống lớn: penetration, breakdown và avalanche. Bài này mình giải thích pattern Cache-Aside cho đúng, vì sao dữ liệu cache và DB có thể lệch nhau, và cách phòng cả ba sự cố.
Distributed Lock và Cách Triển Khai với Redis
Trong các hệ thống phân tán, việc đảm bảo tính nhất quán của dữ liệu (data consistency) và ngăn chặn tranh chấp tài nguyên (race condition) là một thách thức lớn, đặc biệt khi nhiều tiến trình hoặc service truy cập đồng thời vào các tài nguyên dùng chung. Distributed lock là giải pháp hiệu quả để xử lý vấn đề này. Bài viết này mình sẽ giúp bạn hiểu rõ distributed lock là gì, tại sao nó cần thiết, các phương pháp để thực hiện và cách triển khai nó với Redis nhé.
