「Kafka源码美学」之Producer核心流程

67 阅读4分钟

大家好,我是最光阴。最近深入学习了Kafka源码,受益匪浅。因此,我决定分享一些有趣的内容,希望通过这个系列来介绍Kafka源码设计的美学。这个系列将分为两部分:生产者和消费者。

注:本系列是基于Kafka 3.4.0进行源码解析,为了让读者更容易理解,贴出的代码只保留核心流程。

系列目录:

生产者:

「Kafka源码美学」之Producer核心流程

「Kafka源码美学」之RecordAccumulator - 数据累加器

「Kafka源码美学」之BufferPool

「Kafka源码美学」之Sender线程

「Kafka源码美学」之网络发送

消费者:

「Kafka源码美学」之Consumer核心流程

「Kafka源码美学」之ConsumerCoordinator - 协调器

「Kafka源码美学」之Fetcher - 抓取器

「Kafka源码美学」之网络接收

番外:

「Kafka源码美学」之Kafka如何解决粘包、拆包

未完待续

现在让我们开始『kafka源码美学』系列专题的第一篇:生产者流程。在本篇文章中,我们将探索 Kafka 生产者的内部机制,揭示它如何在消息传递方面表现出卓越的性能和可靠性。

首先让我们来看下生产者的架构图:

架构图

image.png

在生产一条消息时,会先后经过拦截器序列化器对消息进行处理,再经过分区器确定消息发送在具体topic下的哪个分区,然后发送到对应的消息累加器中。

sender线程会读取消息累加器中的数据,再包装成Request通过Selector发送到Kafka集群。

可见Kafka的发送流程其实是个异步的流程。

接下来看看Kafka代码中是如何实现的。

客户端代码

package org.kafka;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class Main {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "127.0.0.1:9092");
        props.put("acks", "all");
        props.put("retries", 0);
        props.put("batch.size", 16384);
        props.put("key.serializer", StringSerializer.class.getName());
        props.put("value.serializer", StringSerializer.class.getName());
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

        String[] values = new String[]{"111", "222", "33"};
        ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>("quickstart-events", "key", values.toString());
        producer.send(producerRecord);
        producer.close();

    }
}

Producer初始化

首先分析KafkaProducer的初始化:

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

KafkaProducer(ProducerConfig config,
                  Serializer<K> keySerializer,
                  Serializer<V> valueSerializer,
                  ProducerMetadata metadata,
                  KafkaClient kafkaClient,
                  ProducerInterceptors<K, V> interceptors,
                  Time time) {
        try {
            // ....... 省略一些代码

            // 获取配置的分区器(如果没有配置,则使用默认的org.apache.kafka.clients.producer.DefaultPartitioner),后面用来决定,发送的每条消息是路由到Topic的哪个分区里去的
            this.partitioner = config.getConfiguredInstance(
                    ProducerConfig.PARTITIONER_CLASS_CONFIG,
                    Partitioner.class,
                    Collections.singletonMap(ProducerConfig.CLIENT_ID_CONFIG, clientId));

           
            // 初始化核心组件:RecordAccumulator,它是一个发送消息数据的记录累加器,用于批量发送消息数据
            this.accumulator = new RecordAccumulator(logContext,
                    batchSize,
                    this.compressionType,
                    lingerMs(config),
                    retryBackoffMs,
                    deliveryTimeoutMs,
                    partitionerConfig,
                    metrics,
                    PRODUCER_METRIC_GROUP_NAME,
                    time,
                    apiVersions,
                    transactionManager,
                    new BufferPool(this.totalMemorySize, batchSize, metrics, time, PRODUCER_METRIC_GROUP_NAME));


            // 初始化核心组件:metadata,它记录了生产者的源信息,包括集群信息、topic信息等
            this.metadata = new ProducerMetadata(retryBackoffMs,
                        config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG),
                        config.getLong(ProducerConfig.METADATA_MAX_IDLE_CONFIG),
                        logContext,
                        clusterResourceListeners,
                        Time.SYSTEM);
            this.metadata.bootstrap(addresses);
        
            // 初始化核心组件:sender, 它封装了具体的发送流程
            this.sender = newSender(logContext, kafkaClient, this.metadata);
            String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
            // 创建io线程,执行sender方法
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            // 启动线程
            this.ioThread.start();

            // ....... 省略一些代码
        } catch (Throwable t) {
           
        }
    }

可见KafkaProducer初始化时构建了一系列的核心组件:

  1. Partitioner(分区器),决定消息路由到Topic的哪个分区里
  2. Metadata(元数据),定期向集群拉取元数据信息更新。配置 metadata.max.age.ms 时间间隔,默认是5分钟,默认每隔5分钟一定会强制刷新一下
  3. RecordAccumulator(数据累加器),负责消息的缓冲机制,发送到每个分区的消息会被封装成batch,一般batch size是16kb,默认情况下要凑够一个batch才会发送,但是可以设置 linger.ms,如果在指定时间范围内,都没凑出来一个batch就把这条消息发送出去,比如说5ms,如果5ms还没凑出来一个batch,那么就必须立即把这个消息发送出去
  4. Sender(发送器),负责从缓冲区里获取消息发送到broker上去

其源码设计中充分体现了组合优于继承的思想。

Kafka将各个功能模块进行拆分,每个模块都具有独立的功能和职责,这样可以使得代码更加模块化和可维护。

后续会为每个模块单独写一篇文章详细讲解。

producer.send()


private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
    // 序列化key
    serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
    // 序列号value
    serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
    // 根据分区器算法计算应该往哪个分区中发送消息
    int partition = partition(record, serializedKey, serializedValue, cluster);
    // 将消息追加到 RecordAccumulator中,由accumulator封装成batch发送
    RecordAccumulator.RecordAppendResult result = accumulator.append(record.topic(), partition, timestamp, serializedKey,
                    serializedValue, headers, appendCallbacks, remainingWaitMs, abortOnNewBatch, nowMs, cluster);
    // 如果该批次满了,或者是新创建的批次,则唤醒sender线程,进行消息发送
    if (result.batchIsFull || result.newBatchCreated) {
                log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), appendCallbacks.getPartition());
                this.sender.wakeup();
    }

doSend() 主要是三个步骤:

  1. 序列化 record 的 key 和 value
  2. 获取该 record 要发送到的 partition(可以指定,也可以根据算法计算)
  3. 向 accumulator 中追加 record 数据,数据会先进行缓存(默认32M)

作者简介

曾就职于拼多多,前蚂蚁金服高级Java开发。平时会分享些工作中遇到的技术问题,包括但不限于Java、分布式、大数据。也会分享些理财笔记。关注公众号,了解更多~

image.png