Kafka生产者源码解析(一)——KafkaProducer

在对Spring Boot进行了Kafka整合之后想要对Spring-Kafka生产者发送消息的流程进行一个学习了解,阅读了网上很多优秀的博文和源码剖析文档,也收获了很多,但由于版本的不同存在些许差异,所以决定在2.3.3版的基础之上总结此文,主要目的是让自己加深对Spring-Kafka生产者发送消息流程的理解。虽然版本不同源码内容可能也会有些变化,但Spring-Kafka生产者发送消息的流程的核心逻辑是一样的。

对于Spring-Kafka生产者源码将分成三个部分进行分析:KafkaProducer分析、RecordAccumulator分析、Sender线程分析。本篇是第一部分KafkaProducer分析。

Spring-Kafka生产者源码解析(一)——KafkaProducer

Spring-Kafka生产者源码解析(二)——RecordAccumulator

Spring-Kafka生产者源码解析(三)——Sender

目录

一、KafkaProducer构造函数

二、send方法探析

1、ProducerInterceptors消息拦截器

2、Kafka集群元数据信息更新

3、Serializer序列化器

4、Partitioner分区器

5、追加消息到RecordAccumulator消息累加器

三、总结


KafkaProducer发送消息流程总览

 

流程浅析:消息的发送过程中涉及到两个线程:主线程和Sender线程。主线程将业务逻辑经过拦截器拦截、序列化、分区等操作后封装成ProducerRecord对象,接着调用send方法将消息数据放入到RecordAccumulator中,可以把RecordAccumulator看作一个消息收集器,主线程和Sender线程之间的一个缓冲区,暂存消息数据。Sender线程负责从RecordAccumulator中取出消息数据构成请求,最终执行网络I/O线程批量发送出去,并且在收到响应后执行回调函数。

要分析Kafka生产者发送消息的整个流程,首先需要知道Kafka在Java中发送消息的源头,通过Spring Boot和Kafka的整合,我们知道Spring-Kafka为我们封装好了一个KafkaTemplate,我们只需要注入该对象并调用如下的send方法即可完成消息的发送。

        @Autowired
        private KafkaTemplate<String, Object> kafkaTemplate;
        ……
        ……
        //发送消息,回调结果
        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, obj);

追踪send方法后,我们将目标锁定在一个doSend方法身上。

	protected ListenableFuture<SendResult<K, V>> doSend(final ProducerRecord<K, V> producerRecord) {
		if (this.transactional) {
			Assert.state(inTransaction(),
					"No transaction is in process; "
						+ "possible solutions: run the template operation within the scope of a "
						+ "template.executeInTransaction() operation, start a transaction with @Transactional "
						+ "before invoking the template method, "
						+ "run in a transaction started by a listener container when consuming a record");
		}
		final Producer<K, V> producer = getTheProducer();
		this.logger.trace(() -> "Sending: " + producerRecord);
		final SettableListenableFuture<SendResult<K, V>> future = new SettableListenableFuture<>();
		producer.send(producerRecord, buildCallback(producerRecord, producer, future));
		if (this.autoFlush) {
			flush();
		}
		this.logger.trace(() -> "Sent: " + producerRecord);
		return future;
	}

这段代码的核心是下面这行代码:

producer.send(producerRecord, buildCallback(producerRecord, producer, future));

它首先构造了一个Producer对象,这个Producer是一个接口类,KafkaProducer实现了这个接口,所以此处的Producer对象即为本文的重点内容——KafkaProducer。接着这个producer调用了一个极为重要的send方法,它的第二个参数buildCallback方法返回的是一个Callback接口,用于异步回调结果。本文将围绕KafkaProducer的构造及这个send方法进行分析。

下面我们将从这行代码正式进入生产者的源码探析!!

一、KafkaProducer构造函数

下面是KafkaProducer的构造方法的部分核心源码,先来简单的看看KafkaProducer构造生产者对象时都干了些什么:首先利用参数中的配置信息创建了配置对象,接着利用这个配置对象获取到了partitioner(分区器)、keySerializer(key序列化器)、valueSerializer(value序列化器)、interceptors(拦截器)……,这些都是KafkaProducer类的私有常量,后面都会用到它们;接着创建RecordAccumulator(消息收集器),后面KafkaProducer调用send方法后会用将消息数据存入其中;再然后创建更新Kafka集群的元数据;最后通过newSender()方法创建Sender线程并启动。

    KafkaProducer(Map<String, Object> configs,
                  Serializer<K> keySerializer,
                  Serializer<V> valueSerializer,
                  ProducerMetadata metadata,
                  KafkaClient kafkaClient,
                  ProducerInterceptors interceptors,
                  Time time) {
        //创建生产者配置对象
        ProducerConfig config = new ProducerConfig(ProducerConfig.addSerializerToConfig(configs, keySerializer,
                valueSerializer));
        try {
            …………………………
            //通过反射机制获取到partitioner(分区器)、keySerializer(key序列化器)、valueSerializer(value序列化器)
            this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
            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;
            }
            //获取interceptors拦截器,之后KafkaProducer调用send方法后会用到该拦截器
            List<ProducerInterceptor<K, V>> interceptorList = (List) configWithClientId.getConfiguredInstances(
                    ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptor.class);
            if (interceptors != null)
                this.interceptors = interceptors;
            else
                this.interceptors = new ProducerInterceptors<>(interceptorList);

            //创建RecordAccumulator(消息收集器),之后KafkaProducer调用send方法后会用将消息数据存入其中
            this.accumulator = new RecordAccumulator(……);
            
            //创建更新Kafka集群的元数据
            List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(
                    config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG),
                    config.getString(ProducerConfig.CLIENT_DNS_LOOKUP_CONFIG));
            if (metadata != null) {
                this.metadata = metadata;
            } else {
                this.metadata = new ProducerMetadata(retryBackoffMs,
                        config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG),
                        logContext,
                        clusterResourceListeners,
                        Time.SYSTEM);
                this.metadata.bootstrap(addresses, time.milliseconds());
            }
            //创建Sender线程
            this.sender = newSender(logContext, kafkaClient, this.metadata);
            String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
            //启动Sender对应的线程
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            this.ioThread.start();
            ……………………
        } catch (Throwable t) {
            ……………………
        }
    }

