Producer Kafka: acks, retry và idempotence đúng cách
1. Tổng quan
Khi producer gửi message lên Kafka, có ba câu hỏi bạn phải trả lời: gửi xong chưa coi là thành công, lỡ lỗi thì retry thế nào, và retry có làm trùng message không. Bài này giải thích acks, retries và enable.idempotence — ba cấu hình quyết định độ tin cậy và tính đúng đắn của producer. Đọc xong, bạn sẽ biết cấu hình producer “production-ready” và tránh được các lỗi phổ biến gây mất hoặc trùng dữ liệu.
2. Dependency cần thiết
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.7.0</version>
</dependency>
Ví dụ trong bài dùng Kafka client thuần (KafkaProducer), không qua Spring Kafka, để thấy rõ từng config ảnh hưởng thế nào.
3. acks: khi nào producer coi là gửi thành công
acks quyết định producer phải chờ bao nhiêu broker xác nhận trước khi coi message đã gửi xong.
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.ACKS_CONFIG, "all");
acks=0: producer không chờ xác nhận gì cả. Nhanh nhất, nhưng mất message nếu broker down ngay sau khi nhận — thậm chí không biết là mất.acks=1: chỉ chờ leader partition ghi vào log local rồi trả ACK. Nếu leader crash trước khi follower kịp sync, message mất.acks=all(hoặc-1): chờ tất cả replica trong ISR (in-sync replica set) xác nhận. An toàn nhất, nhưng latency cao hơn.
Lưu ý:
acks=allchỉ thật sự an toàn khi đi kèmmin.insync.replicas >= 2ở broker. Nếumin.insync.replicas=1,acks=allcũng chỉ cần 1 replica xác nhận — không khác gìacks=1về độ an toàn.
3.1. Cấu hình broker đi kèm
# server.properties hoặc topic config
min.insync.replicas=2
Với replication factor = 3 và min.insync.replicas=2, hệ thống chịu được 1 broker chết mà vẫn ghi được, đồng thời đảm bảo dữ liệu nằm trên ít nhất 2 bản sao trước khi ACK.
4. retries: xử lý lỗi tạm thời
Producer có thể gặp lỗi tạm thời như NotLeaderForPartitionException hay mất kết nối mạng ngắn. retries cho phép producer tự gửi lại mà không cần code xử lý thủ công.
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);
retries: số lần retry tối đa. Từ Kafka 2.1, nên set rất cao (hoặcInteger.MAX_VALUE) và dùngdelivery.timeout.msđể kiểm soát tổng thời gian chờ thay vì đếm số lần.retry.backoff.ms: thời gian chờ giữa các lần retry, tránh spam broker.delivery.timeout.ms: tổng thời gian tối đa từ lúc gọisend()đến lúc nhận kết quả (thành công hoặc lỗi cuối cùng). Phải lớn hơnrequest.timeout.ms + linger.ms.
Vấn đề: nếu không cẩn thận, retry có thể làm message bị gửi trùng hoặc gửi sai thứ tự.
4.1. Vì sao retry gây trùng message
1. Producer gửi batch chứa message A
2. Broker ghi A thành công, gửi ACK về
3. ACK bị mất trên đường mạng (network timeout)
4. Producer không nhận được ACK -> coi là lỗi -> retry
5. Broker nhận lại request -> ghi A lần 2
=> Topic có 2 bản của message A
Đây là tình huống kinh điển: broker đã ghi thành công nhưng producer không biết, nên gửi lại. Idempotence ở phần sau giải quyết đúng vấn đề này.
4.2. Vì sao retry gây sai thứ tự
Nếu max.in.flight.requests.per.connection > 1, producer có thể gửi nhiều batch song song. Batch 2 có thể được broker ghi trước batch 1 nếu batch 1 bị lỗi và đang retry.
max.in.flight.requests.per.connection=5
Với cấu hình này mà không có idempotence, thứ tự message trong partition không được đảm bảo khi có retry.
5. Idempotence: giải quyết trùng lặp do retry
enable.idempotence=true giúp producer tự động tránh ghi trùng khi retry, nhờ Kafka gắn Producer ID (PID) và sequence number cho mỗi batch.
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
Khi bật idempotence, Kafka tự động set các giá trị sau (bạn không cần tự cấu hình, nhưng nên biết):
acks = all
retries = Integer.MAX_VALUE (hoặc rất lớn)
max.in.flight.requests.per.connection <= 5
5.1. Cơ chế hoạt động
Mỗi producer session được broker cấp một PID duy nhất. Mỗi message gửi trong một partition có một sequence number tăng dần. Broker lưu sequence number cuối cùng đã ghi cho mỗi PID + partition.
Producer gửi batch seq=5 cho partition 0
Broker: "sequence cuối tôi lưu là 4, 5 là tiếp theo -> ghi, lưu seq=5"
Producer gửi lại (do retry) batch seq=5
Broker: "seq=5 tôi đã ghi rồi -> bỏ qua, trả ACK thành công luôn"
Nhờ vậy, retry không tạo bản ghi trùng — broker tự phát hiện và loại bỏ duplicate dựa trên sequence number, không cần producer hay consumer tự xử lý.
Lưu ý: idempotence chỉ đảm bảo không trùng trong một session producer và trong phạm vi một partition. Nếu producer restart (PID mới được cấp), cơ chế dedup theo PID cũ không còn áp dụng. Muốn exactly-once xuyên suốt nhiều partition/topic, bạn cần transaction (
transactional.id) — nằm ngoài phạm vi idempotence đơn thuần.
5.2. Ví dụ cấu hình producer production-ready
public ProducerFactory() {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// Bật idempotence -> tự set acks=all, retries cao, in-flight <= 5
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);
this.producer = new KafkaProducer<>(props);
}
public void sendOrderEvent(String orderId, String payload) {
ProducerRecord<String, String> record =
new ProducerRecord<>("order-events", orderId, payload);
producer.send(record, (metadata, exception) -> {
if (exception != null) {
log.error("Gửi order {} thất bại", orderId, exception);
} else {
log.info("Order {} ghi vào partition {} offset {}",
orderId, metadata.partition(), metadata.offset());
}
});
}
ENABLE_IDEMPOTENCE_CONFIG = truelà dòng quan trọng nhất — nó kéo theoacks=alltự động.- Callback trong
send()cho biết kết quả thực tế: nếuexception != null, nghĩa là đã retry hếtdelivery.timeout.msmà vẫn lỗi — lúc này bạn mới cần xử lý nghiệp vụ (log, alert, lưu vào bảng lỗi).
6. Kiểm tra hành vi với test
Để minh họa phần dedup, ta giả lập một MessageDeduplicator đơn giản mô phỏng cơ chế sequence number ở mức application (dùng khi bạn không thể bật idempotence, ví dụ hệ thống legacy):
public class MessageDeduplicator {
private final Map<String, Long> lastSeenSequence = new ConcurrentHashMap<>();
public boolean shouldWrite(String partitionKey, long sequence) {
Long last = lastSeenSequence.get(partitionKey);
if (last != null && sequence <= last) {
return false; // duplicate, bỏ qua
}
lastSeenSequence.put(partitionKey, sequence);
return true;
}
}
class MessageDeduplicatorTest {
@Test
void shouldSkipDuplicateSequence() {
MessageDeduplicator deduplicator = new MessageDeduplicator();
assertThat(deduplicator.shouldWrite("order-1", 5)).isTrue();
assertThat(deduplicator.shouldWrite("order-1", 5)).isFalse(); // retry trùng seq
assertThat(deduplicator.shouldWrite("order-1", 6)).isTrue();
}
@Test
void shouldAllowDifferentPartitionKeysIndependently() {
MessageDeduplicator deduplicator = new MessageDeduplicator();
assertThat(deduplicator.shouldWrite("order-1", 1)).isTrue();
assertThat(deduplicator.shouldWrite("order-2", 1)).isTrue();
}
}
Test đầu khẳng định: cùng sequence gửi lại lần hai bị bỏ qua — đúng tinh thần idempotence producer. Test thứ hai khẳng định việc dedup chỉ áp dụng trong phạm vi một key/partition, không ảnh hưởng key khác.
7. Bảng tổng hợp cấu hình theo nhu cầu
| Nhu cầu | acks | enable.idempotence | Rủi ro |
|---|---|---|---|
| Log, metric không quan trọng | 0 | false | Mất message khi lỗi, nhưng throughput cao nhất |
| Cân bằng tốc độ/an toàn | 1 | false | Mất message nếu leader crash trước khi follower sync |
| Dữ liệu nghiệp vụ quan trọng (đơn hàng, thanh toán) | all | true | Latency cao hơn, nhưng không mất, không trùng do retry |
8. Lỗi hay gặp
- Set
acks=allnhưng quênmin.insync.replicas: nghĩ là an toàn nhưng broker chỉ cần 1 ISR vẫn ACK — không khácacks=1. - Tự viết retry logic song song với retry mặc định của Kafka client: dễ gây gửi trùng gấp đôi vì cả hai lớp retry đều hoạt động.
- Bật idempotence nhưng để
max.in.flight.requests.per.connection > 5: Kafka sẽ reject cấu hình này ngay từ khi khởi tạo producer (ConfigException), vì vượt quá giới hạn mà idempotence hỗ trợ. - Nhầm idempotence với exactly-once toàn hệ thống: idempotence chỉ bảo vệ producer-to-broker. Nếu consumer xử lý message rồi gọi side-effect (gửi email, trừ tiền) không idempotent, bạn vẫn có thể xử lý trùng ở phía consumer.
Lưu ý: không nên bật
enable.idempotence=truenếu hệ thống đang chạy Kafka version cũ hơn 0.11 — tính năng này chưa được broker hỗ trợ và producer sẽ lỗi khi khởi tạo.
9. Kết luận
acks, retries và enable.idempotence là ba mảnh ghép không thể tách rời khi bạn cần producer đáng tin cậy. acks=all đảm bảo dữ liệu được replicate đủ trước khi coi là thành công; retries giúp vượt qua lỗi tạm thời; và idempotence đảm bảo việc retry đó không tạo ra bản ghi trùng. Với dữ liệu nghiệp vụ quan trọng, hãy mặc định dùng cả ba cùng nhau — chi phí latency nhỏ hơn rất nhiều so với chi phí xử lý dữ liệu sai hoặc trùng về sau.