Bootstrap

Kafka源码详解3-Kafka生产者消息生产源码详解

        Kafka生产者是Kafka消息队列的消息发送端,负责将消息从客户端发送到 Kafka 集群的指定 Topic 下的各个 Partition分区中。并可提供消息预处理、数据序列化、数据压缩、传输确认等数据处理与传输功能。

        本文内容为KafkaProducer消息生产过程的源码解析,详细解析了Kafka消息生产中,数据预处理、元数据更新、数据序列化、消息分区、写入消息累加器等各个环节的Kafka源码,完整展现了KafkaProducer进行消息生产的各步骤操作细节。

1.KafkaProducer消息生产功能概述

        KafkaProducer的消息生产功能,具体过程包括:拦截器的数据预处理、元数据更新、消息Key与Value的数据序列化、消息的Partition分区ID生成,最终写入RecoredAccumulator消息累加器中,供Sender拉取并向Kafka集群发送。

具体步骤如下:

        1.KafkaProducer通过遍历ProducerInterceptors拦截器对需要发送的Record消息进行数据预处理。

        2.若待发送Record消息所在的Topic和其Partition分区不在本地MetadataCache元数据缓存,KafkaProducer需通过Sender更新元数据。

        3.KafkaProducer通过KeySerializer/ValueSerializer序列化器将Record的Key/Value序列化为byte[]字节数组。

        4.KafkaProducer通过Partitioner分区器生成Record的Partition分区ID。

        5.KafkaProducer把Record追加写入RecordAccumulator消息累加器对应Partition的Deque队列的最后一个ProducerBatch中。

完整的KafkaProducer数据生产过程详解如下:

完整代码解析:

        下面开始具体展开Kafka消息生产的各步骤的源码解析。

2.消息生产程序入口

        从Kafka Api中可以看出,无论同步还是异步发送,当调用KafkaProducer.send()方法时,程序进入消息生产与发送逻辑。因此程序入口为:KafkaProducer.send()方法。

//同步发送消息
producer.send(record)

//异步发送消息
producer.send(record, new Callback() {
    @Override
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        if(exception == null) {
            // 消息发送成功
            System.out.println("消息发送成功");
        } else {
            // 消息发送失败,需要重新发送
            System.out.println("消息发送失败");
        }
    }
});

        为统一同步与异步发送,同步发送会多封装一层,传递参数callback为null,继续调用异步发送使用的带callback参数的KafkaProducer.send()方法。

源码图解:

KafkaProducer.send()-不带callback参数-方法源码:

public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
        return send(record, null);
    }

        进入带callback参数的KafkaProducer.send()后开始进入了正式的消息生产逻辑。

KafkaProducer.send()-带callback参数-方法源码:

public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
    //通过拦截器对record消息进行数据预处理
    ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
    
    //继续调用doSend()方法进入后续数据处理
    return doSend(interceptedRecord, callback);
}

        从KafkaProducer.send()方法可以看出消息生产第一个步骤是通过拦截器进行数据预处理。

3. 通过ProducerInterceptors拦截器对消息进行数据预处理

        KafkaProducer对Record消息进行数据预处理,是通过遍历ProducerInterceptors拦截器实现的。

        在 KafkaProducer.send()方法中, KafkaProducer调用ProducerInterceptors拦截器的onSend()方法进行数据预处理,ProducerInterceptors.onSend()方法遍历了所有的ProducerInterceptors拦截器,调用每个ProducerInterceptor拦截器的onSend()方法对待发送的Record消息进行预处理。       

   源码图解:   

       KafkaProducer.send()方法首先调用了ProducerInterceptors的onSend() 方法。

 KafkaProducer.send()方法源码: 

public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
    //通过拦截器对record消息进行数据预处理
    ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
    
    //继续调用doSend()方法进入后续数据处理
    return doSend(interceptedRecord, callback);
}

       ProducerInterceptors的onSend() 方法遍历了所有的拦截器,依次调用每个ProducerInterceptor拦截器的onSend() 进入具体预处理逻辑。

ProducerInterceptors.onSend()方法源码: 