再来看一下是如何创建Sender线程的。

    Sender newSender(LogContext logContext, KafkaClient kafkaClient, ProducerMetadata metadata) {
        ………………
        //创建NetworkClient,这是Kafka网络I/O的核心
        KafkaClient client = kafkaClient != null ? kafkaClient : new NetworkClient(new Selector(………………), ………………);
        ………………
        //返回Sender线程对象
        return new Sender(………………);
    }

去掉一些非核心代码,发现newSender方法要做的事情其实很简单:创建NetworkClient,这是Kafka网络I/O的核心,在后面发送消息请求时会用到;最后创建Sender对象,Sender实现了Runnable接口,是个线程类。至于Sender线程都做了什么我们现在并不需要太关心,毕竟本文的主角并不是它,我们把它留着在后面的文章中单独分析。

二、send方法探析

构造完KafkaProducer对象之后,接着就会调用它的send方法,所以下面我开始关注send方法。

    @Override
    public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        // 拦截器,可在发送消息之前对消息进行拦截修改
        ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
        return doSend(interceptedRecord, callback);
    }

可以看到在发送消息之前,我们可以利用之前获取的拦截器对消息进行拦截修改,然后调用了一个doSend方法,该方法将会完成更新kafka集群元数据信息、对Key和Value进行序列化、分区选择、追加消息到RecordAccumulator消息累加器中、唤醒Sender线程的操作。下面将围绕这些内容进行分析。

1、ProducerInterceptors消息拦截器

ProducerInterceptors其实是一个ProducerInterceptor拦截器的集合,它的onSend方法只不过是在循环遍历这些拦截器,并调用每个拦截器的onSend方法,源码如下:

    public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {
        ProducerRecord<K, V> interceptRecord = record;
        //循环遍历拦截器
        for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
            try {
                //调用每个拦截器的onSend方法
                interceptRecord = interceptor.onSend(interceptRecord);
            } catch (Exception e) {
                ………………
        }
        return interceptRecord;
    }

ProducerInterceptor是一个接口,所以如果我们需要写自己的拦截逻辑时,只需要去实现这个接口,将自己的拦截逻辑放在onSend方法中即可。

2、Kafka集群元数据信息更新

消息经过拦截修改后进入到doSend方法,若没有指定分区,后面将会使用Cluster信息计算分区号,因此在此之前需要获取最新的Cluster集群信息。下面是doSend方法中涉及到元数据信息更新的代码部分,其余部分省略。

    private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        ………………
        ClusterAndWaitTime clusterAndWaitTime;
        try {
            //等待元数据更新
            clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
        } catch (KafkaException e) {
            ………………
        }
        //获取到Cluster集群最新信息
        Cluster cluster = clusterAndWaitTime.cluster;
        ………………
        //计算分区号
        int partition = partition(record, serializedKey, serializedValue, cluster);
        ………………
    }

