Kafka洞见 生产者消息可靠性

6 阅读5分钟

生产者消息可靠性

1. 生产者可靠性机制

1.1 消息确认机制-ACK

producer提供了三种消息确认的模式,通过配置 acks 来实现不同级别的可靠性保证。

ACK配置选项
flowchart TD
    A[Producer发送消息] --> B{acks配置}
    
    B -->|acks=0| C[不等待确认]
    B -->|acks=1| D[等待Leader确认]
    B -->|acks=-1/all| E[等待Leader+ISR确认]
    
    C --> F[效率最高<br/>可靠性最低<br/>可能丢数据]
    D --> G[中等效率<br/>中等可靠性<br/>Leader宕机可能丢数据]
    E --> H[效率最低<br/>可靠性最高<br/>数据不会丢失]
    
    subgraph "副本同步状态"
        I[AR = ISR + OSR]
        J[ISR: 同步副本集合]
        K[OSR: 非同步副本集合]
    end

详细说明:

  • acks=0:表示生产者将数据发送出去就不管了,不等待任何返回。这种情况下数据传输效率最高,但是数据可靠性最低,当server挂掉的时候就会丢数据。

  • acks=1(默认):表示数据发送到Kafka后,经过leader成功接收消息的确认,才算发送成功。如果leader宕机了,就会丢失数据。

  • acks=-1/all:表示生产者需要等待ISR中的所有follower都确认接收到数据后才算发送完成,这样数据不会丢失,因此可靠性最高,性能最低。

数据完全可靠条件
flowchart LR
  
    subgraph "副本集合关系"
        E[AR全部副本]
        F[ISR同步副本]
        G[OSR非同步副本]
        E --> H[AR = ISR + OSR]
        F --> I[正常情况: AR = ISR, OSR = null]
    end
flowchart LR
    A[数据完全可靠] --> B[ACK级别 = -1]
    A --> C[分区副本 >= 2]
    A --> D[ISR最小副本数 >= 2]
    

数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2

副本集合说明:

  • AR (All Replicas):所有副本集合
  • ISR (In-Sync Replicas):在指定时间内和leader保存数据同步的集合
  • OSR (Out-Sync Replicas):不能在指定的时间内和leader保持数据同步集合
配置示例
// ACK 设置
properties.put(ProducerConfig.ACKS_CONFIG, "1");

// 重试次数,默认的重试次数是 Integer.MAX_VALUE
properties.put(ProducerConfig.RETRIES_CONFIG, 3);

1.2 数据去重-幂等性

1.2.1 幂等性原理

在一般的MQ模型中,常有以下的消息通信概念:

flowchart TD
    A[消息传递语义] --> B["At Least Once<br/>至少一次"]
    A --> C["At Most Once<br/>最多一次"]
    A --> D["Exactly Once<br/>精确一次"]
    
    B --> E["ACK=-1 + 副本>=2 + ISR>=2<br/>保证不丢失,可能重复"]
    C --> F["ACK=0<br/>保证不重复,可能丢失"]
    D --> G["至少一次 + 幂等性<br/>既不丢失也不重复"]
    
    subgraph K11["Kafka 0.11版本特性"]
        H[幂等性]
        I[事务]
    end
    
    D --> H
    D --> I

幂等性定义:对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能之后就可以避免这种情况。

1.2.2 幂等性实现机制
sequenceDiagram
    participant P as Producer
    participant B as Broker
    
    Note over P,B: 幂等性判断机制
    
    P->>B: 消息1 <PID=1, Partition=0, SeqNum=1>
    B->>B: 内存序列号=0, 收到序列号=1 (0+1=1) ✓
    B-->>P: 存储消息,更新内存序列号=1
    
    P->>B: 消息2 <PID=1, Partition=0, SeqNum=1> (重复)
    B->>B: 内存序列号=1, 收到序列号=1 (<1) ✗
    B-->>P: 丢弃重复消息,返回成功
    
    P->>B: 消息3 <PID=1, Partition=0, SeqNum=5> (跳跃)
    B->>B: 内存序列号=1, 收到序列号=5 (>>1) ✗
    B-->>P: 抛出异常-消息丢失

重复数据的判断标准:具有 <PID, Partition, SeqNumber> 相同主键的消息提交时,Broker只会持久化一条。

  • ProducerId(PID):Kafka每次重启都会分配一个新的
  • Partition:分区号
  • Sequence Number:序列化号,单调自增

幂等性判断逻辑

  • 收到序列号 = 内存序列号 + 1:存储消息
  • 收到序列号 < 内存序列号:丢弃重复消息
  • 收到序列号 >> 内存序列号:抛出异常,消息丢失

