大家好,我是最光阴。最近深入学习了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 生产者的内部机制,揭示它如何在消息传递方面表现出卓越的性能和可靠性。
首先让我们来看下生产者的架构图:
架构图
在生产一条消息时,会先后经过拦截器、序列化器对消息进行处理,再经过分区器确定消息发送在具体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初始化时构建了一系列的核心组件:
- Partitioner(分区器),决定消息路由到Topic的哪个分区里
- Metadata(元数据),定期向集群拉取元数据信息更新。配置 metadata.max.age.ms 时间间隔,默认是5分钟,默认每隔5分钟一定会强制刷新一下
- RecordAccumulator(数据累加器),负责消息的缓冲机制,发送到每个分区的消息会被封装成batch,一般batch size是16kb,默认情况下要凑够一个batch才会发送,但是可以设置 linger.ms,如果在指定时间范围内,都没凑出来一个batch就把这条消息发送出去,比如说5ms,如果5ms还没凑出来一个batch,那么就必须立即把这个消息发送出去
- 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() 主要是三个步骤:
- 序列化 record 的 key 和 value
- 获取该 record 要发送到的 partition(可以指定,也可以根据算法计算)
- 向 accumulator 中追加 record 数据,数据会先进行缓存(默认32M)
作者简介
曾就职于拼多多,前蚂蚁金服高级Java开发。平时会分享些工作中遇到的技术问题,包括但不限于Java、分布式、大数据。也会分享些理财笔记。关注公众号,了解更多~