kafka生产者理解

消息发送

数据⽣产流程解析

生产者发送kafka消息流程图

  1. Producer创建时,会创建一个Sender线程并设置为守护线程。
  2. 生产消息时,内部其实是一个异步流程;生产的消息先经过拦截器->序列化器->分区器,然后将消息缓存到缓冲区(该缓冲区也是Producer创建时创建的)
  3. 批次发送的条件是该缓冲区的大小达到配置的batch.size或者linger.ms达到上限了,哪个先达到就算哪个(批次发放是发往同一个分区)。
  4. 批次发送后,发往指定分区后,然后落盘到broken。如果生产者配置了retries参数大于0并且失败原因允许重试的话,那么客户端内部会对该消息进行重试。
  5. 落盘到broken,返回生产元数据给Producer。
  6. 元数据的返回方式有两种:一种是通过阻塞直接返回,另一种是通过回调返回

必要参数配置

broker配置
1.参数配置条目的使用方式
@Bean
public KafkaProducer kafkaProducer() {
    Map<String, Object> configs = new HashMap<>();
    configs.put("bootstrap.servers", "127.0.0.1:9002");
    configs.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
    configs.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
    configs.put("acks", "all");
    configs.put("compression.type", "gzip");
    configs.put("retries", 3);

    KafkaProducer kafkaProducer = new KafkaProducer(configs);
    log.info("配置kafka生产者producer->{}", kafkaProducer);
    return kafkaProducer;
}
2.配置参数
属性说明重要性
bootstrap.servers⽣产者客户端与broker集群建⽴初始连接需要的broker地址列表,由该初始连接发现Kafka集群中其他的所有broker。该地址列表不需要写全部的Kafka集群中broker的地址,但也不要写⼀个,以防该节点宕机的时候不可⽤。形式为: host1:port1,host2:port2,… .hight
key.serializer实现了接⼝ org.apache.kafka.common.serialization.Serializer 的key序列化类。hight
value.serializer实现了接⼝ org.apache.kafka.common.serialization.Serializer 的value序列化类。hight
acks该选项控制着已发送消息的持久性。acks=0 :⽣产者不等待broker的任何消息确认。只要将消息放到了socket的缓冲区,就认为消息已发送。不能保证服务器是否收到该消息, retries 设置也不起作⽤,因为客户端不关⼼消息是否发送失败。客户端收到的消息偏移量永远是-1。acks=1 :leader将记录写到它本地⽇志,就响应客户端确认消息,⽽不等待follower副本的确认。如果leader确认了消息就宕机,则可能会丢失消息,因为follower副本可能还没来得及同步该消息。acks=all :leader等待所有同步的副本确认该消息。保证了只要有⼀个同步副本存在,消息就不会丢失。这是最强的可⽤性保证。等价于 acks=-1 。默认值为1,字符串。可选值:[all, -1, 0, 1]hight
compression.type⽣产者⽣成数据的压缩格式。默认是none(没有压缩)。允许的值: none , gzip , snappy 和 lz4 。压缩是对整个消息批次来讲的。消息批的效率也影响压缩的⽐例。消息批越⼤,压缩效率越好。字符串类型的值。默认是none。hight
retries设置该属性为⼀个⼤于1的值,将在消息发送失败的时候重新发送消息。该重试与客户端收到异常重新发送并⽆⼆⾄。允许重试但是不设置 max.in.flight.requests.per.connection 为1,存在消息乱序的可能,因为如果两个批次发送到同⼀个分区,第⼀个失败了重试,第⼆个成功了,则第⼀个消息批在第⼆个消息批后。int类型的值,默认:0,可选值:[0,…,2147483647]hight

序列化器

在这里插入图片描述
由于kafka中的数据都是字节数组,在将消息发送到kafka之前需要将数据序列化为字节数组。
序列化器的作用就是用于序列化要发送的消息的。

kafka使用org.apache.kafka.common.serialization.Serializer接口用于定义序列化器,将泛型指定类型的数据转换为字节数组。

package org.apache.kafka.common.serialization;
import java.io.Closeable;
import java.util.Map;
/**
 * 将对象转换为byte数组的接⼝
 *  * 该接⼝的实现类需要提供⽆参构造器
 * @param <T> 从哪个类型转换
*/
public interface Serializer<T> extends Closeable {
	/**
	* 类的配置信息
	* @param configs key/value pairs
	* @param isKey key的序列化还是value的序列化
	*/
	void configure(Map<String, ?> configs, boolean isKey);
	/**
	* 将对象转换为字节数组
	*
	* @param topic 主题名称
	* @param data 需要转换的对象
	* @return 序列化的字节数组
	*/
	byte[] serialize(String topic, T data);
	/**
	* 关闭序列化器
	* 该⽅法需要提供幂等性,因为可能调⽤多次。
	*/
	@Override
	void close();
}

