多图详解kafka生产者消息发送过程

本文深入探讨了Kafka生产者发送消息的完整流程,包括构造KafkaProducer、生产者拦截器、分区器、Sender线程启动、消息发送等步骤。详细分析了消息的序列化、分区计算、元信息更新、拦截器的使用和Sender线程的工作原理,帮助读者全面理解Kafka生产者的内部机制。

:fire:《Kafka运维管控平台LogiKM》:fire: :pencil2:更强大的管控能力:pencil2: :tennis:更高效的问题定位能力:tennis: :sunrise:更便捷的集群运维能力:sunrise: :musical_score:更专业的资源治理:musical_score: :sun_with_face:更友好的运维生态:sun_with_face:

@[TOC] 今天我们来通过源码来分析一下,生产者发送一条消息的所有流程~~~

生产者客户端代码

public class SzzTestSend {

    public static final String bootStrap = "xxxxxx:9090";
    public static final String topic = "t_3_1";

    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,bootStrap);
        // 序列化协议  下面两种写法都可以
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        //过滤器 可配置多个用逗号隔开
        properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,"org.apache.kafka.clients.producer.SzzProducerInterceptorsTest");
        //构造 KafkaProducer
        KafkaProducer producer = new KafkaProducer(properties);
        //  发送消息, 并设置 回调(回调函数也可以不要)
        ProducerRecord<String,String> record = new ProducerRecord(topic,"Hello World!");
        try {
            producer.send(record,new SzzTestCallBack(record.topic(), record.key(), record.value()));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 发送成功回调类
     */
    public static class SzzTestCallBack implements Callback{
        private static final Logger log = LoggerFactory.getLogger(SzzTestCallBack.class);
        private String topic;
        private String key;
        private String value;

        public SzzTestCallBack(String topic, String key, String value) {
            this.topic = topic;
            this.key = key;
            this.value = value;

        }
        public void onCompletion(RecordMetadata metadata, Exception e) {
            if (e != null) {
                log.error("Error when sending message to topic {} with key: {}, value: {} with error:",
                        topic, key,value, e);
            }else {
                log.info("send message to topic {} with key: {} value:{} success, partiton:{} offset:{}",
                        topic, key,value,metadata.partition(),metadata.offset());
            }
        }
    }
}

复制代码

构造KafkaProducer

KafkaProducer通过解析 producer.propeties 文件里面的属性来构造自己。 例如 :分区器、Key和Value序列化器、拦截器、 RecordAccumulator消息累加器 、元信息更新器、启动发送请求的后台线程

//构造 KafkaProducer
        KafkaProducer producer = new KafkaProducer(properties);
复制代码

生产者元信息更新器

我们之前有讲过. 客户端都会保存集群的元信息,例如生产者的元信息是 ProducerMetadata. 消费组的是ConsumerMetadata 。

元信息都会有自己的自动更新逻辑, 详细请看 Kafka的客户端发起元信息更新请求

相关的Producer配置有:

属性 描述 默认
metadata.max.age.ms 即使我们没有看到任何分区领导层更改以主动发现任何新代理或分区,我们也强制刷新元数据的时间段(以毫秒为单位)。。 300000(5分钟)
retry.backoff.ms 如果上次更新失败,发起重试的间隔时间 100

虽然Producer元信息会自动更新, 但是有可能在生产者发送消息的时候,发现某个TopicPartition不存在,这个时候可能就需要立刻发起一个元信息更新了。

生产者拦截器

生产者拦截器在消息发送之前可以做一些准备工作, 比如 按照某个规则过滤某条消息, 又或者对 消息体做一些改造, 还可以用来在发送回调逻辑之前做一些定制化的需求,例如统计类的工作! 拦截器的执行时机在最前面,在 消息序列化 和 分区计算 之前

相关的Producer配置有:

属性 描述 默认
interceptor.classes 生产者拦截器配置,填写全路径类名,可用逗号隔开配置多个,执行顺序就是配置的顺序。

生产者分区器

用来设置发送的消息具体要发送到哪个分区上

相关的Producer配置有:

属性 描述 默认值
partitioner.class 消息的分区分配策略 org.apache.kafka.clients.producer.internals.DefaultPartitioner

Sender线程启动

Sender是专门负责将消息发送到Broker的I/O线程。

相关的Producer配置有:

属性 描述 默认值
max.in.flight.requests.per.connection 客户端能够允许的最大未完成请求(在请求中)的请求数量, 如果该值大于1, 并且请求发送失败可可能导致消息重排序的风险(如果重试启用的话)
<think>我们正在讨论Kafka生产者写入数据的详细流程。根据用户提供的引用和之前的问题,我们将详细解释Kafka生产者将数据写入Kafka集群的过程。 注意:引用[3]提到了Kafka的顺序保证,引用[4]提到了生产者的配置(如序列化器、批处理等)。我们将结合这些信息进行说明。 Kafka生产者写入数据的工作流程可以分为以下几个步骤: 1. **生产者初始化配置**: 生产者需要配置一些必要的属性,如: - `bootstrap.servers`:指定Kafka集群的地址(一个或个broker地址) - `key.serializer`:用于序列化消息键的类(如`StringSerializer`) - `value.serializer`:用于序列化消息值的类(如`StringSerializer`) 其他可选配置包括:`acks`(确认机制)、`retries`(重试次数)、`batch.size`(批次大小)、`linger.ms`(等待时间)等。 2. **创建生产者实例**: 使用`KafkaProducer`类创建一个生产者实例。例如: ```java Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer<String, String> producer = new KafkaProducer<>(props); ``` 3. **构建消息(ProducerRecord)**: 每条消息被封装为一个`ProducerRecord`对象,包含: - 目标Topic - 可选的分区号(或通过key计算分区) - 可选的时间戳 - 消息的键(Key)和值(Value) 4. **发送消息**: 生产者发送消息有两种方式: - **同步发送**:使用`send()`方法后立即调用`get()`方法,等待结果。 ```java producer.send(record).get(); ``` - **异步发送**:使用`send()`方法并提供一个回调函数,当消息发送完成(成功或失败)时触发回调。 ```java producer.send(record, (metadata, exception) -> { if (exception != null) { exception.printStackTrace(); } else { System.out.println("消息发送成功,分区:" + metadata.partition() + ", 偏移量:" + metadata.offset()); } }); ``` 5. **消息序列化**: 在发送之前,生产者的序列化器会将消息的键和值转换为字节数组(byte[])[^4]。序列化器需要实现`Serializer`接口。 6. **分区选择**: 如果消息指定了分区,则直接发送到该分区。否则,根据以下规则确定分区: - 如果消息有键(Key),则对键进行哈希(使用默认的分区器,哈希算法为murmur2),然后根据哈希值对分区数取模,确定目标分区。 - 如果没有键,则使用轮询(Round Robin)策略分配分区。 7. **消息累加与批次处理**: 生产者不会立即发送单条消息,而是将消息放入一个批次(Batch)中。每个批次对应一个主题分区。这样设计是为了提高吞吐量。 - 生产者维护一个缓冲区(RecordAccumulator),用于收集发往同一分区的消息。 - 当批次大小达到`batch.size`(默认16KB)或等待时间超过`linger.ms`(默认0,表示立即发送)时,批次会被发送。 8. **发送到Broker**: - 生产者有一个后台线程(Sender线程)负责将批次发送到对应的Broker。 - Sender线程会从缓冲区中取出已准备好的批次,并将它们放入一个请求中发送Kafka集群。 9. **Broker处理**: - Broker接收到消息后,会进行验证(如主题是否存在、分区是否存在、权限等)。 - 如果验证通过,消息被写入分区的日志文件中,并分配一个偏移量(Offset)。 10. **响应处理**: 生产者收到Broker的响应,根据配置的`acks`参数决定何时认为消息发送成功: - `acks=0`:生产者不等待任何确认,直接认为发送成功(可能丢失数据)。 - `acks=1`:Leader副本写入成功即认为成功(默认)。 - `acks=all`(或`-1`):所有ISR(In-Sync Replicas)副本都写入成功才认为成功。 11. **重试机制**: 如果发送失败(如网络问题、Leader不可用等),生产者会根据配置的`retries`参数进行重试(注意:重试可能导致消息重复,需要消费者处理幂等性)。 12. **顺序保证**: 对于同一个分区,Kafka保证消息的顺序性。但要注意,如果启用了重试,并且有个批次在发送,可能会导致重试时乱序(在Kafka 0.11版本之后,通过启用幂等性可以解决乱序问题)。 13. **关闭生产者**: 最后,当不再需要生产者时,应该调用`close()`方法释放资源。 整个流程可以用下表示: ```mermaid graph TD A[创建ProducerRecord] --> B[序列化Key/Value] B --> C[确定分区] C --> D[写入缓冲区RecordAccumulator] D -->|批次满或超时| E[Sender线程读取批次] E --> F[构建请求并发送到Broker] F --> G{Broker处理} G -->|成功| H[返回RecordMetadata] G -->|失败| I[重试或抛出异常] ``` 注意:生产者的性能优化通常涉及缓冲区大小(`buffer.memory`,默认32MB)、批次大小(`batch.size`)、等待时间(`linger.ms`)等参数的调整。 通过以上步骤,Kafka生产者能够高效、可靠地将消息发送Kafka集群。 引用说明: [^3]: 提到了Kafka的顺序保证,即同一分区的消息有序。 [^4]: 提到了生产者的配置,包括序列化器、批处理等参数。 相关问题: 1. 如何配置Kafka生产者以实现高吞吐量? 2. Kafka生产者如何保证消息的可靠性(不丢失、不重复)? 3. 在Kafka生产者中,如何实现自定义的分区策略? 4. 如何处理Kafka生产者发送失败的情况?</think>### Apache Kafka 生产者写入数据工作流程详解 #### 一、核心流程概览 ```mermaid graph TD A[创建ProducerRecord] --> B[序列化Key/Value] B --> C[分区选择] C --> D[写入缓冲区] D --> E[批次聚合] E --> F[网络发送] F --> G[Broker处理] G --> H[返回ACK] ``` #### 二、详细工作流程 1. **消息创建与初始化** - 生产者创建`ProducerRecord`对象,包含: - 目标Topic - 消息Value(必需) - Key(可选,用于分区路由) - 分区号(可选,显式指定) - 时间戳(可选)[^3] 2. **数据序列化** - Key/Value 通过配置的序列化器转换为字节数组: ```java // 示例配置[^4] spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer ``` - 支持JSON、Avro、Protobuf等序列化格式 3. **分区路由** - 分区选择优先级: 1. 显式指定分区 → 直接使用 2. Key存在 → 哈希算法路由(保证相同Key到同一分区) 3. 无Key → 轮询分区(Round Robin)[^3] - 计算公式:$partition = hash(key) \mod numPartitions$ 4. **消息缓冲与批次聚合** - 消息进入`RecordAccumulator`缓冲区 - 按<分区,Leader>分组聚合,满足以下条件触发发送: - 批次大小 ≥ `batch.size`(默认16KB) - 等待时间 ≥ `linger.ms`(默认0ms) - 缓冲区满(`buffer.memory`默认32MB)[^4] 5. **网络发送** - Sender线程从缓冲区获取批次数据 - 构建`ProduceRequest`包含: - 目标分区Leader地址 - 消息批次数据 - ACK配置级别 - 通过Selector组件异步发送 6. **Broker端处理** - Broker顺序写入分区日志(保证分区内有序)[^3] - 写入流程: 1. 验证消息有效性 2. 追加到分区日志末尾 3. 更新LEO(Log End Offset) 4. 同步副本(根据ACK配置) 7. **ACK确认机制** - 生产者根据`acks`配置等待确认: - `acks=0`:不等待确认(可能丢失数据) - `acks=1`:Leader写入成功即返回(默认) - `acks=all`:所有ISR副本同步完成才返回 - 响应包含:分区、偏移量、时间戳等元数据 8. **错误处理与重试** - 可重试错误(如Leader切换): - 根据`retries`配置自动重试(默认0)[^4] - 通过`retry.backoff.ms`控制重试间隔 - 不可重试错误(如消息过大): - 立即抛出异常终止发送 #### 三、关键配置参数 | 参数 | 默认值 | 作用 | |------|--------|------| | `batch.size` | 16384 | 批次大小阈值(字节) | | `linger.ms` | 0 | 批次等待时间(毫秒) | | `buffer.memory` | 33554432 | 生产者缓冲区大小(32MB) | | `acks` | 1 | 消息持久化确认级别 | | `retries` | 0 | 发送失败重试次数 | | `max.request.size` | 1048576 | 单次请求最大尺寸(1MB) | #### 四、顺序性保证机制 - **分区级有序**:同一分区内的消息严格按照发送顺序存储[^3] - 实现原理: 1. 单分区单Leader写入 2. 消息追加到日志尾部 3. 消费者按偏移量顺序读取 - 注意事项: - 跨分区不保证顺序 - 启用重试时需设置`max.in.flight.requests.per.connection=1`避免乱序 #### 五、性能优化建议 1. **提升吞吐量**: - 增大`batch.size`(建议512KB-1MB) - 调整`linger.ms`(5-100ms) - 启用压缩(`compression.type=snappy`) 2. **保证可靠性**: - 设置`acks=all` - 增加`retries`(建议>3) - 监控ISR状态(`min.insync.replicas`) 3. **避免阻塞**: - 使用异步发送+回调函数 - 设置合理的`max.block.ms`(默认60s) > 通过`KafkaProducer`的`send()`方法实现异步发送[^2],典型代码: > ```java > producer.send(record, (metadata, exception) -> { > if (exception != null) { > // 错误处理 > } else { > System.out.println("写入成功: " + metadata.offset()); > } > }); > ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值