KafkaProducer的整体逻辑
概述
KafkaProducer是用户向kafka servers发送消息的客户端。官网上对producer的记载如下:
Kafka所有的节点都可以应答metadata的请求,这些metadata中包含了分区所对应的leader信息,而这些leader允许生产者直接将数据发送到分区leader所在的broker。这样子客户端就可以直接将数据发送给这些leader对应的broker中,而不用经过路由。
客户端可以通过继承接口来控制将消息发送到哪一个分区。用户可以随机发送,也可以通过特定的方式指定发送到某个特定的分区。
批处理是提升效率的一种方式,kafkaProducer可以在内存中积累数据,然后在通过一个请求将这些数据发送出去。并且数据量的大小和积累时间的长短都是可以控制的。
举例
KafkaProducer包含在org.apache.kafka.clients这个包内。参照官方文档使用的时候也比较容易,下面是一个简单的例子。
package com.zjl.play;
import org.apache.kafka.clients.producer.*;
import org.apache.log4j.BasicConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Future;
import static com.zjl.play.ProducerConstant.*;
public class Producer {
private static final Logger logger = LoggerFactory.getLogger(com.zjl.play.Producer.class);
private KafkaProducer<String,String> producer;
private Properties kafkaProperties = new Properties();
private List<Future<RecordMetadata>> kafkaFutures;
private String topic;
public void process(List<String> events) {
if (events == null) {
logger.error("process list is null");
return;
}
int processEvents = events.size();
if (processEvents == 0) {
logger.info("the number of process event is zero");
}
try {
ProducerRecord<String, String> record;
kafkaFutures.clear();
for (String event : events) {
long startTime = System.currentTimeMillis();
Integer partitionId = null;
String eventKey = null;
record = new ProducerRecord(topic, partitionId, eventKey, event);
kafkaFutures.add(producer.send(record, new ProducerCallback(startTime)));
}
} catch (Exception e) {
logger.error("get exception: " + e.toString());
}
try {
if (processEvents > 0) {
for (Future<RecordMetadata> future : kafkaFutures) {
future.get();
}
}
} catch (Exception e) {
logger.error(e.toString());
}
}
public void start() {
kafkaFutures = new LinkedList<Future<RecordMetadata>>();
producer = new KafkaProducer<String, String>(kafkaProperties);
}
public void stop() {
producer.close();
}
public void loadKafkaProperties(String bootStrapServers) {
kafkaProperties.put(ProducerConfig.ACKS_CONFIG, DEFAULT_ACKS);
//Defaults overridden based on config
kafkaProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, DEFAULT_KEY_SERIALIZER);
kafkaProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, DEFAULT_VALUE_SERIAIZER);
// kafkaProperties.putAll(context.getSubProperties(KAFKA_PRODUCER_PREFIX));
kafkaProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootStrapServers);
topic = DEFAULT_TOPIC;
}
public static void main(String args[]) {
BasicConfigurator.configure();
System.setProperty("log4j.configuration", "conf/log4j.properties");
Producer producer = new Producer();
producer.loadKafkaProperties("sha2hb06:9092");
producer.start();
List<String> testList = new ArrayList<String>();
testList.add("123");
testList.add("456");
testList.add("789");
producer.process(testList);
producer.stop();
}
}
class ProducerCallback implements Callback {
private static final Logger logger = LoggerFactory.getLogger(ProducerCallback.class);
private long startTime;
public ProducerCallback(long startTime) {
this.startTime = startTime;
}
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
logger.debug("Error sending message to Kafka {} ", exception.getMessage());
}
if (logger.isDebugEnabled()) {
long eventElapsedTime = System.currentTimeMillis() - startTime;
logger.debug("Acked message partition:{} ofset:{}", metadata.partition(), metadata.offset());
logger.debug("Elapsed time for send: {}", eventElapsedTime);
}
}
}
从上面的代码看,整个过程主要包含两部分。
- 创建KafkaProducer实例。
- 调用send()函数异步发送。
源码学习
总体
看了KafkaProducer的源码,主要包含两个部分,获取集群的metadata,将消息发送到对应的broker中。整个消息的网络传输是通过NIO来实现的。

