1. Producer发送流程简述
如上图所示,Kafka中Producer的消息发送过程主要涉及以下步骤,这里进行简单的阐述:
-
生产者线程:消息经过拦截器、序列化器和分区器,确定了消息发送到哪个主题的哪个分区,然后被发送到对应的 RecordAccumulator 中,每个 RecordAccumulator 与主题分区一一映射。
-
RecordAccumulator:消息被累加和合并,直到达到配置的批次大小(batch.size)或等待超过配置的等待时间(linger.ms),触发 Sender IO 线程的消息发送。
-
Sender IO 线程:这个线程会等待来自 RecordAccumulator 的批量数据,并将它们异步发送到 Kafka 集群。每个请求池默认缓存五个请求(max.in.flight.requests.per.connection),并等待来自服务器的 ACK(确认)。如果未收到 ACK,它会进行重试,默认重试次数上限为整数最大值(retries)。
-
如果 ACK 成功,相应的消息批次将被删除,并通知到生产者端。这一流程确保了消息的异步发送,减少了主线程的阻塞,提高了生产者的效率和性能。
2. 消息发送模式(send方法)
在了解Kafka的具体消息发送流程之前,我们先了解一下Kafka的消息发送模式,Producer向Broker发送消息的第一步就是调用send方法,send方法返回的不是一个void,而是一个Future对象。你可以使用Future来跟踪消息发送的状态和获取相关元数据,主要的消息发送模式有以下三种:
发后即忘:
你可以使用以下方式来发送消息,但它并不关心消息的发送结果,因此可能导致消息发送失败:
try {
producer.send(record);
} catch (ExecutionException I InterruptedException e) {
e.printStackTrace();
}
同步发送:
同步发送会等待消息发送的结果,通过调用get方法来实现。它具有较高的可靠性,因为要么消息发送成功,要么发送失败并抛出异常。你可以根据异常的类型来采取相应的处理措施,例如回滚或执行补偿操作。然而,需要注意的是,阻塞等待结果可能会影响性能,因为在发送一条消息后必须等待结果才能发送下一条。
try {
producer.send(record).get();
} catch (ExecutionException I InterruptedException e) {
e.printStackTrace();
}
异步发送:
使用异步发送,你可以提供一个回调函数(Callback),以便在消息发送完成时进行回调。这种方式提高了消息发送的吞吐量,也在一定程度上提高了消息的可靠性。你可以根据回调的结果来决定是否需要执行回滚逻辑。
kafkaProducer.send(new ProducerRecord<>("first", "atguigu" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null){
System.out.println("主题:"+metadata.topic() + " 分区:"+ metadata.partition());
}
}
});
异步发送允许你在发送消息后继续执行其他操作,而无需等待发送结果。这提高了性能和并发性,尤其在高吞吐量的情况下表现出色。
3. Producer主线程和Sender IO 线程
在 Kafka 生产者端,消息的发送涉及两个关键线程,Producer的主线程和Sender IO线程,这两个线程分别执行不同的任务:
主线程(Producer 主线程): 这是用户在调用 send 方法时运行的线程。当消息发送请求进入主线程时,消息并不会立刻发送到 Kafka Broker。消息依次经过拦截器、序列化、分区器,然后被缓存在名为 RecordAccumulator 的内存缓冲区中,然后 send 方法立即返回,这意味着在这一刻,我们无法确保消息是否已真正发送到 Kafka Broker。
Sender IO 线程: 这是 Kafka Producer 内部的一个专用线程,负责异步地将消息从 RecordAccumulator 发送到 Kafka 集群。它采用非阻塞的 NIO(Non-Blocking I/O)方式执行网络 I/O 操作。这个线程会周期性轮询 RecordAccumulator,一旦满足特定条件,就会触发真正的网络 I/O 发送。此线程工作于异步非阻塞模式下,因此不会阻塞主线程的执行。
3.1 Producer主线程执行任务
主线程整体执行流程的逻辑在方法org.apache.kafka.clients.producer.KafkaProducer#doSend中,核心源码如下:
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
//执行拦截器
ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
//序列化key和value
serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
//执行分区器,获取分区
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);
}
3.1.1 消息封装(ProducerRecord)
首先,kafka主线程会将消息封装成为ProducerRecord对象,这个对象中主要包括了以下内容:
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) {
this.topic = topic; //主题
this.partition = partition; //分区
this.key = key; //key
this.value = value; //消息内容
this.timestamp = timestamp; //时间戳
this.headers = new RecordHeaders(headers); //元数据
}
上面代码中的RecordHeaders是Kafka的Producer和Consumer之间传递消息的元数据,主要用于传递一些与数据内容无关的附加信息,如:消息来源、类型、版本、生产时间、过期时间、分区数、用户id等。
3.1.2 拦截器(ProducerInterceptors)
3.1.2.1 拦截器作用:
Kafka中的Producer拦截器是一种高度可定制的机制,用于在消息被发送到Kafka服务器之前或之后,对消息进行一些处理或记录。这些拦截器提供了以下主要用途:
-
消息转换和增强:Producer拦截器允许你在消息发送之前对其内容进行修改或增强。例如,你可以为消息添加时间戳、序列号或其他自定义信息,以满足特定需求,如在消息中嵌入额外的元数据或执行格式转换。
-
消息过滤:你可以使用拦截器根据自定义条件过滤掉某些消息,确保只有符合特定条件的消息才会被发送到Kafka。这有助于控制哪些消息被记录或传递到Broker。
-
性能监控:Producer拦截器可用于监测Producer的性能指标,例如消息发送速度、错误率等。这些性能数据可以记录到外部系统,以便进行性能分析和故障排除。
-
消息日志记录:你可以使用拦截器记录消息发送的详细日志信息,以追踪消息的生命周期和进行故障排查。这对于跟踪消息传递过程中的问题非常有用。
-
消息验证:拦截器可用于执行消息验证,以确保消息的内容符合预期的格式和要求。这有助于防止无效或不合格的消息被发送到Kafka。
-
自定义扩展:你还可以编写自定义拦截器,以满足特定应用场景的需求。这为你提供了灵活性,以根据具体用例自定义Producer行为。
3.1.2.2 自定义拦截器
拦截器通常以链式调用的方式工作,一个Producer可以配置多个拦截器,它们按照配置的顺序依次处理消息。这为你提供了广泛的定制和扩展Producer行为的能力。在配置Producer时,你可以指定拦截器的类名,Kafka会自动加载并应用这些拦截器。拦截器接口提供了三个方法,如下代码所示:
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
System.out.println("生产者拦截器 onSend() run ."+record);
return new ProducerRecord<>(
record.topic(), record.partition(), record.key(), record.value().concat("_后缀")); }
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
System.out.println("生产者拦截器 onAcknowledgement run ."+metadata.toString() +" exception:"+exception);
}
@Override
public void close() {
System.out.println("生产者拦截器 close() run .");
}
@Override
public void configure(Map<String, ?> configs) {
this.configs = configs;
System.out.println("生产者拦截器 configure run ."+configs);
}
- onSend: 该方法在消息发送到Kafka服务器之前调用,允许对消息进行修改或增强。例如,你可以修改消息内容或目标Topic。
- onAcknowledgement: 该方法在消息成功发送或发送失败后进行回调,用于记录或处理消息确认情况。
- close: 该方法在关闭拦截器时进行资源释放,可用于清理任何临时资源。
3.1.2.3 拦截过程源码解析:
org.springframework.kafka.support.CompositeProducerInterceptor#onSend中为默认的拦截器逻辑,代码如下所示:
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {
ProducerRecord<K, V> interceptRecord = record;
//拦截器可能有多个,依次遍历拦截器列表
for (ProducerInterceptor<K, V> interceptor : this.delegates) {
//依次调用拦截器得到最终过滤后的interceptRecord,并返回
interceptRecord = interceptor.onSend(interceptRecord);
}
return interceptRecord;
}
拦截的逻辑也非常简单,kafka主线程会去遍历拦截器列表,依次执行每个拦截器的过滤操作,最终返回拦截过滤之后的ProducerRecord。
3.1.3 序列化器
3.1.3.1 序列化器作用
序列化器(Serializer)在Kafka中扮演着重要的角色,Producer需要将消息对象序列化为字节数组,而Consumer则需要将字节数组反序列化为可处理的对象。因此,你需要配置适当的序列化器来处理消息的序列化和反序列化。通常需要配置两种序列化器,一种用于消息的键(key),另一种用于消息的值(value)。这些序列化器指定了如何将对象转换为字节数组,以便通过网络发送给Kafka,以及如何从字节数组中还原出对象。
在Kafka中,常见的序列化器包括 StringSerializer、IntegerSerializer 等,它们用于处理常见的数据类型,你可以使用如下方式配置序列化器:
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 配置键序列化器
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 配置值序列化器
在大多数情况下,你可以使用 StringSerializer,因为消息的键和值经常是字符串类型。然而,如果你需要处理自定义对象,你可以实现自己的序列化器,以满足特定的需求。为此,你可以创建一个自定义的序列化器类,实现 Kafka 提供的 org.apache.kafka.common.serialization.Serializer 接口,重写其中的 serialize 方法。
3.1.3.2 序列化器源码解析
在org.apache.kafka.clients.producer.KafkaProducer#doSend中执行了消息的序列化逻辑,Kafka不会直接序列化ProducerRecord而是会对key和value进行序列化,这里只截取了序列化相关代码,源码如下:
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());
//将序列化后的key和value放入RecordAccumulator中
RecordAccumulator.RecordAppendResult result = accumulator.append(record.topic(), partition, timestamp, serializedKey,
serializedValue, headers, appendCallbacks, remainingWaitMs, abortOnNewBatch, nowMs, cluster);
}
具体的序列化器的方法以String序列化器为例,序列化源码如下:
public byte[] serialize(String topic, @Nullable Headers headers, @Nullable T data) {
//消息为空,直接返回
if (data == null) {
return null;
}
if (this.addTypeInfo && headers != null) {
headers.add(this.typeInfoHeader, data.getClass().getName().getBytes());
}
//直接讲String转成byte数组
return data.toString().getBytes(this.charset);
}
3.1.4 分区器
3.1.4.1 分区器作用
分区器(Partitioner)在Kafka中负责决定每条消息属于哪个分区,这有助于实现负载均衡和提高消息发送和接收的并行度。你可以根据消息的键、值或其他标准来选择合适的分区。Kafka的消息构造器提供了多个选项,允许你指定消息的分区,如下:
public ProducerRecord(String topic, Integer partition, K key, V value);
public ProducerRecord(String topic, K key, V value);
public ProducerRecord(String topic, V value);
3.1.4.2 分区器源码解析
Kafka默认使用的分区器是BuiltInPartitioner,在org.apache.kafka.clients.producer.KafkaProducer#partition可以看到调用默认分区器进行分区的相关代码逻辑:
private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
if (record.partition() != null)
return record.partition();
//如果指定了自定义的分区器,则使用指定的分区器
if (partitioner != null) {
int customPartition = partitioner.partition(
record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
if (customPartition < 0) {
throw new IllegalArgumentException(String.format(
"The partitioner generated an invalid partition number: %d. Partition number should always be non-negative.", customPartition));
}
return customPartition;
}
if (serializedKey != null && !partitionerIgnoreKeys) {
//如果指定了key,则使用内置的分区器,根据key来进行分区
return BuiltInPartitioner.partitionForKey(serializedKey, cluster.partitionsForTopic(record.topic()).size());
} else {
//如果未指定key,则返回UNKNOWN_PARTITION
return RecordMetadata.UNKNOWN_PARTITION;
}
}
- 发送消息指定了key:根据消息的key的hash值对分区数取余来确认分区,BuiltInPartitioner的partitionForKey代码如下:
public static int partitionForKey(final byte[] serializedKey, final int numPartitions) {
//先对serializedKey进行hash后转成int值,之后再对numPartitions取余来确定分区
return Utils.toPositive(Utils.murmur2(serializedKey)) % numPartitions;
}
- 如果没有指定key:默认采用sticky的粘性分区策略,消息在相同的分区下填满一个批次就更换下一个分区,org.apache.kafka.clients.producer.internals.RecordAccumulator#append代码如下:
//默认即采用粘性分区策略
final BuiltInPartitioner.StickyPartitionInfo partitionInfo;
final int effectivePartition;
if (partition == RecordMetadata.UNKNOWN_PARTITION) {
partitionInfo = topicInfo.builtInPartitioner.peekCurrentPartitionInfo(cluster);
effectivePartition = partitionInfo.partition();
} else {
partitionInfo = null;
effectivePartition = partition;
}
- 自定义分区策略:当你的消息需要特定的分区策略时,可以自定义分区器。你可以创建一个分区器类,实现 Kafka 提供的 org.apache.kafka.clients.producer.Partitioner 接口,并重写其中的 partition 方法。这个方法负责根据键、值或其他消息特征来选择分区。
3.1.5 消息累加器(RecordAccumulator)
Kafka中的累加器(RecordAccumulator)默认大小为32MB。如果生产者的发送速率超过sender线程的处理速率,消息将会积压在累加器中。这可能导致生产者阻塞或报错,具体取决于阻塞时间的配置。
累加器内部采用ConcurrentMap<TopicPartition, Deque>;ProducerBatch<>的存储结构,每个分区对应一个双端队列,队列中存储的是ProducerBatch。通常,ProducerBatch 的大小是由 batch.size 配置控制的,新的消息会被追加到 ProducerBatch 中,当达到指定大小(例如16KB)时,将触发sender线程进行消息的实际发送。
由于消息生成速率可能很高,而且产生了大量的 ProducerBatch,在发送后需要通过垃圾回收(GC)来回收这些 ProducerBatch,这可能对性能产生影响。因此,Kafka使用 BufferPool 作为内存池来管理 ProducerBatch 的创建和回收。当需要申请新的 ProducerBatch 空间时,会调用 free.allocate(size, maxTimeToBlock) 从内存池中申请空间。
如果单条消息的大小超过了16KB,就不再复用内存池,而是生成一个更大的 ProducerBatch 专门用于存放大消息。发送完成后,该内存空间将由GC回收。
为了降低网络中消息传输的带宽,还可以通过消息压缩的方式。在生产端,消息会在追加到 ProducerBatch 之前进行压缩,常用的压缩算法包括Gzip、Snappy、Lz4和Zstd。压缩虽然是以时间换取空间的方式,但可以有效减小网络传输的数据量。
3.1.5.1 核心参数
RecordAccumulater的核心参数就这一个topicInfoMap,记录了topic->topicInfo的map,重要的是TopicInfo会根据partition来创建消息的batchs。这个参数的目的就相当于将消息根据topic-partition进行管理了起来:
private final ConcurrentMap<String /*topic*/, TopicInfo> topicInfoMap = new CopyOnWriteMap<>();
3.1.5.2 TopicInfo
RecordAccumulater中定义了一个TopicInfo类,里面有一个batches成员变量是一个ConcurrentMap,存储partition和ProducerBatch的映射。TopicInfo实际上就是存储的Partion和其对应的消息信息的一个类
private static class TopicInfo {
//一个map是parition和Deque<ProducerBatch>的映射
public final ConcurrentMap<Integer /*partition*/, Deque<ProducerBatch>> batches = new CopyOnWriteMap<>();
public final BuiltInPartitioner builtInPartitioner;
public TopicInfo(LogContext logContext, String topic, int stickyBatchSize) {
builtInPartitioner = new BuiltInPartitioner(logContext, topic, stickyBatchSize);
}
}
3.1.5.3 ProducerBatch
- 核心参数:
private final List thunks = new ArrayList<>(); //producer讲消息封装到Thunk中
- 核心方法:
public FutureRecordMetadata tryAppend(long timestamp, byte[] key, byte[] value, Header[] headers, Callback callback, long now) {
if (!recordsBuilder.hasRoomFor(timestamp, key, value, headers)) {
return null;
} else {
this.recordsBuilder.append(timestamp, key, value, headers);
this.maxRecordSize = Math.max(this.maxRecordSize, AbstractRecords.estimateSizeInBytesUpperBound(magic(),
recordsBuilder.compressionType(), key, value, headers));
this.lastAppendTime = now;
//将key,value,header和时间戳封装到FutureRecordMetadata
FutureRecordMetadata future = new FutureRecordMetadata(this.produceFuture, this.recordCount,
timestamp,
key == null ? -1 : key.length,
value == null ? -1 : value.length,
Time.SYSTEM);
//进一步将封装好的消息添加到列表中
thunks.add(new Thunk(callback, future));
this.recordCount++;
return future;
}
}
3.1.5.4 append方法
RecordAccumulate的核心方法就是append方法,其主要目的是将Producer需要推送的消息添加到对应Partition所对应的Deque之中,以下是核心代码的展示和注释:
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 {
TopicInfo topicInfo = topicInfoMap.computeIfAbsent(topic, k -> new TopicInfo(logContext, k, batchSize));
appendsInProgress.incrementAndGet();
ByteBuffer buffer = null;
if (headers == null) headers = Record.EMPTY_HEADERS;
try {
while (true) {
//先根据各种分区策略选择消息所需要推送到的分区
final BuiltInPartitioner.StickyPartitionInfo partitionInfo;
final int effectivePartition;
if (partition == RecordMetadata.UNKNOWN_PARTITION) {
partitionInfo = topicInfo.builtInPartitioner.peekCurrentPartitionInfo(cluster);
effectivePartition = partitionInfo.partition();
} else {
partitionInfo = null;
effectivePartition = partition;
}
// Now that we know the effective partition, let the caller know.
setPartition(callbacks, effectivePartition);
// 根据选定的分区判断其对应的Deque是否存在
Deque<ProducerBatch> dq = topicInfo.batches.computeIfAbsent(effectivePartition, k -> new ArrayDeque<>());
synchronized (dq) {
//尝试将当前消息加入到Deque中
RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callbacks, dq, nowMs);
if (appendResult != null) {
boolean enableSwitch = allBatchesFull(dq);
topicInfo.builtInPartitioner.updatePartitionInfo(partitionInfo, appendResult.appendedBytes, cluster, enableSwitch);
//加入成功则直接返回,否则执行后续的新建Deque并插入的功能
return appendResult;
}
}
synchronized (dq) {
//新建Deque并将消息入队,成功则直接返回
RecordAppendResult appendResult = appendNewBatch(topic, effectivePartition, dq, timestamp, key, value, headers, callbacks, buffer, nowMs);
return appendResult;
}
}
} finally {
free.deallocate(buffer);
appendsInProgress.decrementAndGet();
}
}
上述源码的主要流程如下:
-
根据分区策略选择好当前消息需要发送的partition;
-
尝试将消息推送到partition对应的Deque中,如果推送成功则直接返回,否责新建Deque并再次推送;
3.1.5.5 总结
RecordAccumulate的主要功能说起来其实也很简单,就是将消息按照Topic-Partition按批次缓存起来,需要关注的主要有TopicInfo和ProducerBatch两个对象,TopicInfo主要是将Partition和消息的Batch使用一个ConcurrentMap联系起来,同时要保证线程安全。ProducerBatch主要是将消息封装成一个个的批次,在后续SenderIO线程发送消息的时候直接对每个批次的消息进行发送。
4. Sender IO线程
消息在被保存到RecordAccumulate中之后,Sender线程就会将符合条件的消息按照批次进行发送,除了发送消息之外,元数据的加载也是通过Sender线程来进行处理的。Sender线程发送以及接受消息都是基于Java NIO中的Selector来进行的。
4.1 Sender 总体流程图
4.2 Sender 源码解析
4.2.1 Sender 核心参数
public class Sender implements Runnable {
private final KafkaClient client; //kafka的客户端信息,具体是kafka集群成员信息
private final RecordAccumulator accumulator; //消息累加器,批量缓存了消息
private final ProducerMetadata metadata; //生产者的元数据
private final short acks; //应答个数
private final int retries; //重试次数
private final ProducerMetadata metadata;
...
}
-
KafkaClient client: Kafka 网络通信客户端,主要负责封装与 broker 的网络通信。
-
RecordAccumulator accumulator: 消息记录累积器,是消息追加的入口,通过 RecordAccumulator 的 append 方法实现。它负责将消息追加到生产者批次中。
-
ProducerMetadata metadata: 元数据管理器,用于管理 topic 的路由分区信息,确保生产者能够将消息发送到正确的分区。
-
int maxRequestSize: 通过调用 send 方法发送的最大请求大小,包括 key、消息体序列化后的消息总大小不能超过该值。通过参数 max.request.size 进行设置。
-
short acks: 用来定义消息“已提交”的条件,即 Broker 端向客户端承诺已经提交的条件。可选值包括 0、-1、1。
-
int retries: 重试次数,表示在发送失败时的重试次数。
-
Time time: 时间工具类,用于处理与时间相关的操作。
-
boolean running: 该线程状态,为 true 表示运行中。
-
SenderMetrics sensors: 消息发送相关的统计指标收集器,用于收集与消息发送性能相关的指标。
-
int requestTimeoutMs: 请求的超时时间,表示发送请求等待响应的最大时间。
-
long retryBackoffMs: 请求失败之后在重试之前等待的时间,用于控制重试的时间间隔。
-
TransactionManager transactionManager: 事务处理器,用于处理事务性消息。
-
Map<TopicPartition, List> inFlightBatches: 正在执行发送相关的消息批次的映射,记录了每个分区上正在发送的消息批次。
4.2.2 Sender 的执行位置
在Kafka Produer的构造函数中,就会创建IO线程并且执行start方法来执行sender,sender本身是一个Runnable对象,能够直接被线程执行。
代码位置:org.apache.kafka.clients.producer.KafkaProducer#KafkaProducer
//创建IO线程,将sender放入线程中
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
//线程启动
this.ioThread.start();
4.2.3 Sender的核心方法
- run方法:不断尝试执行runOnce方法,直到IO线程退出
public void run() {
// 实际上核心代码就这一个部分,不断的尝试去执行runOnce方法,直到IO线程退出
while (running) {
try {
runOnce();
} catch (Exception e) {
log.error("Uncaught error in kafka producer I/O thread: ", e);
}
}
}
- runOnce:由于这里我们不关注Kafka的事务,因此只关注核心方法,就是带上时间戳执行sendProducerData方法
void runOnce() {
//删除了事务相关的代码
long currentTimeMs = time.milliseconds();
long pollTimeout = sendProducerData(currentTimeMs);
client.poll(pollTimeout, currentTimeMs);
}
- sendProducerData: 遍历找出已满的ProducerBatch并调用Java NIO方法推送消息到Kafka集群
private long sendProducerData(long now) {
Cluster cluster = metadata.fetch();
//遍历集群信息,找出已经准备好的partition
RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
//调用drain方法,拿到已经准备好的ProducerBatchs
Map<Integer, List<ProducerBatch>> batches = this.accumulator.drain(cluster, result.readyNodes, this.maxRequestSize, now);
//将准备好的batches中的record推送到kafka的partition中
sendProduceRequests(batches, now);
return pollTimeout;
}
-
遍历cluster信息,找出已经准备好的partition
-
调用RecordAccumulator的drain方法,这个方法的功能是判断哪些ProducerBatch中的数据已满需要Sender发送
-
接着sendProduceRequests方法会使用Java NIO的Selector方法发送消息到Kafka的Partition
具体的像drain方法和sendProduceRequests过于繁琐,这里不深入探讨,本文主要不是进行源码的详细解析,感兴趣的可以自己研究。
- sendProduceRequest:这个方法就是具体调用KafkaClient即NetWorkClient,将数据从本地发送到Kafka集群中去
private void sendProduceRequest(long now, int destination, short acks, int timeout, List<ProducerBatch> batches) {
if (batches.isEmpty())
return;
//从这里开始经过一系列判断,将需要发送的数据封装到recordsByPartition中
final Map<TopicPartition, ProducerBatch> recordsByPartition = new HashMap<>(batches.size());
byte minUsedMagic = apiVersions.maxUsableProduceMagic();
for (ProducerBatch batch : batches) {
if (batch.magic() < minUsedMagic)
minUsedMagic = batch.magic();
}
ProduceRequestData.TopicProduceDataCollection tpd = new ProduceRequestData.TopicProduceDataCollection();
for (ProducerBatch batch : batches) {
TopicPartition tp = batch.topicPartition;
MemoryRecords records = batch.records();
if (!records.hasMatchingMagic(minUsedMagic))
records = batch.records().downConvert(minUsedMagic, 0, time).records();
ProduceRequestData.TopicProduceData tpData = tpd.find(tp.topic());
if (tpData == null) {
tpData = new ProduceRequestData.TopicProduceData().setName(tp.topic());
tpd.add(tpData);
}
tpData.partitionData().add(new ProduceRequestData.PartitionProduceData()
.setIndex(tp.partition())
.setRecords(records));
recordsByPartition.put(tp, batch);
}
String transactionalId = null;
if (transactionManager != null && transactionManager.isTransactional()) {
transactionalId = transactionManager.transactionalId();
}
//以下就是具体的创建请求和发送消息到Kafka集群的逻辑
ProduceRequest.Builder requestBuilder = ProduceRequest.forMagic(minUsedMagic,
new ProduceRequestData()
.setAcks(acks)
.setTimeoutMs(timeout)
.setTransactionalId(transactionalId)
.setTopicData(tpd));
RequestCompletionHandler callback = response -> handleProduceResponse(response, recordsByPartition, time.milliseconds());
String nodeId = Integer.toString(destination);
ClientRequest clientRequest = client.newClientRequest(nodeId, requestBuilder, now, acks != 0,
requestTimeoutMs, callback);
client.send(clientRequest, now);
log.trace("Sent produce request to {}: {}", nodeId, requestBuilder);
}
5. 总结
本文较为完整的解析了Kafka的Producer的消息发送流程,针对其消息发送流程我们重点需要关注两个线程:Main线程和Sender IO线程即可。
Main线程的主要功能是将消息封装之后,依次进行过滤、序列化和分区选择的操作之后,放入RecordAccumulator中对消息进行批量缓存,直到消息批次装满或者时间超限之后会被Sender IO线程检测到并发送到Kafka集群。当消息被封装推送到RecordAccumulator之后,Main线程的工作就完成了,此时会直接返回。
Sender IO线程主要是一个死循环,不断的去检查Kafka Cluster集群的健康状态和RecordAccumulator中哪些分区已经被填满了,当发现分区数据就绪之后,就会发起网络请求通过Java NIO的方式异步的发送数据,并且等待ACKS的返回。
上面就是Kafka的Producer推送消息的主要过程,实际上在Kafka集群那边数据被推送到Leader Partition之后,还会通过ISR机制将消息分区复制到Follower Partition,并且最终返回ACKS。