系统提供了该接⼝的⼦接⼝以及实现类:

  • org.apache.kafka.common.serialization.ByteArraySerializer
  • org.apache.kafka.common.serialization.ByteBufferSerializer
  • org.apache.kafka.common.serialization.BytesSerializer
  • org.apache.kafka.common.serialization.DoubleSerializer
  • org.apache.kafka.common.serialization.FloatSerializer
  • org.apache.kafka.common.serialization.IntegerSerializer
  • org.apache.kafka.common.serialization.StringSerializer
  • org.apache.kafka.common.serialization.LongSerializer
  • org.apache.kafka.common.serialization.ShortSerializer
自定义序列化器

自定义序列化器,需要实现org.apache.kafka.common.serialization.Serializer<T>接口,并实现其中的serialize方法。
案例:
user实体类

@Data
public class User {
    private Integer userId;
    private String username;
}

自定义序列化器

package luu.demo.kafka.producer;

import luu.demo.kafka.model.User;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.Serializer;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * @author liuming
 * @Title: MyUserSerializer
 * @Description: 自定义序列化器
 * @date 2022/6/29
 */
public class MyUserSerializer implements Serializer<User> {
    /**
     * 类的配置信息
     *
     * @param configs key/value pairs
     * @param isKey   key的序列化还是value的序列化
     */
    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        // do nothing
    }

    /**
     * 将对象转换为字节数组
     *
     * @param topic 主题名称
     * @param data  需要转换的对象
     * @return 序列化的字节数组
     */
    @Override
    public byte[] serialize(String topic, User data) {
        // 如果数据是null,则返回null
        if (data == null) {
            return null;
        }

        Integer userId = data.getUserId();
        String userName = data.getUsername();

        int leng = 0;
        byte[] bytes = null;
        if (null != userName) {
            bytes = userName.getBytes(StandardCharsets.UTF_8);
            leng = bytes.length;
        }

        ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + leng);
        buffer.putInt(userId);
        buffer.putInt(leng);
        buffer.put(bytes);
        return buffer.array();
    }

    @Override
    public void close() {
        // do notjing
    }

    /**
     * 生产者
     *
     * @param args
     */
    public static void main(String[] args) {
        Map<String, Object> configs = new HashMap<>();
        configs.put("bootstrap.servers", "127.0.0.1:9092");
        configs.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        configs.put("value.serializer", MyUserSerializer.class);
        configs.put("acks", "all");
        configs.put("compression.type", "gzip");
        configs.put("retries", 3);
        KafkaProducer<String, User> kafkaProducer = new KafkaProducer<String, User>(configs);

        User user = new User();
        user.setUserId(1001);
        user.setUsername("张三");

        // 创建topic
        new NewTopic("topic_user", 1, (short) 1);

        ProducerRecord<String, User> producerRecord = new ProducerRecord<>("topic_user", 0, user.getUsername(), user);
        kafkaProducer.send(producerRecord, (metadata, exception) -> {
            if (exception == null) {
                System.out.println("消息发送成功:" + metadata.topic() + "\t"
                        + metadata.partition() + "\t"
                        + metadata.offset());
            } else {
                System.out.println("消息发送异常");
            }
        });

        kafkaProducer.close();
    }
}

分区器

在这里插入图片描述
默认使用的分区器DefaultPartitioner

  1. 如果record有提供分区号,则使用record提供的分区号
  2. 如果record没有提供分区号,则使用key的序列化后的值对分区数量进行取模
  3. 如果record没有提供分区号,也没有提供key的话,则使用轮询的方式分配分区号,会首先在可用的分区号中分配,,如果没有可用的则在该主题所有的分区中分配分区号。
    在这里插入图片描述
自定义分区器

如果需要自定义分区器,则需要实现org.apache.kafka.clients.producer.Partitioner接口,并且实现其中partition方法。
然后需要在Producer的配置中增加configs.put("partitioner.class", "xxx.xx.Xxx.class") 配置

package luu.demo.kafka.producer;

import luu.demo.kafka.model.User;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.Cluster;

import java.util.HashMap;
import java.util.Map;

/**
 * @author liuming
 * @Title: MyPartitioner
 * @Description: 自定义分区器
 * @date 2022/6/30
 */
