Đơn vị biên soạn: TechData.AI
Chủ đề: Kafka Reliability và Data Consistency
Đối tượng: Data Engineer, Solution Architect và Backend Developer
Phiên bản tham chiếu: Apache Kafka 4.x
MỤC LỤC
- Mục tiêu bài học
- Vì sao message có thể mất hoặc bị trùng
- At Most Once
- At Least Once
- Exactly Once thực sự nghĩa là gì
- Idempotent Producer
- Kafka Transaction
- Read Process Write với Transaction
- isolation.level của Consumer
- Exactly Once với database bên ngoài
- Transactional Outbox
- Idempotent Consumer
- Retry, Timeout và lỗi không xác định
- Thiết kế cho thanh toán và đơn hàng
- Kiểm thử Fault Injection
- Những hiểu lầm thường gặp
- Checklist lựa chọn Delivery Semantics
- Tổng kết
- Câu hỏi ôn tập
- Tài liệu tham khảo
1. Mục tiêu bài học
Các thuật ngữ At Most Once, At Least Once và Exactly Once xuất hiện trong hầu hết tài liệu Kafka, nhưng rất dễ bị sử dụng như nhãn quảng cáo. Một pipeline có Producer idempotent chưa chắc Exactly Once. Một Kafka Transaction thành công cũng không tự động làm database, email và API bên ngoài trở thành một transaction duy nhất.
Bài này phân tích từng ranh giới lỗi và xác định chính xác Kafka bảo đảm điều gì. Mục tiêu là giúp Data Engineer chọn cơ chế phù hợp thay vì bật nhiều cấu hình rồi tin rằng dữ liệu không thể mất hoặc trùng.
2. Vì sao message có thể mất hoặc bị trùng

Hệ thống phân tán có nhiều điểm lỗi:
- Producer gửi request nhưng network bị ngắt.
- Broker ghi thành công nhưng phản hồi không đến Producer.
- Leader hỏng trước khi Replica bắt kịp.
- Consumer xử lý thành công nhưng chưa commit Offset.
- Consumer commit Offset rồi database transaction thất bại.
- Ứng dụng khởi động lại giữa hai thao tác.
- Timeout xảy ra nhưng trạng thái thực tế chưa rõ.
Tình huống khó nhất là kết quả không xác định. Producer timeout không biết Broker đã ghi hay chưa. Consumer mất kết nối không biết database đã commit hay chưa. Retry có thể cứu dữ liệu bị mất nhưng cũng có thể tạo dữ liệu trùng.
Delivery Semantics là cách hệ thống chọn và kiểm soát đánh đổi này.
3. At Most Once

At Most Once nghĩa là record được xử lý tối đa một lần. Nó có thể không được xử lý lần nào nếu lỗi xảy ra.
Ở phía Consumer, một cách tạo At Most Once là commit Offset trước khi xử lý nghiệp vụ:
message = consumer.poll(1.0)
consumer.commit(message=message, asynchronous=False)
process(message)Nếu tiến trình hỏng sau commit nhưng trước khi process() hoàn tất, Kafka sẽ tiếp tục từ record sau. Record hiện tại bị bỏ qua.
At Most Once có thể phù hợp với telemetry ít quan trọng hoặc dữ liệu lấy mẫu, nơi bỏ sót một lượng nhỏ được chấp nhận và xử lý trùng gây hậu quả lớn hơn. Nó hiếm khi phù hợp cho thanh toán, đơn hàng hoặc bút toán tài chính.
4. At Least Once
At Least Once nghĩa là hệ thống cố gắng bảo đảm mỗi record được xử lý, nhưng một record có thể được xử lý nhiều lần.
Consumer xử lý trước rồi commit:
message = consumer.poll(1.0)
process(message)
consumer.commit(message=message, asynchronous=False)Nếu process() đã cập nhật database nhưng ứng dụng hỏng trước commit, Kafka giao lại record sau khi Consumer khởi động. Nghiệp vụ chạy lần hai.
At Least Once là lựa chọn phổ biến vì mất dữ liệu thường nghiêm trọng hơn dữ liệu lặp. Tuy nhiên, nó chỉ an toàn khi Consumer idempotent hoặc hệ thống có cơ chế phát hiện duplicate.
5. Exactly Once thực sự nghĩa là gì
Exactly Once không có nghĩa một record chỉ đi qua network đúng một lần. Retry vẫn xảy ra. Record có thể được đọc nhiều lần ở tầng kỹ thuật, nhưng ảnh hưởng cuối cùng được commit đúng một lần trong phạm vi được bảo đảm.
Cần hỏi rõ phạm vi:
- Producer ghi vào một Topic.
- Ứng dụng đọc Kafka và ghi lại Kafka.
- Consumer ghi database.
- Consumer gọi API thanh toán.
- Consumer gửi email.
Kafka hỗ trợ mạnh cho Exactly Once trong luồng Kafka tới Kafka bằng Idempotent Producer, Transaction và Consumer read_committed. Khi tác động vượt khỏi Kafka, cần phối hợp với cơ chế của hệ thống bên ngoài.
Một tuyên bố Exactly Once không nêu phạm vi và Failure Model là tuyên bố thiếu giá trị kỹ thuật.
6. Idempotent Producer