重要限制:幂等性只能保证在单分区单会话内不重复。

1.2.3 如何使用幂等性

开启幂等性功能的方式很简单,只需要显式地将生产者客户端参数 enable.idempotence 设置为true即可(这个参数的默认值为true),并且还需要确保生产者客户端的retries、acks、max.in.flight.requests.per.connection参数不被配置错,默认值就是对的。

// 开启幂等性(默认已开启)
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

1.3 消息事务

1.3.1 事务机制原理

由于幂等性不能跨分区运作,为了保证同时发的多条消息,要么全成功,要么全失败,kafka引入了事务的概念。

sequenceDiagram
    participant P as Producer
    participant TC as Transaction Coordinator
    participant B1 as Broker1 (Partition0)
    participant B2 as Broker2 (Partition1)
    
    Note over P,B2: 事务处理流程
    
    P->>TC: 1. initTransactions()
    TC-->>P: 分配PID和Epoch
    
    P->>TC: 2. beginTransaction()
    TC-->>P: 开始事务
    
    P->>B1: 3. 发送消息到Partition0
    P->>B2: 3. 发送消息到Partition1
    B1-->>P: 消息写入成功
    B2-->>P: 消息写入成功
    
    alt 正常提交
        P->>TC: 4. commitTransaction()
        TC->>B1: 写入事务提交标记
        TC->>B2: 写入事务提交标记
        TC-->>P: 事务提交成功
    else 异常回滚
        P->>TC: 4. abortTransaction()
        TC->>B1: 写入事务回滚标记
        TC->>B2: 写入事务回滚标记
        TC-->>P: 事务回滚成功
    end

开启事务的条件:producer 设置 transactional.id 的值并同时开启幂等性。

1.3.2 事务API
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;
1.3.3 事务使用示例
package com.luojia.kafka.product;
import com.luojia.kafka.partitioner.MyPartitioner;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class CustomProducerTransactions {

    public static void main(String[] args) {

        // 配置属性类
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());
        // Kafka事务消息,必须要指定事务ID,ID可以任意填写但必须全局唯一
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction_id_01");

        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);

        // 1.初始化事务
        producer.initTransactions();
        // 2.开始事务
        producer.beginTransaction();

        // 3.发送数据
        try {
            for (int i = 0; i < 5; i++) {
                producer.send(new ProducerRecord<>("first", "kafka first transaction msg : " + i));
            }
            // 故意整一个异常,模拟事务回滚
            // int i = 10/0;
            // 4.提交事务
            producer.commitTransaction();
        } catch (Exception e) {
            // 5.回滚事务
            producer.abortTransaction();
        } finally {
            producer.close();
        }
    }
}

2. 消息的有序性

2.1 有序性基本概念

消息在单分区内有序,多分区内无序(如果对多分区进行排序,造成分区无法工作需要等待排序,浪费性能)。

flowchart LR
    A[Producer] --> B[Topic]
    
    subgraph SP["单分区有序保证"]
        C[Partition 0]
        C --> D["Message 1 - 时间戳: T1"]
        D --> E["Message 2 - 时间戳: T2"]
        E --> F["Message 3 - 时间戳: T3"]
        G["Consumer读取顺序: 1→2→3 ✓"]
    end
    
    subgraph MP["多分区无序情况"]
        H[Partition 1]
        I[Partition 2]
        H --> J["Msg A(T1), Msg C(T3)"]
        I --> K["Msg B(T2), Msg D(T4)"]
        L["Consumer读取可能顺序: A→B→C→D ✗"]
    end
    
    B --> C
    B --> H
    B --> I

kafka只能保证单分区下的消息顺序性,为了保证消息的顺序性,需要做到如下几点:

2.2 有序性配置要求

flowchart TD
    A[消息有序性配置] --> B{是否开启幂等性?}
    
    B -->|未开启幂等性| C[max.in.flight.requests.per.connection = 1]
    B -->|开启幂等性| D[max.in.flight.requests.per.connection <= 5]
    
    C --> E[缓冲队列最多放置1个请求<br/>严格保证顺序]
    D --> F[Broker端缓存最多5个request<br/>利用序列号保证顺序]
    
    subgraph "幂等性优势"
        G[允许更高并发]
        H[利用序列号排序]
        I[性能更好]
    end
    
    D --> G
    D --> H
    D --> I

配置说明:

  • 如果未开启幂等性:需要 max.in.flight.requests.per.connection 设置为1(缓冲队列最多放置1个请求)
  • 如果开启幂等性:需要 max.in.flight.requests.per.connection 设置为小于等于5