进入waitOnMetadata方法源码,可以看到这里的逻辑主要是判断metadata中的元数据信息是否需要更新,当需要更新时,则通过do-while循环进行更新,其中核心部分是通过metadata.awaitUpdate()方法阻塞当前线程,等待Sender线程向远程服务器发起元数据更新请求,直到远程服务器返回了新的元数据信息才唤醒当前线程,最终返回最新的cluster元数据信息。

    private ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long maxWaitMs) throws InterruptedException {
        //通过metadata获取cluster信息, metadata之前已经在KafkaProducer构造方法中获取到
        Cluster cluster = metadata.fetch();

        ……………………

        //将topic加入到metadata中进行维护
        metadata.add(topic);
        //从cluster信息中获取topic的分区数
        Integer partitionsCount = cluster.partitionCountForTopic(topic);
        //如果partitionsCount不为空则说明metadata中已经维护了该topic的元数据,并且需要更新的分区号未定义或者在已知的分区范围内
        //则直接返回metadata中的cluster信息
        if (partitionsCount != null && (partition == null || partition < partitionsCount))
            return new ClusterAndWaitTime(cluster, 0);

        ………………

        //如果metadata中没有维护该topic的元数据,或者需要更新的分区号是新的时,则进行metadata的更新。
        //do-while循环更新
        do {

            …………

            //将topic加入到metadata中进行维护
            metadata.add(topic);
            //获取当前元数据版本号
            int version = metadata.requestUpdate();
            //唤醒sender线程
            sender.wakeup();
            try {
                //阻塞等待元数据更新结束
                metadata.awaitUpdate(version, remainingWaitMs);
            } catch (TimeoutException ex) {
                ……………………
            }
            //拿到更新后的集群信息
            cluster = metadata.fetch();
            elapsed = time.milliseconds() - begin;
            //检测超时时间
            if (elapsed >= maxWaitMs) {
                ……………………
            }

            ……………………

        } while (partitionsCount == null || (partition != null && partition >= partitionsCount));
        //返回更新后的cluster信息
        return new ClusterAndWaitTime(cluster, elapsed);
    }

3、Serializer序列化器

Kafka发送的消息是在网络上进行传输,所以,doSend方法还会通过keySerializer和valueSerializer将我们的消息进行序列化。producer端需要序列化,consumer端需要反序列化。下面是doSend方法中涉及到消息序列化的代码部分,其余部分省略。

    private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        ………………
        byte[] serializedKey;
            try {
                //使用keySerializer将key进行序列化
                serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
            } catch (ClassCastException cce) {
                ………………
            }
            byte[] serializedValue;
            try {
                //使用valueSerializer将value进行序列化
                serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
            } catch (ClassCastException cce) {
                ………………
            }
        ………………
    }

4、Partitioner分区器

我们的消息最终都会发往一个合适的分区,如果我们在ProducerRecord消息记录中已经给partition字段指定好了分区号,那么将会优先选择此分区,否则将会通过partitioner.partition()方法为我们选择一个合适的分区。下面是doSend方法中涉及到计算分区的代码部分,其余部分省略。

    private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        ………………
        //计算分区
        int partition = partition(record, serializedKey, serializedValue, cluster);
        ………………
    }

进入partition方法。

    private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
        //获得ProducerRecord中的partition字段值
        Integer partition = record.partition();
        //如果ProducerRecord中partition字段已经设置了分区号,则直接返回该分区号,否则调用分区器进行计算合适的分区号
        return partition != null ?
                partition :
                partitioner.partition(
                        record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
    }

好家伙,里面还有个partition方法,继续进入核心的partitioner.partition()方法:

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        //从cluster中获取topic的分区信息
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        //获得分区数量
        int numPartitions = partitions.size();
        //如果消息没有key
        if (keyBytes == null) {
            //递增counter,用于后面取模运算
            int nextValue = nextValue(topic);
            //选择availablePartitions 
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                return availablePartitions.get(part).partition();
            } else {
                //返回一个不可用的分区
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            // 如果消息有key的情况
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

最后来总结下partitioner.partition()方法的流程:

1、首先是通过cluster获取到topic的分区信息,从而获得分区数量。

2、接下来会有两种情况:当我们发送的消息没有key和有key两种情况。

(1)消息没有key时:先通过nextValue()方法递增counter返回一个int型的变量给nextValue,然后获取该topic可用的分区存入list中,如果可用分区数大于0,则将刚才的nextValue和可用分区数取模运算,最后得出分区号结果;如果可用分区数小于等于0,则返回一个不可用的分区。

(2)消息有key时:获取key的hash值然后和分区数进行取模运算,得出分区号结果。

5、追加消息到RecordAccumulator消息累加器

最后doSend方法会将我们的消息追加到accumulator消息累加器中,然后唤醒Sender线程。下面是doSend方法中涉及到追加消息入RecordAccumulator的代码部分,其余部分省略。

    private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        ………………
        //将消息追加到accumulator中
        RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
                    serializedValue, headers, interceptCallback, remainingWaitMs);
        //若消息存储器满了或者第一次创建消息存储器则唤醒Sender线程
        if (result.batchIsFull || result.newBatchCreated) {
            //唤醒Sender线程
            this.sender.wakeup();
        }
        ………………
    }

这个步骤的核心实现是通过accumulator.append()方法将我们的消息追加到消息累加器中,至于这个方法的实现细节将留到下一篇中进行详细分析。

三、总结

本文的主角是KafkaProducer,到目前为止所讲到的内容都是在主线程完成的工作,Kafka发送消息有同步和异步两种方式,但其实两者的底层实现都是通过异步来实现的,主线程调用producer.send()方法发送消息,先将消息放到了RecordAccumulator消息累加器中暂存,然后主线程就可以从send线程返回,需要注意的是此时消息并没有真正的发送给Kafka。之后KafkaProducer可能会根据业务需求不断地向RecordAccumulator中发送消息,当满足一定的条件后就会唤醒Sender线程来发送RecordAccumulator中累积起来的消息。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大何向东流1997

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值