Khi enable.idempotence=true, Producer sử dụng Producer ID và Sequence Number. Broker phát hiện những batch được gửi lại do retry và không ghi duplicate trong phạm vi phiên Producer được hỗ trợ.
producer = Producer({
"bootstrap.servers": "localhost:9092",
"enable.idempotence": True,
"acks": "all",
})Idempotence giải quyết duplicate do Producer retry, không giải quyết việc ứng dụng chủ động tạo cùng một Event hai lần. Nếu Order Service tạo hai event_id khác nhau cho cùng nghiệp vụ, Kafka xem đó là hai record hợp lệ.
Producer Idempotence cũng không ngăn Consumer ghi một record hai lần vào database. Mỗi lớp vẫn cần cơ chế phù hợp.
7. Kafka Transaction
Kafka Transaction cho phép Producer ghi nhiều record vào nhiều Partition như một đơn vị nguyên tử. Tất cả record được commit hoặc bị abort.
Producer cần transactional.id ổn định và duy nhất cho mỗi instance đang hoạt động:
producer = Producer({
"bootstrap.servers": "localhost:9092",
"transactional.id": "order-enrichment-01",
"enable.idempotence": True,
})
producer.init_transactions()
try:
producer.begin_transaction()
producer.produce("order-valid", key=key, value=value)
producer.produce("audit-events", key=key, value=audit_value)
producer.commit_transaction()
except Exception:
producer.abort_transaction()
raisetransactional.id giúp Kafka fencing Producer cũ. Nếu hai instance dùng cùng ID do triển khai sai, instance cũ có thể bị chặn để tránh hai Producer cùng ghi như một thực thể.
Transaction có chi phí về latency, Coordinator và state log. Không nên dùng cho mọi Event nếu nghiệp vụ không yêu cầu atomicity.
8. Read Process Write với Transaction
Một use case quan trọng là đọc record từ Topic nguồn, xử lý rồi ghi Topic đích. Ứng dụng cần commit cả output record và input Offset trong cùng Kafka Transaction.
Quy trình:
- Consumer đọc batch.
- Producer bắt đầu Transaction.
- Ứng dụng xử lý và ghi output.
- Offset của input được đưa vào Transaction.
- Transaction được commit.
Nếu ứng dụng hỏng trước commit, output bị abort và input Offset không tiến lên. Batch được đọc lại nhưng kết quả abort không xuất hiện với Consumer read_committed.
Pseudo code:
producer.begin_transaction()
for message in messages:
output = transform(message.value())
producer.produce("clean-orders", value=output)
producer.send_offsets_to_transaction(
offsets,
consumer.consumer_group_metadata(),
)
producer.commit_transaction()Đây là nền tảng của Exactly Once trong Kafka Streams và các ứng dụng Read Process Write tự xây dựng.
9. isolation.level của Consumer
Consumer mặc định có thể đọc record thuộc Transaction chưa commit hoặc đã abort tùy cấu hình Client. Consumer cần isolation.level=read_committed nếu chỉ muốn thấy dữ liệu Transaction đã commit.
consumer = Consumer({
"bootstrap.servers": "localhost:9092",
"group.id": "clean-order-consumer",
"isolation.level": "read_committed",
})Với read_committed, Consumer chỉ đọc đến Last Stable Offset và bỏ qua record của Transaction bị abort. Điều này có thể làm record đã ghi vật lý chưa xuất hiện ngay cho Consumer trong khi Transaction còn mở.
Nếu Producer sử dụng Transaction nhưng Consumer đọc ở chế độ không phù hợp, mục tiêu atomicity ở tầng ứng dụng có thể bị phá vỡ.
10. Exactly Once với database bên ngoài
Kafka Transaction không bao phủ transaction của PostgreSQL, MySQL hoặc BigQuery. Không thể mặc định commit Kafka Offset và database update như một thao tác nguyên tử duy nhất.
Một Consumer có thể:
- Ghi database thành công.
- Hỏng trước khi commit Kafka Offset.
- Đọc lại record.
- Ghi database lần nữa.
Cách xử lý phổ biến là At Least Once cộng Idempotent Consumer. Database dùng Unique Constraint trên event_id, Upsert theo Business Key hoặc bảng Inbox lưu Event đã xử lý.
Nếu database hỗ trợ transaction, việc ghi nghiệp vụ và đánh dấu event_id phải nằm trong cùng transaction database. Sau khi database commit, Consumer mới commit Kafka Offset.
Cách này vẫn có thể đọc lại Event nhưng ảnh hưởng nghiệp vụ không lặp.
11. Transactional Outbox

