Khi một hệ thống gửi tiền, trừ kho, hay tính cước, câu hỏi "message này có chắc chắn được xử lý đúng một lần không?" không còn là chi tiết kỹ thuật — nó là đúng/sai nghiệp vụ. Kafka cho bạn ba mức đảm bảo giao message, và chọn sai mức nghĩa là hoặc mất dữ liệu, hoặc xử lý trùng (trừ tiền hai lần). Ba mức đó — at-most-once, at-least-once, exactly-once — không phải nút gạt duy nhất mà là kết quả của vài tham số: chủ yếu là acks phía producer và enable.idempotence. Bài này (phần 4/12) mổ xẻ cơ chế rồi đo thật, với vài con số đi ngược trực giác.
Ba mức đảm bảo và cơ chế đằng sau
Mấu chốt nằm ở acks — producer chờ xác nhận tới đâu trước khi coi một message là "đã gửi":

Hình 1: acks=0 gửi rồi quên — có thể mất (at-most-once). acks=all + retry — không mất nhưng có thể trùng khi ack bị mất (at-least-once). enable.idempotence=true gắn producer-id + số thứ tự để broker khử trùng — ghi đúng một lần (exactly-once phía producer).
- At-most-once (
acks=0): producer gửi rồi quên, không chờ broker xác nhận. Nếu mạng rớt hay broker bận, message bay mất mà không ai biết. Chỉ hợp dữ liệu bỏ được (metric, log sampling). - At-least-once (
acks=all+ retry): producer chờ broker ghi xong mới coi là thành công. Nếu ack mất trên đường về, producer tưởng thất bại và retry — broker ghi lần nữa → message trùng. Không mất dữ liệu, nhưng consumer phải chịu được trùng (xử lý idempotent phía nghiệp vụ). - Exactly-once phía producer (
enable.idempotence=true): mỗi producer được gán một producer-id (PID) và đánh số thứ tự (sequence) cho từng message. Broker thấy cặp(PID, sequence)đã ghi rồi thì bỏ qua bản trùng và vẫn trả ack. Retry trở nên an toàn: ghi đúng một lần dù gửi lại. Từ Kafka 3.x, đây là mặc định bật.
# at-least-once + khử trùng phía producer (khuyến nghị mặc định)
kafka-producer-perf-test.sh --topic ds --num-records 1000000 --record-size 100 --throughput -1 \
--producer-props bootstrap.servers=localhost:9092 acks=all enable.idempotence=true
Đo thật: throughput theo acks
Mình chạy cùng một tải (1 triệu bản ghi 100 byte) với bốn cấu hình acks khác nhau:

Hình 2: Đo thật trên single-node. acks=0: 88,5 MB/giây; acks=1: 113,8 MB/giây; acks=all: 100,5 MB/giây; acks=all + idempotence: 106,7 MB/giây. Chạy lại với 2 triệu bản ghi cho cùng thứ hạng (acks=0 chậm nhất, acks=1 nhanh nhất).
Hai điều cần đọc trung thực từ bảng này:
1. Trên single-node, acks=all gần như acks=1 — về cả độ bền lẫn tốc độ. acks=all nghĩa là "chờ mọi replica trong ISR xác nhận". Nhưng lab chỉ có một broker, replication-factor = 1, nên ISR chỉ có đúng một replica (chính là leader). Vì vậy acks=all ở đây không bền hơn acks=1 chút nào, và throughput cũng sát nhau. Khác biệt độ bền thật của acks=all chỉ xuất hiện trên cluster nhiều broker, nơi nó chờ dữ liệu được sao chép sang máy khác — mình sẽ nói kỹ ở bài kafka-09. Đừng để con số single-node đánh lừa rằng "acks=all gần như miễn phí"; trên cluster thật nó có cái giá đồng bộ qua mạng.
2. acks=0 lại CHẬM hơn acks=1 — ngược trực giác. "Gửi rồi quên" đáng lẽ phải nhanh nhất, nhưng đo thật (lặp lại hai lần, 1 triệu rồi 2 triệu bản ghi) đều cho acks=0 thấp hơn acks=1. Lý do: khi không có ack, producer mất tín hiệu để áp back-pressure và quản lý số request đang bay (in-flight) hiệu quả — nó gửi dồn dập, gây tranh chấp buffer, và nghịch lý là tổng throughput giảm. Đây là ví dụ điển hình vì sao phải đo thay vì suy luận: trực giác "ít chờ hơn = nhanh hơn" sai ở đây.
Và một điểm thực dụng: idempotence gần như miễn phí — acks=all + enable.idempotence=true cho 106,7 MB/giây với độ trễ trung bình 3,33 ms, ngang (thậm chí nhỉnh hơn) acks=all thuần. Vì nó rẻ và chặn trùng khi retry, hãy cứ bật — và Kafka 3.x đã bật sẵn.
Exactly-once end-to-end: còn hơn thế
Cần phân biệt rõ: enable.idempotence cho exactly-once khi ghi vào Kafka (producer → broker không tạo bản trùng). Nhưng "exactly-once" đầy đủ của một pipeline (đọc → xử lý → ghi) cần thêm transaction (transactional.id, isolation.level=read_committed) để gom việc đọc-xử-lý-ghi thành một đơn vị nguyên tử. Đó là cơ chế đứng sau Kafka Streams exactly-once. Với phần lớn ứng dụng, kết hợp at-least-once + consumer idempotent (consumer tự chịu được trùng, ví dụ dùng khoá nghiệp vụ để khử) là lựa chọn đơn giản và đủ mạnh; exactly-once transaction mạnh hơn nhưng phức tạp và tốn hơn.
Đánh đổi cần cân nhắc
Không có "exactly-once" miễn phí cho mọi thứ. Idempotent producer chỉ khử trùng trong một phiên producer và trên cùng partition. Nếu producer khởi động lại (PID mới), hoặc bạn ghi cùng dữ liệu từ hai chỗ, Kafka không biết đó là trùng về mặt nghiệp vụ. Khử trùng theo nghĩa nghiệp vụ (cùng đơn hàng gửi hai lần) vẫn là trách nhiệm của ứng dụng, thường bằng một khoá idempotency.
at-least-once là mặc định thực dụng, nhưng dồn gánh nặng sang consumer. Chọn at-least-once nghĩa là chấp nhận "thà trùng còn hơn mất", và consumer phải xử lý được message trùng mà không gây tác dụng phụ hai lần (dùng UPSERT theo khoá, kiểm tra đã xử lý chưa...). Nếu consumer không idempotent, at-least-once âm thầm gây lỗi nghiệp vụ.
acks=0 hiếm khi đáng dùng. Đo thật cho thấy nó không nhanh hơn (còn chậm hơn), lại chấp nhận mất dữ liệu. Gần như luôn có lựa chọn tốt hơn: acks=1 nếu chịu được mất hiếm khi leader chết trước lúc sao chép, hoặc acks=all trên cluster thật nếu cần độ bền. Giữ acks=0 cho đúng loại dữ liệu thực sự bỏ được.
Ba ý mang về
- acks + idempotence quyết định mất/trùng/đúng-một-lần.
acks=0có thể mất (at-most-once);acks=all+ retry không mất nhưng có thể trùng (at-least-once);enable.idempotence=truekhử trùng bằng producer-id + sequence (exactly-once phía producer, mặc định bật từ 3.x). - Đo thật phá vỡ hai trực giác sai. Trên single-node acks=all ≈ acks=1 (chỉ một replica, nên độ bền không hơn — khác biệt thật cần cluster nhiều broker); và acks=0 chậm hơn acks=1 vì mất back-pressure. Idempotence gần như miễn phí (106 MB/giây) — cứ bật.
- Exactly-once đầy đủ cần hơn idempotent producer. Idempotence chỉ lo producer→broker; pipeline đọc-xử-lý-ghi exactly-once cần transaction. Thực dụng nhất cho đa số: at-least-once + consumer tự idempotent theo khoá nghiệp vụ.
Nguồn
- Apache Kafka — Producer Configs (acks, enable.idempotence, retries): https://kafka.apache.org/documentation/#producerconfigs
- Apache Kafka — Message Delivery Semantics: https://kafka.apache.org/documentation/#semantics
- Confluent — Exactly-Once Semantics in Apache Kafka: https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/
Phần sau ta mổ xẻ offset: consumer nhớ đã đọc tới đâu bằng cách commit offset — auto-commit tiện nhưng nguy hiểm thế nào, khi nào phải commit thủ công, và cách tua lại (seek) để đọc lại từ một điểm.