public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {

    //...    
    
    ProducerRecord<K, V> interceptRecord = record;
    //遍历每个拦截器进行具体的数据预处理逻辑
    for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
        interceptRecord = interceptor.onSend(interceptRecord);
		}
    return interceptRecord;

    //...  
}

      数据预处理功能,用户可编写自定义拦截器实现org.apache.kafka.clients.producer .ProducerInterceptor接口的onSend()方法来研发自己的数据预处理逻辑。 

ProducerInterceptor接口源码:

public interface ProducerInterceptor<K, V> extends Configurable, AutoCloseable {

    //数据预处理方法
    ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);

    void onAcknowledgement(RecordMetadata metadata, Exception exception);

    void close();

}

      处理完拦截器数据预处理后,KafkaProducer.send()继续调用KafkaProducer的doSend()方法继续执行后续消息生产逻辑。

//继续调用doSend()方法进入后续数据处理
return doSend(interceptedRecord, callback);

    KafkaProducer的doSend()方法实现了消息生产的主逻辑,包括:获取集群的元数据;消息的数据序列化;消息的Partition分区ID生成;最终将消息追加写入RecordAccumulator消息累加器的最后一个ProducerBatch中。

源码图解:   

ProducerInterceptors.onSend()方法源码: 

private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
    //...

    //获取元数据
    clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), nowMs, maxBlockTimeMs);

    //从结果中获取已更新好的元数据Cluster
    Cluster cluster = clusterAndWaitTime.cluster;

    //...    
    
    //序列化Key为字节数组byte[]
    serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());

    //序列化Value为字节数组byte[]
    serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());

    //...

    //通过分区器生成消息所在的PartitionID
    int partition = partition(record, serializedKey, serializedValue, cluster);

    //...

    //把消息放到RecordAccumulator消息累加器
    RecordAccumulator.RecordAppendResult result = accumulator.append(record.topic(), partition, timestamp, serializedKey, serializedValue, headers, appendCallbacks, remainingWaitMs, abortOnNewBatch, nowMs, cluster);

    //若需要生成新一个的ProducerBatch
    if (result.abortForNewBatch){
       //新建ProducerBatch
       onNewBatch(record.topic(), cluster, prevPartition);
       partition = partition(record, serializedKey, serializedValue, cluster);

       //在RecordAccumulator加入新的ProducerBatch
      result = accumulator.append(record.topic(), partition, timestamp, serializedKey,          serializedValue, headers, appendCallbacks, remainingWaitMs, false, nowMs, cluster);
    }

    //如果batch满了或者生成了新的batch
    if (result.batchIsFull || result.newBatchCreated) {
       //唤醒Sender线程处理消息
       this.sender.wakeup();
    }

    //...

    //最终返回写入RecordAccumulator的结果
    return result.future;

}

        下面进入KafkaProducer.doSend()方法每项功能的具体解析。

4.获取Kafka元数据,检查元数据是否需要更新

        每次消息写入时,KafkaProducer会检查本地元数据缓存是否包含本次写入的消息的Topic与partition,若不存在,则需要拉取集群最新的元数据。

        Kafka Producer会从MetadataCache元数据缓存中取出当前Kafka集群的元数据信息,若当前消息需要发送到的Topic和其Partition不在MetadataCache元数据缓存中,则需要通过Sender向Kafka集群重新拉取元数据。

源码图解:

        首先进入KafkaProducer.doSend()方法,方法调用了waitOnMetadata()方法判断元数据是否需要更新,并把最新的Kafka元数据封装为Cluster。

KafkaProducer.doSend()方法元数据更新的源码: 

//检查元数据是否需要更新
clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), nowMs, maxBlockTimeMs);

//从结果中获取最新的元数据Cluster
Cluster cluster = clusterAndWaitTime.cluster;

        KafkaProducer.waitOnMetadata()方法为判断元数据是否需要更新的方法。首先在本地元数据存储中查找消息的topic和partiton是否存在,存在则返回,不存在则执行后面do-while()逻辑进行元数据的更新拉取逻辑。

 KafkaProducer.waitOnMetadata()方法源码:

private ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long nowMs, long maxWaitMs) throws InterruptedException {
//从元数据缓存MetadataCache中取出当前集群元数据信息
	Cluster cluster = metadata.fetch();

	//加入当前topic和访问时间到元数据缓存MetadataCache
	metadata.add(topic, nowMs);
	
	//获取当前topic的partition数目
	Integer partitionsCount = cluster.partitionCountForTopic(topic);
	
	//如果分区存在(元数据以获取)则返回元数据和等待时间
	if (partitionsCount != null && (partition == null || partition < partitionsCount))
	            return new ClusterAndWaitTime(cluster, 0);
	            
	//如果分区不存在,则执行下面do-while()逻辑更新元数据
	do {
	   //如果元数据不存在这个topic,加入当前topic和访问时间到元数据缓存MetadataCache
	   metadata.add(topic, nowMs + elapsed);
	   
	   //唤醒Sender线程获取元数据
	   sender.wakeup();
	   
	   //等待sender完成元数据更新
	   metadata.awaitUpdate(version, remainingWaitMs);
	   
	   //更新完元数据后,更新cluster实例(封装元数据信息)
	   cluster = metadata.fetch();
	   
	   //返回cluster封装的元数据
	   return new ClusterAndWaitTime(cluster, elapsed);
	   	   
	} while (partitionsCount == null || (partition != null && partition >= partitionsCount));

}

        若不存在则需要更新元数据,元数据的更新拉取是通过唤醒Sender向Kafka集群拉取实现的,调用了sender.wakeup()方法通过Sender拉取,并通过metadata.awaitUpdate()方法获取拉取后最新的元数据。

        Sender底层通讯是通过NetworkClient实现的,因此Sender又唤醒了NetworkClient进行具体的数据通信。

Sender.wakeup()方法源码:

public void wakeup() {
    this.client.wakeup();
}

        具体NetworkClient拉取元数据的源码解析,将放在后文介绍 Sender与NetworkClient进行数据发送时详细解析。

5. 通过序列化器进行消息的Key与Value的数据序列化

        在消息Record发送前,KafkaProducer需要把Key与Value序列化成byte[]字节数组,将应用数据序列化为字节流,以实现高效的消息传输。

        KafkaProducer.doSend()方法,KafkaProducer调用了keySerializer和valueSerializer序列化器的serialize()方法对消息进行了数据序列化。序列化器的serialize()方法为数据序列化的内容,具体代码为把当前数据类型转换为byte[]字节数组的序列化逻辑。

源码图解:   

        在KafkaProducer.doSend()方法,KafkaProducer分别调用了keySerializer和valueSerializer序列化器的serialize()方法,对消息的Key和Value进行了序列化。

KafkaProducer.doSend()方法序列化部分的源码: 

//序列化Key为字节数组byte[]
serializedKey = keySerializer.serialize(record.topic(),record.headers(), record.key());
 
//序列化Value为字节数组byte[]
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());

       序列化器的serialize()为具体序列化的逻辑,以StringSerializer为例,serialize()方法中包含了字符串转换为byte[]字节数组的逻辑。

StringSerializer.serialize()方法源码:

public class StringSerializer implements Serializer<String> {
		private Charset encoding = StandardCharsets.UTF_8;
    
    //...
		
	//序列化方法
    @Override
    public byte[] serialize(String topic, String data) {
        if (data == null)
            return null;
        else
            //将字符串转换为byte[]数组
            return data.getBytes(encoding);
    }
}

自定义序列化器: 

       研发时也可编写自定义序列化器拓展数据序列化功能,可通过实现org.apache.kafka.common.serialization.Serializer接口,重写serialize()方法实现自己的序列化逻辑。

Serializer接口源码:

public interface Serializer<T> extends Closeable {

    //序列化方法
    byte[] serialize(String topic, T data);

    //...
}

6.通过Partitioner分区器生成消息的Partition分区ID

        KafkaProducer需要为每个Record消息生成Partition分区ID,分配到指定的Partition分区,为消息进行路由选择。

        在KafkaProducer.doSend()方法中,KafkaProducer先调用了自己的partition()方法,KafkaProducer.partition()方法又具体调用了partitioner序列化器的partition()方法,完成对对消息生成partition分区ID的功能。

