Bỏ qua để đến nội dung
Knowledge Hub

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

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ệ.

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.

Broker đã append, ack bị mất, producer retry cùng sequence
Broker đã append, ack bị mất, producer retry cùng sequenceBước 1/4: App gửi event; producer gắn producer ID và sequence cho batch ở partition.AppProducerBrokerevent-42
App → Producerevent-42
Bước 1/4

App 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ệ.

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.

  • 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 send sau 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.

  • 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.

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.

Terminal window
npx --prefix examples ts-node examples/src/kafka-idempotent-producer/retry-model.ts

Output: 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.

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.
  1. Giữ enable.idempotence=true, acks=all, max.in.flight<=5; log config effective lúc khởi động.
  2. Topic quan trọng dùng RF=3, min.insync.replicas=2.
  3. Tạo eventId một lần ở thời điểm nghiệp vụ, lưu bền, dùng lại cho mọi lần publish.
  4. Dùng outbox khi DB và Kafka phải nhất quán.
  5. Consumer dedupe theo eventId.
  6. Xử lý lỗi cuối của send theo loại lỗi, không bọc while(true) send().

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?