Kafka idempotent producer: retry cùng batch không ghi trùng, gửi mới thì vẫn có thể Trung cấp
Thời gian đọc ước tính: 4 phút
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”Order service gửi OrderPaid. Broker đã append record nhưng response bị mất do kết nối đứt; producer chỉ thấy timeout và không biết request đầu thành công hay chưa. Không retry thì có thể mất event; retry mù thì có thể ghi trùng, và billing ghi bút toán hai lần.
Idempotent producer giải quyết đúng khoảng không chắc chắn này ở ranh giới producer đến Kafka: retry của cùng batch được broker nhận ra và không append lần hai. Một lệnh send mới cho cùng event thì không được bảo vệ.
Cơ chế
Phần tiêu đề “Cơ chế”Broker nhận diện producer bằng producer ID (PID) và sequence number theo từng partition. Retry giữ nguyên PID và sequence của batch gốc.
event-42App gửi event; producer gắn producer ID và sequence cho batch ở partition.
| Lần gửi | PID / sequence | Broker đã thấy? | Kết quả |
|---|---|---|---|
| Batch đầu | 7 / 0 | Chưa | Append |
| Retry cùng batch vì mất ack | 7 / 0 | Rồi | Không append lần hai |
App tạo send mới cùng eventId |
7 / 1 | Chưa | Append record thứ hai |
Broker không hiểu OrderPaid hay orderId; nó chỉ so PID và sequence. Hai lời gọi send độc lập có cùng payload vẫn là hai record hợp lệ.
Cheat-sheet config (producer Java 4.2)
Phần tiêu đề “Cheat-sheet config (producer Java 4.2)”| Config | Mặc định | Ghi chú |
|---|---|---|
enable.idempotence |
true (mặc định từ 3.0) |
Nếu đặt tường minh mà có config xung đột thì ném ConfigException; nếu chỉ dựa mặc định mà có xung đột thì idempotence bị tắt |
acks |
all |
Idempotence yêu cầu all |
retries |
2147483647 | Giới hạn thực tế là delivery.timeout.ms |
delivery.timeout.ms |
120000 | Tổng thời gian cho một lần send kể cả retry |
max.in.flight.requests.per.connection |
5 | Phải <= 5 khi idempotence bật |
linger.ms |
5 (đổi từ 0 ở 4.0) | Đợi gom batch |
transactional.id |
null | Chỉ cần khi dùng transaction |
Config effective phải kiểm tra sau khi ghép default, framework và biến môi trường: một acks=1 từ môi trường có thể âm thầm tắt idempotence.
Phạm vi và giới hạn
Phần tiêu đề “Phạm vi và giới hạn”- Thứ tự: với idempotence và
max.in.flight<=5, retry không đảo thứ tự batch trong partition. Producer config docs chỉ nêu yêu cầu<= 5; lý do nằm ở cài đặt broker (chỉ giữ trạng thái sequence cho một số ít batch gần nhất mỗi cặp producer/partition), không phải hợp đồng được tài liệu hoá. - Vòng đời PID: producer restart nhận PID mới, nên một
sendsau restart không bị coi là retry của batch trước. - Phạm vi: một producer session và một partition. Không bao trùm nhiều partition, nhiều session, hay DB.
- Không thay
acks=all: idempotence chống trùng khi retry; durability là việc của acks, ISR và min.insync.replicas (bài ISR).
Kịch bản đúng ranh giới: DB ghi đơn đã thanh toán nhưng không lưu việc đã phát event. Sau restart, ứng dụng gọi send mới. Đây là batch mới, topic có hai OrderPaid. Cách phòng: transactional outbox ở phía phát và idempotent consumer ở phía nhận.
Khi nào dùng / không dùng
Phần tiêu đề “Khi nào dùng / không dùng”- Luôn bật (đã là mặc định ở 4.x); không có lý do tắt cho producer thông thường.
- Không đủ khi cần ghi nhiều partition nguyên tử hoặc gắn với offset input: dùng Kafka transaction.
- Không thay event ID nghiệp vụ và dedupe ở consumer.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Mô hình trạng thái PID/sequence bằng Map (không phải producer hay broker thật; không dùng để kiểm chứng config). Repo có demo KafkaJS 2.2 chạy trên broker Kafka 3.6; bài này mô tả config theo 4.2.
npx --prefix examples ts-node examples/src/kafka-idempotent-producer/retry-model.tsOutput: first send: appended, same batch after lost ack: duplicate retry, new logical send: appended; log mô phỏng chứa hai order-42-paid ở cuối.
Trade-offs
Phần tiêu đề “Trade-offs”Bảo vệ retry của producer
- Retry cùng batch sau ack mất không append bản sao.
- Giữ thứ tự batch trong partition khi config đúng.
- Ứng dụng không phải đoán trạng thái write từ timeout.
Giới hạn
- Config xung đột có thể tắt idempotence hoặc làm producer lỗi.
- Không dedupe send mới cùng payload hoặc event ID.
- Không làm pipeline DB đến Kafka thành exactly-once.
Checklist production
Phần tiêu đề “Checklist production”- Giữ
enable.idempotence=true,acks=all,max.in.flight<=5; log config effective lúc khởi động. - Topic quan trọng dùng RF=3,
min.insync.replicas=2. - Tạo
eventIdmột lần ở thời điểm nghiệp vụ, lưu bền, dùng lại cho mọi lần publish. - Dùng outbox khi DB và Kafka phải nhất quán.
- Consumer dedupe theo
eventId. - Xử lý lỗi cuối của
sendtheo loại lỗi, không bọcwhile(true) send().
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) Broker append rồi ack mất, producer retry cùng batch tạo mấy record? (2) App restart rồi send lại cùng eventId thì sao? (3) Vì sao acks=all và idempotence giải quyết hai vấn đề khác nhau?