Commit offset Kafka: trước hay sau side effect, và auto.offset.reset Trung cấp
Thời gian đọc ước tính: 6 phút
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”Consumer đọc event offset 7, cập nhật DB rồi báo Kafka đã xong. Process có thể chết giữa bất kỳ hai thao tác nào, và Kafka offset với DB là hai hệ thống, không có transaction chung. Vì vậy chỉ có hai lựa chọn: chấp nhận mất hoặc chấp nhận lặp.
Khuyến nghị mặc định: ghi side effect trước, commit offset sau (at-least-once), và làm side effect idempotent. Mất nghiệp vụ thường đắt hơn xử lý lại.
Hai thứ tự, hai kiểu hỏng
Phần tiêu đề “Hai thứ tự, hai kiểu hỏng”Cả hai consumer cùng nhận record offset 7 (offset đã commit của group đang là 7).
| Thứ tự | Crash ngay sau bước đầu | Khi restart | Cần gì |
|---|---|---|---|
| Ghi DB rồi commit 8 | DB đã ghi, group còn 7 | Record 7 giao lại | Idempotent write |
| Commit 8 rồi ghi DB | Group đã 8, DB chưa ghi | Record 7 bị bỏ qua | Không sửa được nếu không replay thủ công |
Giá trị commit luôn là offset sẽ đọc tiếp: xử lý xong 7 thì commit 8.
Cheat-sheet config consumer (Java client 4.2)
Phần tiêu đề “Cheat-sheet config consumer (Java client 4.2)”| Config | Mặc định | Khuyến nghị |
|---|---|---|
enable.auto.commit |
true |
false khi side effect không thể mất; tự commit sau khi xử lý |
auto.commit.interval.ms |
5000 | Chỉ liên quan khi auto-commit bật |
auto.offset.reset |
latest |
earliest cho consumer cần đủ dữ liệu; none khi muốn lỗi to thay vì đoán |
max.poll.records |
500 | Giảm nếu mỗi record xử lý lâu |
max.poll.interval.ms |
300000 (5 phút) | Phải lớn hơn thời gian xử lý một batch (rebalance) |
isolation.level |
read_uncommitted |
read_committed khi đọc topic có transaction (transactions) |
offsets.retention.minutes (broker) |
10080 (7 ngày) | Theo dõi nếu group có thể nghỉ lâu |
Auto-commit trong Java client commit định kỳ theo auto.commit.interval.ms các offset mà poll() đã trả về. Javadoc KafkaConsumer ghi rõ: auto-commit chỉ cho at-least-once nếu bạn xử lý xong toàn bộ record của mỗi lần poll() trước lần gọi tiếp theo hoặc trước khi đóng consumer.
- Xử lý đồng bộ trong vòng lặp: at-least-once.
- Chuyển record sang thread/queue khác rồi quay lại poll: offset có thể được commit trước khi việc xong (mất việc khi crash).
KafkaJS có autoCommit, resolveOffset, commitOffsetsIfNecessary với hành vi riêng, cần test crash window với đúng phiên bản.
auto.offset.reset: nguyên nhân thật của sự cố
Phần tiêu đề “auto.offset.reset: nguyên nhân thật của sự cố”Config này chỉ có tác dụng khi group không có offset hợp lệ: group mới, offset hết hạn sau offsets.retention.minutes (group rỗng, không còn consumer nào, quá 7 ngày), hoặc offset trỏ vào dữ liệu đã bị retention xoá.
| Giá trị | Hậu quả khi mất offset |
|---|---|
latest (mặc định) |
Bỏ qua mọi thứ cũ, mất dữ liệu chưa đọc trong im lặng |
earliest |
Đọc lại từ đầu log, replay toàn bộ, side effect chạy lại hàng loạt |
none |
Ném lỗi, buộc người vận hành quyết định |
Với consumer có side effect, đặt earliest kèm idempotency, hoặc none cho dữ liệu nhạy cảm. Đừng để mặc định latest cho pipeline không được mất.
Chiến lược commit
Phần tiêu đề “Chiến lược commit”| Cách | Khi nào | Lưu ý |
|---|---|---|
Sync trong vòng lặp (commitSync sau mỗi batch) |
Mặc định cho xử lý tuần tự | Đơn giản, an toàn, chậm hơn một chút |
commitAsync |
Throughput cao | Không retry an toàn được vì có thể ghi đè offset mới hơn; commit sync khi đóng consumer |
Commit trong onPartitionsRevoked |
Luôn nên có với manual commit | Lưu tiến độ trước khi mất partition |
| Offset trong cùng DB transaction | Muốn exactly-once với DB | Tự quản lý vị trí, seek khi nhận partition |
| Kafka transaction | Đầu ra cũng là Kafka | Bài transactions |
Với batch: offset là tiến độ liên tục, không phải bitmap. Xử lý xong 7 và 9 nhưng 8 lỗi thì không commit được 10. Hoặc xử lý tuần tự theo partition, hoặc theo dõi offset hoàn tất liên tục lớn nhất.
Luồng bốn bước
Phần tiêu đề “Luồng bốn bước”offset 7Consumer đọc record offset 7; group vẫn đang lưu vị trí đọc tiếp là 7.
- Broker giao offset 7. Crash lúc này: group đọc lại 7 (redelivery bình thường).
- Handler ghi DB trong một transaction, kèm insert
event_idvào bảng processed có unique constraint (idempotent consumer).SELECTrồiUPDATElà không đủ khi hai process chạy đồng thời. - DB commit. Crash lúc này vẫn giao lại 7; handler thấy
event_idđã có và bỏ qua. - Commit offset 8. Lỗi commit không có nghĩa DB rollback; chấp nhận redelivery.
Khi nào dùng / không dùng
Phần tiêu đề “Khi nào dùng / không dùng”- At-least-once + idempotent write: mặc định cho billing, ledger, projection.
- Commit trước (at-most-once): chỉ cho dữ liệu chấp nhận mất như metric, log thô.
- Kafka transaction: khi đầu ra là Kafka topic; không bao được PostgreSQL.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Mô hình failure window trong RAM (hai biến, không chạy Kafka hay PostgreSQL):
npx --prefix examples ts-node examples/src/kafka-offset-commit/commit-window-demo.tsOutput: db-first cho dbApplied=true, committedNextOffset=7 (redelivery); offset-first cho dbApplied=false, committedNextOffset=8 (mất record 7). Kiểm tra thật bằng integration test với crash hook giữa DB commit và offset commit.
Trade-offs
Phần tiêu đề “Trade-offs”Commit sau side effect
- Không vượt qua record khi side effect chưa bền.
- Redelivery giúp phục hồi sau crash.
- Unique event ID giữ business write một lần dù delivery lặp.
Giá phải trả
- Handler phải idempotent vì replay và rebalance đều đưa record trở lại.
- Manual commit cần quản lý offset liên tục và vòng đời partition.
- Không có transaction chung cho Kafka offset và DB ngoài Kafka.
Checklist production
Phần tiêu đề “Checklist production”- Tắt
enable.auto.commitcho consumer có side effect quan trọng, hoặc đảm bảo xử lý đồng bộ trong vòng lặp poll. - Đặt
auto.offset.resetcó chủ đích (earliesthoặcnone), không dựa vào mặc địnhlatest. - Side effect idempotent theo
event_id(unique constraint). - Commit trong
onPartitionsRevokedvà commit sync khi shutdown. - Alert khi group không hoạt động gần
offsets.retention.minutes(7 ngày). - Test crash giữa DB commit và offset commit.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Tóm tắt
Phần tiêu đề “Tóm tắt”Tự kiểm tra: (1) DB đã commit nhưng offset vẫn là 7, restart làm gì? (2) Vì sao commit 10 là sai khi 8 chưa xử lý? (3) Group nghỉ 8 ngày rồi chạy lại với auto.offset.reset=latest thì chuyện gì xảy ra?