源码图解:

        KafkaProducer的分区功能首先是在KafkaProducer.doSend()方法中调用了KafkaProducer自身的partition()方法。

KafkaProducer.doSend()分区部分的源码:

//通过分区器生成消息所在的PartitionID
int partition = partition(record, serializedKey, serializedValue, cluster);

        KafkaProducer.partition()中又调用了partitioner分区器的partition()方法执行具体的分区逻辑。

KafkaProducer.partition()方法源码:

private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {

    //...

    //通过调用partitioner分区器的partition()进行分区id生成
    int customPartition = partitioner.partition(
          record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);

    //返回值为生成的partition分区id
    return customPartition;
      
    //...
}

         Kafka在org.apache.kafka.clients.producer.internals包中内置了DefaultPartitioner默认分区器,会根据Key的哈希值为Record生成Partition分区ID。研发时也可编写自定义分区器,通过实现org.apache.kafka.clients.producer.Partitioner接口,重写partition()方法实现自己的分区逻辑。

源码图解:

        以默认分区器DefaultPartitioner为例。

DefaultPartitioner.partition()方法源码:

public class DefaultPartitioner implements Partitioner {
    
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster, int numPartitions) {
        //...

        //通过BuiltInPartitioner进行分区
        return BuiltInPartitioner.partitionForKey(keyBytes, numPartitions);
    }

    //...
}

        再看BuiltInPartitioner.partitionForKey()方法的具体分区逻辑

BuiltInPartitioner.partitionForKey()方法源码:

public class BuiltInPartitioner {
    
    //...

    //通过对序列化key做hash,再取模到partition的范围内,完成对partitionID的生成。
    public static int partitionForKey(final byte[] serializedKey, final int numPartitions) {
        return Utils.toPositive(Utils.murmur2(serializedKey)) % numPartitions;
    }

    //...

}

        BuiltInPartitioner.partitionForKey()方法通过对序列化key做hash,再取模到partition的范围内,完成对partitionID的生成。

自定义分区器:

        在研发时,也通过实现org.apache.kafka.clients.producer.Partitioner接口,重写partition()方法实现自己的序列化逻辑。

Partitioner接口源码:

public interface Partitioner extends Configurable, Closeable {

    //分区方法
    int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);

    //...
}

 

7.把消息加入RecordAccumulator消息累加器

        在完成拦截器的数据预处理、序列化器的数据序列化、分区器的分区ID生成这一系列数据操作后,KafkaProducer需把处理好的Record写入RecordAccumulator消息累加器中,供Sender取走发送给Kafka集群。

       在KafkaProducer.doSend()方法中,通过调用RecordAccumulator消息累加器的append()方法将消息加入到消息累加器中,并提供ProducerBatch写满后,生成新Batch及唤醒Sender发送数据的逻辑。RecordAccumulator.append()则包含了具体的消息写入逻辑,先找到消息所在Partition的Deque队列,再通过tryAddend()方法将消息写入到队列中最后一个ProducerBatch,完成具体数据写入。

源码图解:

        在KafkaProducer.doSend()方法中,KafkaProducer先调用了RecordAccumulator消息累加器的append()方法将消息加入到消息累加器中消息对应partition的队列的最后一个ProducerBatch上。

        若出现需要生成新的ProducerBatch的情况,KafkaProducer会生成新的ProducerBatch,并添加到RecordAccumulator消息累加器中。若写满或有新的ProducerBatch生成,KafkaProducer会唤醒Sender,取出RecordAccumulator的ProducerBatch,进行数据发送。

KafkaProducer.doSend()方法写入消息累加器的源码:

//把消息放到RecordAccumulator消息累加器
RecordAccumulator.RecordAppendResult result = accumulator.append(record.topic(), partition, timestamp, serializedKey, serializedValue, headers, appendCallbacks, remainingWaitMs, abortOnNewBatch, nowMs, cluster);

