前言
- 描述版本 0.10.1.1 ,1.x之后版本一些类会有相应的调整优化。
- 文章学习自《Apache Kafka源码剖析》和实际源码。
概述
- 整体流程
初始化-》序列化-》路由分配策略-》写入缓存-》异步发送 - 流程图
消息发送过程中,会涉及两个线程协同工作,主线程将业务数据封装成ProducerRecord对象,然后调用send() 方法写入缓冲区RecordAccumulator。Sender线程负责将消息从里面取出消息并批量发送。
详解
KafkaProduct初始化
Producer<String, String> producer = new KafkaProducer<>(props);
下KafkaProduct实现Producer接口:
send() 发送消息,实际将消息暂存RecordAccumulator,等待发送;
flush() 刷新,等到RecordAccumulator中所有消息发送完成,会阻塞调用线程;
partitionsFor() 负责从Metadata(元数据)中获取指定Topic的分区信息;
close()关闭。
// 构造函数里面主要操作有:反射构建partitionier类、keySerializer类,valueSerializer类。
this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
...
this.keySerializer = config.getConfiguredInstance(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,Serializer.class);
this.valueSerializer = config.getConfiguredInstance(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
...
// 创建kafka集群元数据
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners);
...
// 创建RecordAccumulator
this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
this.totalMemorySize,
this.compressionType,
config.getLong(ProducerConfig.LINGER_MS_CONFIG),
retryBackoffMs,
metrics,
time);
...
// 创建NetworkClient,这个是KafkaProducter网络 I/O 的核心
NetworkClient client = new NetworkClient(
new Selector(config.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG), this.metrics, time, "producer", channelBuilder),
this.metadata,
clientId,
config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION),
config.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG),
config.getInt(ProducerConfig.SEND_BUFFER_CONFIG),
config.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG),
this.requestTimeoutMs, time);
...
// 创建并启动Sender线程
this.sender = new Sender(client,
this.metadata,
this.accumulator,
config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION) == 1,
config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
(short) parseAcks(config.getString(ProducerConfig.ACKS_CONFIG)),
config.getInt(ProducerConfig.RETRIES_CONFIG),
this.metrics,
new SystemTime(),
clientId,
this.requestTimeoutMs);
String ioThreadName = "kafka-producer-network-thread" + (clientId.length() > 0 ? " | " + clientId : "");
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();
send()
我们将上文流程图中主线程部分拆解。1至5的操作就是在KafkaProducer的send()方法里完成的。下面我们看下send()调用流程:
-
onsend() 通过Intercetor对消息进行拦截或修改
ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record);
-
waitOnMetadata() 更新kafka集群信息
ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
-
serialize() 序列化消息的key和value
serializedKey = keySerializer.serialize(record.topic(), record.key());
serializedValue = valueSerializer.serialize(record.topic(), record.value());
-
partition() 选择合适分区
int partition = partition(record, serializedKey, serializedValue, cluster);
-
accumulator.append() 将消息追加到缓冲区
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);
RecordAccumulator 中有许多设计操作细节,值得大家进一步学习。比如:private final ConcurrentMap<TopicPartition, Deque<RecordBatch>> batches
中Deque一个双向队列的选择。 -
wakeup() 通过Sender线程将RecordAccumulator缓冲区的消息发送出去。
this.sender.wakeup();
Sender线程run()方法
- 概述
Sender线程发送消息,首先会从缓存里筛选出可以向哪些Node节点发送消息,然后更加生产者和各个节点的链接情况(NetworkClient管理),过滤节点,之后生产相应的请求调用NetworkClient发送出去。下面下看run()方法的流程图:
- fetch 从Metadata获取集群元数据
Cluster cluster = metadata.fetch();
- ready() 筛选哪些节点存在准备好的数据。
RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
- drain() 获取待发送的数据
Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster, result.readyNodes, this.maxRequestSize, now);
- createProduceRequests() 将待发送的数据封装成ClientRequest
List<ClientRequest> requests = createProduceRequests(batches, now);
- send() 将ClientRequest写入KafkaChannel的send字段,不是真的发送。
client.send(request, now);
- poll() 将KafkaChannel.send字段中保存的ClientRequest发送出去,同时还会处理服务端发出的响应,用户自定义的回调函数等。
this.client.poll(pollTimeout, now);
结尾
最后补充张总体的UML图。上述简要描述了Kafka生产者的主要流程及源码,其中重要核心的结构及内的设计希望后续能够加以补充。