Dual Write xảy ra khi một service cần ghi database và gửi Kafka. Nếu làm hai thao tác độc lập, có thể xảy ra:
- Database commit nhưng Kafka gửi thất bại.
- Kafka gửi thành công nhưng database rollback.
Transactional Outbox giải quyết bằng cách ghi dữ liệu nghiệp vụ và Event Outbox trong cùng một database transaction.
BEGIN;
INSERT INTO orders (...);
INSERT INTO outbox_events (
event_id,
aggregate_id,
event_type,
payload,
created_at
) VALUES (...);
COMMIT;Một tiến trình riêng hoặc Debezium đọc Outbox và công bố Event vào Kafka. Nếu publish lặp, event_id giúp Consumer chống trùng.
Outbox không loại bỏ mọi duplicate. Nó bảo đảm thay đổi nghiệp vụ và ý định công bố Event được lưu cùng nhau. Phía Consumer vẫn nên idempotent.
12. Idempotent Consumer
Ba chiến lược phổ biến:
Unique Event ID
Lưu event_id vào bảng processed_events có Unique Constraint. Nếu insert bị duplicate, bỏ qua nghiệp vụ.
Upsert theo Business Key
Trạng thái được ghi bằng order_id và version. Consumer chỉ cập nhật nếu version mới lớn hơn version đã lưu.
UPDATE order_state
SET status = :status,
event_version = :event_version
WHERE order_id = :order_id
AND event_version < :event_version;Idempotency Key ở API đích
Khi gọi Payment API, gửi Idempotency Key ổn định. API đích trả cùng kết quả cho các request lặp thay vì tạo giao dịch mới.
Không nên dùng thời gian nhận Event làm khóa chống trùng. Retry có thời gian khác nhưng vẫn là cùng nghiệp vụ.
13. Retry, Timeout và lỗi không xác định
Timeout không đồng nghĩa thất bại. Nó chỉ cho biết Client không nhận kết quả trong thời gian chờ.
Đối với thao tác không idempotent, retry mù có thể tạo hậu quả nghiêm trọng. Trước khi retry thanh toán, hệ thống cần tra cứu trạng thái giao dịch bằng Idempotency Key hoặc Transaction ID.
Phân loại lỗi:
- Retriable Error: lỗi tạm thời, có thể thử lại.
- Fatal Error: cấu hình hoặc trạng thái khiến Producer không thể tiếp tục.
- Abortable Error: Transaction hiện tại cần abort nhưng Producer còn dùng được.
- Permanent Business Error: dữ liệu không hợp lệ và không nên retry tự động.
Retry nên có giới hạn, Exponential Backoff và Jitter. Mọi lần thất bại cần metric và cảnh báo.
14. Thiết kế cho thanh toán và đơn hàng

