(Kafka源码一)生产者初始化及分区策略

本文详细探讨了Kafka生产者的配置过程,包括Producer属性配置、分区策略(默认和自定义)、RecordAccumulator的使用以及sender线程的运行机制。重点展示了如何通过配置发送消息到不同的分区方式。
摘要由CSDN通过智能技术生成

本次阅读源码采用的方法是场景驱动的方式,从Demo入手,先对代码的整体运行流程进行大概了解,对于不重要的代码"不求甚解,观其大略",再对重要的源码进行深度剖析。

(一)生产者

Kafka有producer,broker,consumer三部分组成,producer和consumer属于客户端,broker属于服务端,producer将消息写入broker中,consumer从broker中读取消息。

1.Producer属性配置

首先我们来看看生产者属性配置的代码

public class ProducerTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //给Kafka配置对象添加配置信息
        Properties properties = new Properties();
        //服务信息
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.214.128:9092");
        //配置key的序列化
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //配置value的序列化
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        //设置事务id
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction_id_01");
        // 每批次的消息大小,默认16K
        properties.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "16384");
        // 内存池的总大小,默认32M
        properties.setProperty(ProducerConfig.BUFFER_MEMORY_CONFIG, "33554432");
        // socket发送的缓冲区默认大小128KB
        properties.setProperty(ProducerConfig.SEND_BUFFER_CONFIG, "32768");
        // socket接收的缓冲区默认大小
        properties.setProperty(ProducerConfig.RECEIVE_BUFFER_CONFIG, "32768");
        //创建生产者对象
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        //初始化事务
        producer.initTransactions();
        //开启事务
        producer.beginTransaction();
        try{
            //发送消息
            for (int i = 0; i < 5; i++) {
                producer.send(new ProducerRecord<String, String>("first", "wang wu" + i), new Callback() {
                    public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                        if(e == null){
                            System.out.println("主题"+recordMetadata.topic()+" 分区"+recordMetadata.partition());
                        }
                    }
                });
                Thread.sleep(20);
            }
            //提交事务
            producer.commitTransaction();
        }catch (Exception e){
            //回滚事务
            producer.abortTransaction();
        }finally {
            //关闭生产者资源
            producer.close();
        }
    }
}

首先初始化一个Properties对象,往该对象加入一些配置信息,生产者初始化的时候就会读取这些配置信息。

2. Producer初始化

先来看看KafkaProducer实例化的代码,这里只展示重要代码,对于像日志的一些代码就不贴出来了。