2.3 幂等性下的有序性保证机制

这是因为broker端会缓存producer主题分区下的五个request,保证最近5个request是有序的。

sequenceDiagram
    participant P as Producer
    participant B as Broker
    
    Note over P,B: 幂等性下的有序性保证
    
    P->>B: Request 1 (SeqNum=1)
    P->>B: Request 2 (SeqNum=2)
    P->>B: Request 3 (SeqNum=3) - 网络丢失
    P->>B: Request 4 (SeqNum=4)
    P->>B: Request 5 (SeqNum=5)
    
    Note over B: Broker收到Request 1,2,4,5<br/>缓存Request 4,5等待Request 3
    
    B-->>P: 确认 Request 1,2
    
    P->>B: Request 3 (SeqNum=3) - 重发
    
    Note over B: 收到Request 3,按序处理3,4,5
    
    B-->>P: 确认 Request 3,4,5
    
    rect rgb(200, 255, 200)
        Note over P,B: 最终消息顺序: 1→2→3→4→5 ✓
    end

2.4 有序性实现示例

示例场景:现在有5个请求,Request1、Request2、Request3、Request4、Request5,在向Kafka集群中发送的时候:

  1. Request1、Request2成功发送并确认
  2. Request3由于网络问题丢失,但Request4、Request5先到达
  3. Broker将Request4、Request5缓存起来,等待Request3
  4. Request3重发到达后,Broker按序处理Request3、Request4、Request5
  5. 因为是幂等的,所以每条消息都有自己的单调递增的序列号
  6. Broker最多缓存5条数据
flowchart LR
    A[Request 1] --> B["✓ 成功发送"]
    C[Request 2] --> D["✓ 成功发送"]
    E[Request 3] --> F["✗ 网络丢失"]
    G[Request 4] --> H["→ Broker缓存"]
    I[Request 5] --> J["→ Broker缓存"]
    
    F --> K[Request 3 重发]
    K --> L["✓ 成功发送"]
    
    subgraph BP["Broker处理顺序"]
        M["1. 处理Request 1,2"]
        N["2. 缓存Request 4,5"]
        O["3. 等待Request 3"]
        P["4. 收到Request 3后按序处理3,4,5"]
    end
    
    subgraph FR["最终结果"]
        Q["消息顺序: 1→2→3→4→5"]
        R["保证了单分区内的有序性"]
    end

2.5 有序性配置建议

// 开启幂等性(推荐)
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

// 设置合适的并发请求数
properties.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);

// 如果对顺序要求极其严格,可以设置为1(性能较低)
// properties.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);

// 开启重试保证消息不丢失
properties.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);

总结

Kafka生产者的消息可靠性机制通过多层保障确保数据的安全传输:

核心机制总结

flowchart LR
    A["消息可靠性机制"] --> B["ACK确认机制"]
    A --> C["幂等性机制"]
    A --> D["事务机制"]
    A --> E["有序性保证"]
    
    B --> B1["acks=0 高效率低可靠"]
    B --> B2["acks=1 平衡模式"]
    B --> B3["acks=-1 高可靠低效率"]
    B --> B4["完全可靠条件"]
    
    C --> C1["PID+Partition+SeqNum"]
    C --> C2["单分区单会话"]
    C --> C3["防止重复消息"]
    C --> C4["序列号判断"]
    
    D --> D1["跨分区保证"]
    D --> D2["原子性操作"]
    D --> D3["事务协调器"]
    D --> D4["全成功或全失败"]
    
    E --> E1["单分区有序"]
    E --> E2["多分区无序"]
    E --> E3["幂等性配合"]
    E --> E4["请求缓存机制"]

最佳实践建议

  1. 可靠性配置

    • 生产环境建议使用 acks=-1
    • 开启幂等性 enable.idempotence=true
    • 设置合理的重试次数
  2. 有序性保证

    • 关键业务使用单分区发送
    • 开启幂等性时设置 max.in.flight.requests.per.connection<=5
    • 极端场景可设置为1但会影响性能
  3. 事务使用

    • 跨分区操作需要事务保证
    • 设置全局唯一的 transactional.id
    • 正确处理事务异常和回滚
  4. 性能平衡

    • 根据业务需求选择合适的可靠性级别
    • 在可靠性和性能之间找到平衡点
    • 监控和调优相关参数

通过合理配置这些机制,Kafka生产者能够在保证高可靠性的同时,提供良好的性能表现,满足各种业务场景的需求。