Skip to content

Kafka 可靠性语义

Kafka 的可靠性不是一个单独配置决定的,而是 Producer、Broker、副本、Consumer Offset 和业务处理共同决定的结果。

三种投递语义

语义含义典型原因
At-most-once最多一次,可能丢,不重复先提交 Offset 再处理,或 Producer 不重试
At-least-once至少一次,不丢,可能重复先处理再提交 Offset,失败后重放
Exactly-once端到端效果一次幂等写入、事务、Offset 原子提交、业务幂等共同保证

很多系统宣称的 Exactly-once 指的是在 Kafka 内部读-处理-写链路中实现一次效果。如果消息最终写入外部数据库,还需要数据库侧幂等或事务配合。

Producer 可靠性

Producer 写入链路:

send()

  ├── 写入 Producer 缓冲区
  ├── Sender 线程发送到 Leader
  ├── Leader 追加日志
  ├── ISR 副本同步
  └── Broker 返回 ACK

关键配置:

配置建议说明
acksall等待 ISR 副本确认
enable.idempotencetrue避免 Producer 重试导致重复写入
retries保持较大网络抖动时自动重试
delivery.timeout.ms合理设置限制单条消息最终投递时间
max.in.flight.requests.per.connection<= 5幂等生产者要求

acks 的影响

acks成功返回条件风险
0不等待 Broker 确认Broker 未收到也算成功
1Leader 写入成功Leader 故障且 Follower 未同步时丢失
allISR 满足确认条件配合 min.insync.replicas 最可靠

Broker 副本可靠性

推荐生产配置:

properties
default.replication.factor=3
min.insync.replicas=2
unclean.leader.election.enable=false

对应 Producer:

properties
acks=all
enable.idempotence=true

这组配置的含义:

  • 每条消息有 3 个副本
  • 至少 2 个 ISR 副本可用才允许成功写入
  • 不允许从落后副本中选 Leader
  • Producer 重试不会在同一 Partition 内写出重复消息

写入失败比丢数据更好

当 ISR 数量低于 min.insync.replicas 时,Kafka 会拒绝写入。这看起来降低了可用性,但它阻止了“只有 Leader 单副本写入成功,随后 Leader 故障导致消息丢失”的风险。

Consumer Offset 可靠性

Consumer 的 Offset 提交位置决定了失败后的行为。

先提交再处理

poll message

  ├── commit offset
  └── process message

如果提交后处理失败,重启后不会再消费这条消息,结果是丢消息。这属于 At-most-once。

先处理再提交

poll message

  ├── process message
  └── commit offset

如果处理成功但提交失败,重启后会再次消费这条消息,结果是重复处理。这属于 At-least-once。

对大多数业务,At-least-once + 业务幂等是更实用的方案。

幂等生产者

幂等生产者解决的是 Producer 重试导致的重复写入。

Producer PID = 1001
Partition orders-0
Sequence: 0, 1, 2, 3...

Broker 会记录每个 Producer 在每个 Partition 上的序列号。如果网络异常导致 Producer 重发相同序列号的批次,Broker 可以识别并去重。

幂等生产者保证:

  • 单 Producer Session
  • 单 Partition
  • 重试场景下不重复写入

它不保证:

  • 跨 Topic 原子性
  • 跨 Partition 原子性
  • 外部数据库写入只执行一次

事务

Kafka 事务用于把多条写入和 Offset 提交绑定为一个原子操作。

典型流处理链路:

Consumer 读取 input topic

  ├── 处理消息
  ├── Producer 写 output topic
  ├── sendOffsetsToTransaction()
  └── commitTransaction()

事务提交后:

  • 输出 Topic 的消息对 read_committed Consumer 可见
  • 输入 Topic 的 Offset 随事务一起提交
  • 如果事务中止,输出消息不可见,Offset 也不会提交

事务示例

java
props.put("transactional.id", "order-pipeline-1");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.initTransactions();

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    producer.beginTransaction();
    try {
        for (ConsumerRecord<String, String> record : records) {
            producer.send(new ProducerRecord<>("order-events", record.key(), transform(record.value())));
        }
        producer.sendOffsetsToTransaction(currentOffsets(records), consumer.groupMetadata());
        producer.commitTransaction();
    } catch (Exception e) {
        producer.abortTransaction();
    }
}

Consumer 需要设置:

properties
isolation.level=read_committed

顺序性保证

Kafka 只保证 Partition 内有序,不保证 Topic 全局有序。

如果需要同一订单的事件有序:

java
new ProducerRecord<>("orders", orderId, event);

相同 Key 会进入同一 Partition,因此同一订单的事件保持顺序。

影响顺序的因素:

因素影响
Key 变化同一业务实体可能进入不同 Partition
增加 PartitionKey 到 Partition 的映射可能变化
Producer 重试未开启幂等时可能乱序
多个 ProducerKafka 不保证不同 Producer 之间的全局顺序

端到端可靠性方案

日志采集

text
acks=1
enable.idempotence=true
enable.auto.commit=true

可接受少量重复或丢失,优先吞吐和成本。

订单事件

text
replication.factor=3
min.insync.replicas=2
acks=all
enable.idempotence=true
enable.auto.commit=false

消费端先处理成功,再手动提交 Offset。下游写数据库时使用业务唯一键做幂等。

流处理

text
enable.idempotence=true
transactional.id=固定实例 ID
isolation.level=read_committed

读 Kafka、写 Kafka、提交 Offset 使用事务绑定。

参考