图1
图1中,Metadata里面保存了集群的topic信息,RecordAccmulator 类似一个队列,里面保存了要发送的内容,Sender会从RecordAccmulator队列中取出消息,并交给NetworkClient进行发送。

图2
图2表示了producer的代码层次,从下往上层层封装。
首先简要看下每层里面的代码结构,然后不断深入。
org.apache.kafka.clients.producer
整个producer包里面是放着producer的客户端实现,以及和客户端相关的接口,用户实现接口来完成不同的功能。- KafkaProducer: producer客户端。
- Partitioner: 分区接口,可以实现它来制定不同的分区策略。
- ProducerInterceptor: 过滤接口,可以实现它来对数据进行过滤。
- ProducerRecord: 封装发送到kafka的数据,里面除了消息,还有其他的一些相关属性值,例如topic,partition。
- RecordMetadata: 封装了kafka server 返回的数据信息。
- Callback: 回调接口
org.apache.kafka.clients.producer.internals
- BufferPool: 一个ByteBuffers资源池,用来分配内存
- DefaultPartitioner: 实现一个默认的分区方式,如果指定了partition,就使用它,然后如果指定了key,就使用hash,然后如果都没有,就轮训使用
- ErrorLoggingCallback: 一个Callback实现方式
- FutureRecordMetadata: The future result of a record send
- ProducerRequestResult: 一个类封装了将一条信息发送到对应的一个partition后的返回结果。这里面有一个done 函数,调用它之后会提示对应的线程这条record已经处理完毕。
- producerInterceptors:这个类是是一个容器,里面包含了一个的list 对象,list中是用户自定义的 ProducerInterceptor。 每条 record 在 序列化之前都会被list 中的每个 ProducerInterceptor 进行预处理。
- RecordAccmulator:这个类维护了一个队列,保存了将要发送的records
- RecordBatch: 一个类保存了一批将要发送的record
- Sender: 这个类不断的从accumulator 里面获取records,并发送
Kafka producer
KafkaProducer 这个类相当于一个builder,它初始化了interceptors, accumulator, metadata, NetworkClient, Sender等多个对象。并启动了一个守护线程来不断地跑Sender.run函数。
private KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
....
this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList);
....
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners);
....
this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE,
....
time);
....
NetworkClient client = new NetworkClient(
....
this.requestTimeoutMs, time);
this.sender = new Sender(client,
....
this.requestTimeoutMs);
String ioThreadName = "kafka-producer-network-thread" + (clientId.length() > 0 ? " | " + clientId : "");
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();
....
} catch (Throwable t) {
....
}
}
KafkaProducer 的 send 函数实际上是将record 添加到 accumulator 队列中。
@Override
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
// intercept the record, which can be potentially modified; this method does not throw exceptions
ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record);
return doSend(interceptedRecord, callback);
}
/**
* Implementation of asynchronously send a record to a topic.
*/
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
TopicPartition tp = null;
try {
// first make sure the metadata for the topic is available
ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
....添加到accumulator中
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);
....返回future 对象,里面保存了record发送的结果。
return result.future;
// handling exceptions and record the errors;
// for API exceptions return them in the future,
// for other exceptions throw directly
} catch (ApiException e) {
....处理各种异常
}
}
而实际的发送是通过Sender的run 函数实现的。
void run(long now) {
获取到当前的集群信息
Cluster cluster = metadata.fetch();
// get the list of partitions with data ready to send
获取当前准备发送的partitions,获取的条件如下:
1.record set 满了
2.record 等待的时间达到了 lingerms
3.accumulator 的内存满了
4.accumulator 要关闭了
RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
如果有些partition没有leader信息,更新metadata
// if there are any partitions whose leaders are not known yet, force metadata update
if (!result.unknownLeaderTopics.isEmpty()) {
// The set of topics with unknown leader contains topics with leader election pending as well as
// topics which may have expired. Add the topic again to metadata to ensure it is included
// and request metadata update, since there are messages to send to the topic.
for (String topic : result.unknownLeaderTopics)
this.metadata.add(topic);
this.metadata.requestUpdate();
}
去掉那些不能发送信息的节点,能够发送的原因有:
1.当前节点的信息是可以信赖的
2.能够往这些节点发送信息
// remove any nodes we aren't ready to send to
Iterator<Node> iter = result.readyNodes.iterator();
long notReadyTimeout = Long.MAX_VALUE;
while (iter.hasNext()) {
Node node = iter.next();
if (!this.client.ready(node, now)) {
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
}
}
获取要发送的records
// create produce requests
Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,
result.readyNodes,
this.maxRequestSize,
now);
保证发送的顺序
if (guaranteeMessageOrder) {
// Mute all the partitions drained
for (List<RecordBatch> batchList : batches.values()) {
for (RecordBatch batch : batchList)
this.accumulator.mutePartition(batch.topicPartition);
}
}
检查那些过期的records
List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now);
// update sensors
for (RecordBatch expiredBatch : expiredBatches)
this.sensors.recordErrors(expiredBatch.topicPartition.topic(), expiredBatch.recordCount);
sensors.updateProduceRequestMetrics(batches);
构建request并发送
List<ClientRequest> requests = createProduceRequests(batches, now);
// If we have any nodes that are ready to send + have sendable data, poll with 0 timeout so this can immediately
// loop and try sending more data. Otherwise, the timeout is determined by nodes that have partitions with data
// that isn't yet sendable (e.g. lingering, backing off). Note that this specifically does not include nodes
// with sendable data that aren't ready to send since they would cause busy looping.
long pollTimeout = Math.min(result.nextReadyCheckDelayMs, notReadyTimeout);
if (result.readyNodes.size() > 0) {
log.trace("Nodes with data ready to send: {}", result.readyNodes);
log.trace("Created {} produce requests: {}", requests.size(), requests);
pollTimeout = 0;
}
将这些requests加入channel中
for (ClientRequest request : requests)
client.send(request, now);
// if some partitions are already ready to be sent, the select time would be 0;
// otherwise if some partition already has some data accumulated but not ready yet,
// the select time will be the time difference between now and its linger expiry time;
// otherwise the select time will be the time difference between now and the metadata expiry time;
真正的发送消息
this.client.poll(pollTimeout, now);
}
总结
上面的内容描述了producer这个层面消息发送的整体情况。通过上面的内容,我们知道producer是将消息放到了一个队列中,并通过一个线程不断的从这个队列中取内容,然后发送到服务器。在这个层面中,我们没有看到nio的一点影子,所有的发送请求都是通过调用org.apache.kafka.clients.client 这个包里面的函数进行的,实现了很好的封装。
KafkaProducer的整体逻辑的更多相关文章
- TeamTalk Android代码分析(业务流程篇)---消息发送和接收的整体逻辑说明
第一次纪录东西,也没有特别的顺序,想到哪里就随手画了一下,后续会继续整理- 6.2消息页面动作流程 6.2.1 消息页面初始化的总体思路 1.页面数据的填充更新直接由页面主线程从本地数据库请求 2.数 ...
- mysql 概念和逻辑架构
1.MySQL整体逻辑架构 mysql 数据库的逻辑架构如下图: 第一层,即最上一层,所包含的服务并不是MySQL所独有的技术.它们都是服务于C/S程序或者是这些程序所需要的 :连接处理,身份验证,安 ...
- Fabric架构:抽象的逻辑架构与实际的运行时架构
Fabric从1.X开始,在扩展性及安全性上面有了大大的提升,且新增了诸多的新特性: 多通道:支持多通道,提高隔离安全性. 可拔插的组件:支持共识组件.权限管理组件等可拔插功能. 账本数据可被存储为多 ...
- Vue源码中compiler部分逻辑梳理(内有彩蛋)
目录 一. 简述 二. 编译流程 三. 彩蛋环节 示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:<大史住在大前端>原创博文目录 ...
- Golang源码学习:调度逻辑(二)main goroutine的创建
接上一篇继续分析一下runtime.newproc方法. 函数签名 newproc函数的签名为 newproc(siz int32, fn *funcval) siz是传入的参数大小(不是个数):fn ...
- kafka producer 源码总结
kafka producer可以总体上分为两个部分: producer调用send方法,将消息存放到内存中 sender线程轮询的从内存中将消息通过NIO发送到网络中 1 调用send方法 其实在调用 ...
- 解构C#游戏框架uFrame兼谈游戏架构设计
1.概览 uFrame是提供给Unity3D开发者使用的一个框架插件,它本身模仿了MVVM这种架构模式(事实上并不包含Model部分,且多出了Controller部分).因为用于Unity3D,所以它 ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 《Note --- Unreal 4 --- behavior tree》
Web: https://docs.unrealengine.com/latest/INT/Engine/AI/BehaviorTrees/index.html Test project: D:\En ...
随机推荐
- linux下使用DBCA(database configuration assistant)创建oracle数据库
前提:切换到图形界面 到Oracle的bin文件夹下,使用oracle用户.运行dbca就可以.和windows的效果一样. 假设出现乱码 export LANG="en_US:UTF-8& ...
- 使用docker搭建hadoop分布式集群
使用docker搭建部署hadoop分布式集群 在网上找了非常长时间都没有找到使用docker搭建hadoop分布式集群的文档,没办法,仅仅能自己写一个了. 一:环境准备: 1:首先要有一个Cento ...
- hdu1290
由二维的切割问题可知,平面切割与线之间的交点有关,即交点决定射线和线段的条数,从而决定新增的区域数. 当有n-1个平面时,切割的空间数为f(n-1).要有最多的空间数.则第n个平面需与前n-1个平面相 ...
- ubuntu 交叉编译qt 5.7 程序到 arm 开发板
ubuntu 交叉编译qt 5.7 程序到 arm 开发板平台1 ubuntu 12.042 arm-linux-gcc 4.5.13 QT 5.74 开发板210 armcortex-A8 一 概述 ...
- bzoj4547: Hdu5171 小奇的集合(矩阵乘法)
4547: Hdu5171 小奇的集合 题目:传送门 题解: 做一波大佬们的坑...ORZ 不得不说,我觉得矩阵很简单啊,就一个3*3的(直接看代码吧) 给个递推柿纸:f[i]=f[i-1]+max1 ...
- sicily 1002 Anti-prime Sequences
debug了好久..各种犯错..按照某个学长的思路,终于AC了.. #include <iostream> #include <cstring> using namespace ...
- mysql 字符串的处理
1.SUBSTRING 2.SUBSTRING_INDEX 3. right/left 4.POSITION sql实例 select left(right(SUBSTRING_INDEX(data_ ...
- C#使用tesseract3.02识别验证码模拟登录
一.前言 使用tesseract3.02识别有验证码的网站 安装tesseract3.02 在VS nuget 搜索Tesseract即可. 二.项目结构图 三.项目主要代码 using System ...
- (转载)Android UI设计之AlertDialog弹窗控件
Android UI设计之AlertDialog弹窗控件 作者:qq_27630169 字体:[增加 减小] 类型:转载 时间:2016-08-18我要评论 这篇文章主要为大家详细介绍了Android ...
- exsi中的虚拟机添加磁盘后虚拟机中磁盘不出现
exsi中的虚拟机添加磁盘后虚拟机中磁盘不出现解决: 计算机---> 管理: 这里可以选择磁盘,格式,分区, 改盘符等操作