public class MyPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        return 0;
    }

    @Override
    public void close() {
        // do nothong
    }

    @Override
    public void configure(Map<String, ?> configs) {
        // do nothing
    }
}

然后在生产者中实现:

Map<String, Object> configs = new HashMap<>();
        configs.put("bootstrap.servers", "127.0.0.1:9092");
        configs.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        configs.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        // 设置自定义分区器
        configs.put("partitioner.class", "luu.demo.kafka.producer.MyPartitioner");
        configs.put("acks", "all");
        configs.put("compression.type", "gzip");
        configs.put("retries", 3);
        KafkaProducer<String, User> kafkaProducer = new KafkaProducer<String, User>(configs);

        User user = new User();
        user.setUserId(1001);
        user.setUsername("张三");

        // 创建topic
        new NewTopic("topic_user", 1, (short) 1);

        ProducerRecord<String, User> producerRecord = new ProducerRecord<>("topic_user", 0, user.getUsername(), user);
        kafkaProducer.send(producerRecord, (metadata, exception) -> {
            if (exception == null) {
                System.out.println("消息发送成功:" + metadata.topic() + "\t"
                        + metadata.partition() + "\t"
                        + metadata.offset());
            } else {
                System.out.println("消息发送异常");
            }
        });

        kafkaProducer.close();

拦截器

在这里插入图片描述
Producer拦截器(Interceptor)和Consumer拦截器(Interceptor)主要用于实现Client端的定制化控制逻辑。
对于Producer来说,interceptor使得用户在消息发送前以及Productor回调前有机会对消息做一些定制化需求,比如修改消息等。同时,Producer允许用户指定多个interceptor按序作用形成一条拦截链。interceptor的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法:

  • onSend(ProducerRecord):该⽅法封装进KafkaProducer.send⽅法中,即运⾏在⽤户主线程中。Producer确保在消息被序列化以计算分区前调⽤该⽅法。⽤户可以在该⽅法中对消息做任何操作,但最好保证不要修改消息所属的topic和分区,否则会影响⽬标分区的计算。
  • onAcknowledgement(RecordMetadata, Exception):该⽅法会在消息被应答之前或消息发送失败时调⽤,并且通常都是在Producer回调逻辑触发之前onAcknowledgement运⾏在Producer的IO线程中,因此不要在该⽅法中放⼊很重的逻辑,否则会拖慢Producer的消息发送效率。
  • close:关闭Interceptor,主要⽤于执⾏⼀些资源清理⼯作。
    Interceptor可能被运行在多个线程中,因此在具体实现的时候需要开发人员自行确保线程安全。另外在interceptor拦截链执行过程中,需要注意将每个拦截器抛出的异常记录到错误日志,并且是否需要向上传递,这在使用过程中需要特别留意根据业务判断。
自定义拦截器

如果自定义拦截器,需要实现org.apache.kafka.clients.producer.ProducerInterceptor接口。并且在Pruducer配置中配置拦截器,多个拦截器的情况使用,号分隔

package luu.demo.kafka.producer;

import luu.demo.kafka.model.User;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.header.Headers;

import java.util.HashMap;
import java.util.Map;

public class MyInterceptor<K, V> implements ProducerInterceptor<K, V> {
    @Override
    public ProducerRecord onSend(ProducerRecord record) {
        System.out.println("拦截器1---go");
        String topic = record.topic();
        Integer partition = record.partition();
        Long timestamp = record.timestamp();
        Object key = record.key();
        Object value = record.value();
        Headers headers = record.headers();
        // 添加消息头
        headers.add("interceptor", "interceptorOne".getBytes());

        ProducerRecord<String, User> producerRecord = new ProducerRecord("topic_user", 0, key, value, headers);
        return producerRecord;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        System.out.println("拦截器1---back");
        if (exception != null) {
            // 如果发⽣异常,记录⽇志中
            System.out.println(exception.getMessage());
        }
    }

    @Override
    public void close() {
        // do nothing
    }