@SuppressWarnings({"unchecked", "deprecation"})
    private KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
        try {
            // 配置用户之前自定义的一些参数值
            Map<String, Object> userProvidedConfigs = config.originals();
            this.producerConfig = config;
            this.time = new SystemTime();
            //配置客户端ID
            clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG);
            //设置分区器
            this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
            //当生产者发送消息失败的时候会进行重试,重试的时间间隔retry.backoff.ms默认100ms
            long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
            //设置key和value的序列化器
            if (keySerializer == null) {
                this.keySerializer = config.getConfiguredInstance(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                        Serializer.class);
                this.keySerializer.configure(config.originals(), true);
            } else {
                config.ignore(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG);
                this.keySerializer = keySerializer;
            }
            if (valueSerializer == null) {
                this.valueSerializer = config.getConfiguredInstance(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                        Serializer.class);
                this.valueSerializer.configure(config.originals(), false);
            } else {
                config.ignore(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG);
                this.valueSerializer = valueSerializer;
            }
            //设置拦截器
            List<ProducerInterceptor<K, V>> interceptorList = (List) (new ProducerConfig(userProvidedConfigs)).getConfiguredInstances(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
                    ProducerInterceptor.class);
            this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList);
            //创建元数据实例对象
            this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners);
            //发送一条消息的最大容量默认是1M,在生产环境中,经验值是10M。
            this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
            //内存池的容量大小默认值是32M
            this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
            //设置消息的压缩格式,从而提高系统的吞吐量
            this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));
            //创建消息记录累加器
            this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
                    this.totalMemorySize,
                    this.compressionType,
                    config.getLong(ProducerConfig.LINGER_MS_CONFIG),
                    retryBackoffMs,
                    metrics,
                    time);
						//解析并验证broker地址的有效性
            List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
            //生产者默认每隔5分钟去更新元数据(初始化的时候没有去拉取)
            this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());
             //初始化网络客户端对象
            NetworkClient client = new NetworkClient(
            				//生产者连上broker超过最大空闲时间时就关闭这个网络连接。最大空闲时间默认值是9分钟
                    new Selector(config.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG), this.metrics, time, "producer", channelBuilder),
                    this.metadata,
                    clientId,
                    //生产者发送给broker能够缓存还没有接收到响应的最大请求个数,默认是5个
                    config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION),
                    //设置重试时间间隔,默认100ms
                    config.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG),
                    //设置发送缓冲区的最大大小,默认128KB
                    config.getInt(ProducerConfig.SEND_BUFFER_CONFIG),
                    //设置接收缓冲区的默认大小,默认32KB
                    config.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG),
                    this.requestTimeoutMs, time);
            //初始化sender线程
            this.sender = new Sender(client,
                    this.metadata,
                    this.accumulator,
                    config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION) == 1,
                    config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
                    (short) parseAcks(config.getString(ProducerConfig.ACKS_CONFIG)),
                    config.getInt(ProducerConfig.RETRIES_CONFIG),
                    this.metrics,
                    new SystemTime(),
                    clientId,
                    this.requestTimeoutMs);
            //创建了一个Kafka线程,传入sender对象。
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            //启动Kafka线程。
            this.ioThread.start();
    }

2.1消息分区策略

Producer在初始化的时候,会使用默认的分区器DefaultPartitioner,this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);Partitioner是一个接口,据此可以实现该接口实现自定义的分区策略。
自定义分区的例子

public class MyPartition implements Partitioner {
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        String msg = o1.toString();
        //若消息包含hi则发送到1号分区
        if(msg.contains("hi")) return 1;
        //否则发送到0号分区
        else return 0;
    }

    public void close() {

    }

    public void configure(Map<String, ?> map) {

    }
}

再来看看Kafka默认的分区器实现

public interface Partitioner extends Configurable {}
public class DefaultPartitioner implements Partitioner {
    //初始化的时候,定义一个原子类型的计数器
    private final AtomicInteger counter = new AtomicInteger(new Random().nextInt());
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        //获取消息要发往的topic的分区的信息
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        //计算出来分区的总的个数
        int numPartitions = partitions.size();
        //情况一,如果发送消息的时候,没有指定key
        if (keyBytes == null) {
            //计数器每次执行都会自增1,对分区进行轮询,从而实现负载均衡的效果
            int nextValue = counter.getAndIncrement();
            //获取可用的分区数
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            //当可用分区数大于1,将计数器对可用分区数取余得到目标分区号
            if (availablePartitions.size() > 0) {
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                //根据这个值分配分区好。
                return availablePartitions.get(part).partition();
            } else {
                //当没有可用分区数时,将计数器对总分区数取余
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            //情况2,在指定key的情况下,将key的hash值对总分区数取余,通过指定同一个key可以将消息发往同一个分区.
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }
}

可以看出,当需要将消息发往同一个分区时,可以指定key,同一个key的hash值是相同的,将hash值对总分区数取余得到的分区号肯定是一样的;或者自定义分区器实现Partitioner接口将消息发送到指定分区。

2.2重要组件的初始化

在这里插入图片描述生产者将消息批次发送到RecordAccumulator的双端队列中,当满足一定条件时(后面会详细介绍)就会唤醒sender线程从队列中取出消息,通过NetworkClient将消息发送至broker。
首先是RecordAccumulator的初始化代码。

 this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
                    this.totalMemorySize,
                    this.compressionType,
                    config.getLong(ProducerConfig.LINGER_MS_CONFIG),
                    retryBackoffMs,
                    metrics,
                    time);