Giả sử Payment Consumer nhận payment-requested.
Thiết kế an toàn:
- Event có
event_id,order_idvàpayment_id. payment_idđược dùng làm Idempotency Key khi gọi cổng thanh toán.- Kết quả giao dịch được lưu bằng Unique Constraint.
- Nếu timeout, hệ thống tra cứu trạng thái trước khi tạo request mới.
- Event
payment-completedchỉ được tạo từ trạng thái đã lưu. - Outbox bảo đảm database và Event không lệch nhau.
- Consumer phía sau tiếp tục xử lý idempotent.
Không nên tin rằng Kafka Transaction có thể rollback giao dịch đã gửi đến ngân hàng. Ranh giới Kafka và ranh giới nghiệp vụ tài chính là hai phạm vi khác nhau.
15. Kiểm thử Fault Injection
Happy Path không chứng minh độ tin cậy. Cần chủ động tạo lỗi:
- Dừng Broker trong khi Producer đang gửi.
- Ngắt Consumer sau khi database commit nhưng trước Offset commit.
- Làm một Replica chậm.
- Tạo timeout từ API đích.
- Gửi cùng Event nhiều lần.
- Restart ứng dụng giữa Transaction.
- Tạo Poison Message.
Sau mỗi thử nghiệm, kiểm tra:
- Có record nào mất không?
- Có nghiệp vụ nào lặp không?
- Offset ở đâu?
- Transaction nào abort?
- Consumer có phục hồi tự động không?
- Cảnh báo có được phát ra không?
Độ tin cậy là thuộc tính phải được kiểm thử, không phải giả định từ cấu hình.
16. Những hiểu lầm thường gặp
acks=all nghĩa là không bao giờ mất dữ liệu
Sai. Còn phụ thuộc ISR, min.insync.replicas, Replication Factor, unclean election và vận hành Cluster.
Idempotent Producer tạo Exactly Once toàn hệ thống
Sai. Nó không bảo vệ database sink hoặc nghiệp vụ do ứng dụng tạo lặp.
Kafka Transaction bao phủ REST API
Sai. Kafka không rollback tác động đã xảy ra trong hệ thống ngoài Kafka.
At Least Once luôn tạo duplicate
Không nhất thiết. Nó tạo khả năng xử lý lại. Idempotent Consumer có thể giữ kết quả cuối đúng.
DLT giải quyết mọi lỗi
DLT chỉ giữ Event thất bại. Không có quy trình điều tra và replay, nó chỉ chuyển vấn đề sang Topic khác.
17. Checklist lựa chọn Delivery Semantics
Trước khi thiết kế, trả lời:
- Mất một Event có được chấp nhận không?
- Xử lý lặp gây hậu quả gì?
- Hệ thống đích có hỗ trợ idempotency không?
- Có thể dùng Unique Constraint hoặc Upsert không?
- Luồng chỉ Kafka tới Kafka hay có hệ thống bên ngoài?
- Cần atomicity cho một record hay một batch?
- Latency và throughput mục tiêu là bao nhiêu?
- Retention có đủ cho phục hồi không?
- Có thể replay an toàn không?
- Ai chịu trách nhiệm xử lý DLT?
Nếu dữ liệu có thể tái tạo và sai số nhỏ được chấp nhận, At Most Once có thể đủ. Nếu dữ liệu quan trọng, At Least Once cùng idempotency thường là nền tảng thực tế. Nếu pipeline Kafka tới Kafka cần trạng thái nhất quán, Transaction và Exactly Once có giá trị rõ ràng.
18. Tổng kết
Delivery Semantics không phải một công tắc duy nhất. Nó là kết quả của thứ tự xử lý, Offset commit, retry, idempotence, transaction và khả năng của hệ thống đích.
Các điểm cần nhớ:
- At Most Once có thể mất record.
- At Least Once có thể xử lý record lặp.
- Exactly Once phải luôn nêu rõ phạm vi.
- Idempotent Producer hạn chế duplicate do retry.
- Kafka Transaction tạo atomicity cho các thao tác trong Kafka.
read_committedgiúp Consumer chỉ thấy Transaction đã commit.- Database bên ngoài cần Unique Constraint, Upsert hoặc Inbox.
- Transactional Outbox giải quyết Dual Write từ database ra Kafka.
- Fault Injection là phần bắt buộc của kiểm thử độ tin cậy.
Bài tiếp theo sẽ sử dụng Kafka Connect và Debezium để xây dựng pipeline CDC từ MySQL vào Kafka.
19. Câu hỏi ôn tập
- At Most Once có thể mất dữ liệu trong tình huống nào?
- Vì sao At Least Once yêu cầu Consumer idempotent?
- Idempotent Producer ngăn loại duplicate nào?
- Kafka Transaction bảo đảm atomicity trong phạm vi nào?
read_committedảnh hưởng Consumer ra sao?- Vì sao Kafka không thể rollback Payment API?
- Transactional Outbox giải quyết Dual Write thế nào?
- Những lỗi nào cần Fault Injection?
20. Tài liệu tham khảo
- Apache Kafka Delivery Semantics: https://kafka.apache.org/documentation/#semantics
- Apache Kafka Producer Configurations: https://kafka.apache.org/documentation/#producerconfigs
- Apache Kafka Consumer Configurations: https://kafka.apache.org/documentation/#consumerconfigs
- Apache Kafka Transactions: https://kafka.apache.org/documentation/#transactions
- Debezium Outbox Event Router: https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html