    @Override
    public void configure(Map<String, ?> configs) {
        // do nothing
    }

生产者

/**
     * 生产者
     *
     * @param args
     */
    public static void main(String[] args) {
        Map<String, Object> configs = new HashMap<>();
        configs.put("bootstrap.servers", "127.0.0.1:9092");
        configs.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        configs.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        // 设置拦截器
        configs.put("interceptor.classes", "luu.demo.kafka.producer.MyInterceptor");
        configs.put("acks", "all");
        configs.put("compression.type", "gzip");
        configs.put("retries", 3);
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(configs);

        // 创建topic
        new NewTopic("topic_test", 1, (short) 1);

        ProducerRecord<String, String> producerRecord = new ProducerRecord<>("topic_user", 0, "test_msg", "test_mag_tetstststststsststts");
        kafkaProducer.send(producerRecord, (metadata, exception) -> {
            if (exception == null) {
                System.out.println("消息发送成功:" + metadata.topic() + "\t"
                        + metadata.partition() + "\t"
                        + metadata.offset());
            } else {
                System.out.println("消息发送异常");
            }
        });

        kafkaProducer.close();
    }

原理解析

在这里插入图片描述

KafkaProducer有两个基本线程:

主线程

负责消息创建,拦截器、序列化器、分区器等操作,并将消息追加到消息收集器RecoderAccumulator中。