//若生成新一个的ProducerBatch
if (result.abortForNewBatch){
    //新建ProducerBatch
    onNewBatch(record.topic(), cluster, prevPartition);
    partition = partition(record, serializedKey, serializedValue, cluster);

    //在RecordAccumulator加入新的ProducerBatch
    result = accumulator.append(record.topic(), partition, timestamp, serializedKey, serializedValue, headers, appendCallbacks, remainingWaitMs, false, nowMs, cluster);
}

//如果batch满了或者生成了新的batch
if (result.batchIsFull || result.newBatchCreated){
    //唤醒Sender线程处理消息
    this.sender.wakeup();
}

        RecordAccumulator消息累加器的append()方法为消息写入RecordAccumulator消息累加器的具体实现。首先先找到消息对应的Deque队列,再调用tryAddend()方法,把消息加入Deque队列中最后一个ProdcuerBatch中,完成数据写入。

RecordAccumulator.append()方法源码:   

 public RecordAppendResult append(String topic,
                                     int partition,
                                     long timestamp,
                                     byte[] key,
                                     byte[] value,
                                     Header[] headers,
                                     AppendCallbacks callbacks,
                                     long maxTimeToBlock,
                                     boolean abortOnNewBatch,
                                     long nowMs,
                                     Cluster cluster) throws InterruptedException {
    //...
      
    //找到本次写入的Record所在partition对应的Deque队列
    Deque<ProducerBatch> dq = topicInfo.batches.computeIfAbsent(effectivePartition, k -> new ArrayDeque<>());                              

    //调用tryAppend()试着追加本条record
    RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callbacks, dq, nowMs);
			
    //...
}

        RecordAccumulator.tryAddend()方法首先找到了Deque队列的最后一个ProducerBatch,再通过调用ProducerBatch的tryAppend()方法具体完成写入。

RecordAccumulator.tryAddend()方法源码:

private RecordAppendResult tryAppend(long timestamp, byte[] key, byte[] value, Header[] headers,
                                     Callback callback, Deque<ProducerBatch> deque, long nowMs) {
   //...
   
   //从deque获取最后一个ProducerBatch
   ProducerBatch last = deque.peekLast();
   
   //调用ProducerBatch.tryAppend()进行消息追加,把ProducerRecord追加到队列最后一个ProducerBatch里
   FutureRecordMetadata future = last.tryAppend(timestamp, key, value, headers, callback, nowMs);
   
   //返回Record写入ProducerBatch的结果,其中deque.size() > 1 || last.isFull()是ProducerBatch是否写满了的条件
   return new RecordAppendResult(future, deque.size() > 1 || last.isFull(), false, false, appendedBytes);
   
   //...
}

        ProducerBatch.tryAppend()方法为具体的消息写入ProducerBatch的写入逻辑,是通过recordsBuilder将Record消息的时间戳、key、value、headers追加写入到ProducerBatch末尾的位置。

ProducerBatch.tryAppend()方法源码:

public FutureRecordMetadata tryAppend(long timestamp, byte[] key, byte[] value, Header[] headers, Callback callback, long now) {
   //...
   
   //通过recordsBuilder将Record写到ProducerBatch中
   this.recordsBuilder.append(timestamp, key, value, headers);
   
   //...
}

        当KafkaProducer将消息写入RecordAccumulator消息累加器后,消息生产的全部操作就结束了,下面就由Sender从RecordAccumulator消息累加器取走ProducerBatch数据,将数据发送到Kafka集群,进入数据发送的环节了。

8.RecordAccumulator消息累加器数据存储

        从上述消息写入RecordAccumulator消息累加器的过程也可以看到RecordAccumulator消息累加器的数据存储结构。RecordAccumulator是按Partition分区为单位建立Deque队列,以ProducerBatch为存储粒度存储待发送的Record消息。

9.结语

        KafkaProducer消息生产过程主要是经历了拦截器的数据预处理、元数据拉取、消息Key与Value的序列化、消息的Partition分区ID生成,最终写入RecoredAccumulator消息累加器中。下文将继续介绍Sender拉取RecoredAccumulator消息累加器的ProducerBatch并向Kafka集群发送的数据发送过程源码。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;