先介绍下RecordAccumulator是怎么存储消息的,在这个类里面有一个用ConcurrentMap实现的数据结构,key是主题分区,value是用双端队列来存储消息批次RecordBatch。

public final class RecordAccumulator {
//TopicPartition -> Deque<RecordBatch> 双端队列
    private final ConcurrentMap<TopicPartition, Deque<RecordBatch>> batches;

在这里插入图片描述
可以知道每个主题分区对应一个双端队列,该队列用来存储发送该分区的消息批次。
再来看看sender对象

 this.sender = new Sender(client,
                  this.metadata,
                    this.accumulator,
                    config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION) == 1,
                    config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
                    (short) parseAcks(config.getString(ProducerConfig.ACKS_CONFIG)),
                    config.getInt(ProducerConfig.RETRIES_CONFIG),
                    this.metrics,
                    new SystemTime(),
                    clientId,
                    this.requestTimeoutMs);
                     //创建了一个Kafka线程,传入sender对象。
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            //启动Kafka线程。
            this.ioThread.start();

sender对象实现了Runnable接口,本质上就是一个线程

public class Sender implements Runnable {}

而KafkaThread继承了Thread,本身就是一个线程。

public class KafkaThread extends Thread {

创建完sender线程后将其Kafka线程中,然后启动,此时sender线程就已经开始运行了,消息的发送,响应的接收以及处理都是由sender线程处理的。

3.sender线程

首先来看看sender线程的run方法

public class Sender implements Runnable {
public void run() {
     	//这里是一个while死循环,sneder线程是一经启动就一直在运行
        while (running) {
            try {
                // 核心代码
                run(time.milliseconds());
            } catch (Exception e) {
                log.error("Uncaught error in kafka producer I/O thread: ", e);
            }
        }
    }
 		//上面的run方法调用下面的run方法
    void run(long now) {
			//步骤一:获取元数据
        Cluster cluster = metadata.fetch();
     //步骤二:获取要发送partiton的leader副本对应的broker
        RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
     		//对没有拉取到元数据的topic进行标记,以便下一次拉取该主题的元数据
        if (!result.unknownLeaderTopics.isEmpty()) {
            for (String topic : result.unknownLeaderTopics)
                this.metadata.add(topic);
            this.metadata.requestUpdate();
        }
        Iterator<Node> iter = result.readyNodes.iterator();
        long notReadyTimeout = Long.MAX_VALUE;
        while (iter.hasNext()) {
            Node node = iter.next();          
             //步骤四:检查生产者要发送数据的broker的网络是否已经建立好。
                if (!this.client.ready(node, now)) {
                //如果与目标broker的网络没有建立好,则从集合中移除该broker
                //第一次进来的时候所有主机的网络都没有建立好,所以这里会将所有的broker移除集合
                iter.remove();
                notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
            }
        }    
         // 步骤五:将消息批次按照broker分组,发往同一个broker的分区归为一组,这样可以减少网络请求的传输次数
        Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,
                                                                         result.readyNodes,
                                                                         this.maxRequestSize,
                                                                         now);
    	//步骤六:对超时的消息批次进行处理
        List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now);    
      	//步骤七:创建发送消息的请求
        //第一次运行到这里的时候与broker的网络连接没有建立好 batches是为空,所以这段代码不会执行
        List<ClientRequest> requests = createProduceRequests(batches, now);       
        //发送请求的操作
        for (ClientRequest request : requests)
            //绑定 op_write事件
            client.send(request, now);
         //步骤八:第一次运行的时候首先通过下面的方法先拉取元数据,然后建立网络连接,发送请求,以及响应的接收和处理都是由这个方法完成的
        this.client.poll(pollTimeout, now);
    }

(二)总结

本文主要讲解了生产者初始化时的一些配置属性,以及消息的3种分配策略,分别是自定义分区,随机指定分区,通过key的hash指定分区三种,还介绍了RecordAccumulator是如何存储消息的,最后分析了sender线程的初始化以及代码的运行流程。下一篇将详细介绍sender线程是如何建立网络连接,发送消息以及处理响应。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值