  • 消息收集器RecordAccumulator为每个分区都维护了一个Deque<ProdutorBatch>双端队列;
  • ProdutorBatch可以理解为是ProducerRecord的集合,批量发送有利于提升吞吐量,提高性能降低网络的影响;
  • 生产者端还使用了 java.io.ByteBuffer在发送消息之前进行消息保存,并且维护了一个BufferPool 实现 ByteBuffer 的复⽤;该缓存迟只针对特定大⼩(batch.size 指定)的 ByteBuffer 进行复用,对于消息过大的缓存,不能做到重复利用。
  • 每次追加一条ProducerRecord消息,会寻找/新建对应的双端队列,从其尾部获取一个ProducerBatch,判断当前消息的⼤⼩是否可以写⼊该批次中。若可以写⼊则写⼊;若不可以写⼊,则新建⼀个ProducerBatch,判断该消息⼤⼩是否超过客户端参数配置batch.size 的值,不超过,则以 batch.size默认16k建⽴新的ProducerBatch,这样⽅便进⾏缓存重复利⽤;若超过,则以计算的消息⼤⼩建⽴对应的 ProducerBatch ,缺点就是该内存不能被复⽤了。
    在这里插入图片描述
Sender线程
  • 该线程从消息收集器获取缓存的消息,将其处理为Map<Integer, List<ProducerBatch>>的形式,key表示集群的broken的ID
  • 通过进一步处理数据,原来ProducerBatch通过List结构进行存储,现在将ProducerBatch中的TopicPartition数据取出来分组形成Map<TopicPartition, MemoryRecords>数据结构,然后定义了请求的回调接口RequestCompletionHandler和请求ClientRequest,再通过client来将请求发送出去。
    在这里插入图片描述

生产者参数配置补充

参数名称描述
retry.backoff.ms在向⼀个指定的主题分区重发消息的时候,重试之间的等待时间。⽐如3次重试,每次重试之后等待该时间⻓度,再接着重试。在⼀些失败的场景,避免了密集循环的重新发送请求。long型值,默认100。可选值:[0,…]
retriesretries重试次数,当消息发送出现错误的时候,系统会重发消息。跟客户端收到错误时重发⼀样。如果设置了重试,还想保证消息的有序性,需要设MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1否则在重试此失败消息的时候,其他的消息可能发送成功了
request.timeout.ms客户端等待请求响应的最⼤时⻓。如果服务端响应超时,则会重发请求,除⾮达到重试次数。该设置应该⽐replica.lag.time.max.ms (a broker configuration)要⼤,以免在服务器延迟时间内重发消息。int类型值,默认:30000,可选值:[0,…]
interceptor.classes在⽣产者接收到该消息,向Kafka集群传输之前,由序列化器处理之前,可以通过拦截器对消息进⾏处理。要求拦截器类必须实现org.apache.kafka.clients.producer.ProducerInterceptor 接⼝。默认没有拦截器。Map<String, Object> configs中通过List集合配置多个拦截器类名。
acks当⽣产者发送消息之后,如何确认消息已经发送成功了。⽀持的值:acks=0:如果设置为0,表示⽣产者不会等待broker对消息的确认,只要将消息放到缓冲区(消息收集器),就认为消息已经发送完成。该情形不能保证broker是否真的收到了消息,retries配置也不会⽣效,因为客户端不需要知道消息是否发送成功。发送的消息的返回的消息偏移量永远是-1。acks=1: 表示消息只需要写到主分区即可,然后就响应客户端,⽽不等待副本分区的确认。在该情形下,如果主分区收到消息确认之后就宕机了,⽽副本分区还没来得及同步该消息,则该消息丢失。acks=all: ⾸领分区会等待所有的ISR副本分区确认记录。该处理保证了只要有⼀个ISR副本分区存货,消息就不会丢失。这是Kafka最强的可靠性保证,等效于 acks=-1 。
batch.size当多个消息发送到同⼀个分区的时候,⽣产者尝试将多个记录作为⼀个批来处理。批处理提⾼了客户端和服务器的处理效率。该配置项以字节为单位控制默认批的⼤⼩。所有的批⼩于等于该值。发送给broker的请求将包含多个批次,每个分区⼀个,并包含可发送的数据。如果该值设置的⽐较⼩,会限制吞吐量(设置为0会完全禁⽤批处理)。如果设置的很⼤,⼜有⼀点浪费内存,因为Kafka会永远分配这么⼤的内存来参与到消息的批整合中。
client.id⽣产者发送请求的时候传递给broker的id字符串。⽤于在broker的请求⽇志中追踪什么应⽤发送了什么消息。⼀般该id是跟业务有关的字符串。
compression.type⽣产者发送的所有数据的压缩⽅式。默认是none,也就是不压缩。⽀持的值:none、gzip、snappy和lz4。压缩是对于整个批来讲的,所以批处理的效率也会影响到压缩的⽐例。
send.buffer.bytesTCP发送数据的时候使⽤的缓冲区(SO_SNDBUF)⼤⼩。如果设置为0,则使⽤操作系统默认的。
buffer.memory⽣产者可以⽤来缓存等待发送到服务器的记录的总内存字节(消息收集器)。如果记录的发送速度超过了将记录发送到服务器的速度,则⽣产者将阻塞 max.block.ms 的时间,此后它将引发异常。此设置应⼤致对应于⽣产者将使⽤的总内存,但并⾮⽣产者使⽤的所有内存都⽤于缓冲。⼀些额外的内存将⽤于压缩(如果启⽤了压缩)以及维护运⾏中的请求。long型数据。默认值:33554432,可选值:[0,…]
connections.max.idle.ms当连接空闲时间达到这个值,就关闭连接。long型数据,默认:540000
linger.ms⽣产者在发送请求传输间隔会对需要发送的消息进⾏累积,然后作为⼀个批次发送。⼀般情况是消息的发送的速度⽐消息累积的速度慢。有时客户端需要减少请求的次数,即使是在发送负载不⼤的情况下。该配置设置了⼀个延迟,⽣产者不会⽴即将消息发送到broker,⽽是等待这么⼀段时间以累积消息,然后将这段时间之内的消息作为⼀个批次发送。该设置是批处理的另⼀个上限:⼀旦批消息达到了 batch.size 指定的值,消息批会⽴即发送,如果积累的消息字节数达不到 batch.size 的值,可以设置该毫秒值,等待这么⻓时间之后,也会发送消息批。该属性默认值是0(没有延迟)。如果设置 linger.ms=5 ,则在⼀个请求发送之前先等待5ms。long型值,默认:0,可选值:[0,…]
max.block.ms控制 KafkaProducer.send() KafkaProducer.partitionsFor() 阻塞的时⻓。当缓存(消息收集器)满了或元数据不可⽤的时候,这些⽅法阻塞。在⽤户提供的序列化器和分区器的阻塞时间不计⼊。long型值,默认:60000,可选值:[0,…]
max.request.size单个请求的最⼤字节数。该设置会限制单个请求中消息批的消息个数,以免单个请求发送太多的数据。服务器有⾃⼰的限制批⼤⼩的设置,与该配置可能不⼀样。int类型值,默认1048576,可选值:[0,…]
partitioner.class实现了接⼝ org.apache.kafka.clients.producer.Partitioner 的分区器实现类。默认值为:org.apache.kafka.clients.producer.internals.DefaultPartitioner
receive.buffer.bytesTCP接收缓存(SO_RCVBUF),如果设置为-1,则使⽤操作系统默认的值。int类型值,默认32768,可选值:[-1,…]
security.protocol跟broker通信的协议:PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL.string类型值,默认:PLAINTEXT
max.in.flight.requests.per.connection单个连接上未确认请求的最⼤数量。达到这个数量,客户端阻塞。如果该值⼤于1,且存在失败的请求,在重试的时候消息顺序不能保证。int类型值,默认5。可选值:[1,…]
reconnect.backoff.max.ms对于每个连续的连接失败,每台主机的退避将成倍增加,直⾄达到此最⼤值。在计算退避增量之后,添加20%的随机抖动以避免连接⻛暴。long型值,默认1000,可选值:[0,…]
reconnect.backoff.ms尝试重连指定主机的基础等待时间。避免了到该主机的密集重连。该退避时间应⽤于该客户端到broker的所有连接。long型值,默认50。可选值:[0,…]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值