KafkaProducer
1.发送消息流程
生产者与服务端完成一次网络通信步骤如下:
-
生产者客户端应用程序产生消息
-
客户端连接对象将消息包装到请求中,发送到服务端
-
服务短连接对象负责接收请求,并将消息以文件形式存储
-
服务端返回响应结果给生产者客户端
以上流程对应到代码中如下: -
创建ProducerRecord对象,此对象包含目标主题和要发送的内容,还可以指定键或者分区
-
生产者对ProducerRecord对象进行序列化
-
分区器接收到消息后,先来选择一个分区
-
这条记录被添加到一个记录批次里,这个批次会被发送到相同的主题和分区上
-
独立线程负责把这些记录批次,发送到相应的broker上
-
broker就是服务器,在接收到消息时,会返回一个响应,如果成功,返回一个RecordMetaData对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果失败,就会返回一个错误
-
生产者如果接收到错误之后,会尝试重新发送,几次尝试后,还是失败,就返回错误信息
生产者发送的代码在clients包下,下面我们详细讲解下发送流程,附有源码和流程图,方便大家理解。
2.实例
要往Kafka里面写入消息,首先要创建一个生产者对象,并设置一些属性。
配置属性:
- bootstrap.servers
该属性指定broker的地址清单,地址的格式为host:port - key.serializer
指定了序列化的类,生产者会使用这个类把键对象序列化成字节数组 - value.serializer
指定了序列化的类,生产者会使用这个类把值序列化 - client.id
客户端的id
发送消息的方式: - 发送并忘记
我们把消息发送给服务器,但并不关心它是否正常到达 - 同步发送
我们使用send()方法发送消息,它会返回一个Future对象,调用get()方法进行等待,就可以知道消息是否发送成功 - 异步发送
我们调用send()方法,并指定一个回调 函数,服务器在返回响应时调用该函数
我们来看下源码中的例子
public class Producer extends Thread {
private final KafkaProducer<Integer, String> producer;
private final String topic;
//定义消息的发送方式:异步发送还是同步发送
private final Boolean isAsync;
public Producer(String topic, Boolean isAsync) {
Properties props = new Properties();
//Kafka服务端的主机名和端口号
props.put("bootstrap.servers", "localhost:9092");
//客户端的id
props.put("client.id", "DemoProducer");
//key的序列化方法
props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
//value的序列化方法
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer<>(props);
this.topic = topic;
this.isAsync = isAsync;
}
public void run() {
//消息的key
int messageNo = 1;
while (true) {
//消息的value
String messageStr = "Message_" + messageNo;
long startTime = System.currentTimeMillis();
if (isAsync) {
// Send asynchronously 异步发送
//第一个参数是ProducerRecord对象,封装了目标Topic、消息的key,消息的value
//第二个参数是一个Callback对象,当生产者接收到Kafka发来的ACK确认消息的时候,
//会调用此CallBack对象的onCompletion()方法,实现回调
producer.send(new ProducerRecord<>(topic,
messageNo,
messageStr), new DemoCallBack(startTime, messageNo, messageStr));
} else {
// Send synchronously 同步发送
try {
//send()返回的是一个Future<RecordMetadata>这里通过Future.get()方法,阻塞当前线程
//等待Kafka服务端的ACK响应
producer.send(new ProducerRecord<>(topic,
messageNo,
messageStr)).get();
System.out.println("Sent message: (" + messageNo + ", " + messageStr + ")");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
//递增消息的Key
++messageNo;
}
}
}
//回调对象
class DemoCallBack implements Callback {
//开始发送消息的时间戳
private final long startTime;
//消息的Key
private final int key;
//消息的value
private final String message;
public DemoCallBack(long startTime, int key, String message) {
this.startTime = startTime;
this.key = key;
this.message = message;
}
/**
* 生产者成功发送消息,收到Kafka服务端发来的ACK确认消息后,会调用此回调函数
*
* @param metadata 生产者发送的消息的元数据,如果发送过程中出现异常,此参数为null
* @param exception 发送过程中出现的异常,如果发送成功,则此参数为null
*/
public void onCompletion(RecordMetadata metadata, Exception exception) {
long elapsedTime = System.currentTimeMillis() - startTime;
if (metadata != null) {
//RecordMetadata中包含了分区信息、offset信息等
System.out.println(
"message(" + key + ", " + message + ") sent to partition(" + metadata.partition() +
"), " +
"offset(" + metadata.offset() + ") in " + elapsedTime + " ms");
} else {
exception.printStackTrace();
}
}
}
KafkaProducer只用了一个send方法,就可以完成同步和异步两种模式的消息发送。这是因为send方法返回的是一个Future。基于Future,我们可以实现同步或异步的消息发送语义。
- 同步。调用send返回Future时,需要立即调用get,因为Future.get在没有返回结果时会一直阻塞。
- 异步。提供一个回调,调用send后,可以继续发送消息而不用等待。当有结果返回时,会自动执行回调函数。
源码中的注释写的很清楚了,就不再赘述了,大家有兴趣也可以把源码从github上下载下来。
3.kafkaProducer分析
我们来看下发送消息时的类图
消息发送的流程如上,主要有3个模块,主线程,负责生产消息并序列化,选择分区。然后将消息存放到RecordAccumulator中,相当于记录的缓冲区,当缓冲区满了之后,sender线程会从缓冲区中读取数据,并发送到Kafka的服务器上。
- ProducerInterceptors对消息进行拦截
- Serializer对消息的key和value进行序列化
- Patitioner为消息选择合适的Partition
- RecordAccumulator收集消息,实现批量发送
- Sernder线程从RecordAccumulator中收集消息,实现批量发送
- 构造ClientRequest
- 将ClientRequest交给NetworkClient,准备发送
- NetworkClient将请求放入KafkaChannel
- 执行网络IO,发送请求
- 收到响应,调用ClientRequest的回调函数
- 调用RecordBatch的回调函数,最终调用每个消息上注册的回调函数
代码调用的时序图如下:
解释一下上图的主要流程:
1.1:调用ProducerInterceptors的onSend()方法,对消息进行加工
1.2:实际发送doSend()方法
1.2.1:waitOnMetadata判断是否要更新元数据,以及wakeUp sender线程去更新元数据
1.2.2:序列化key
1.2.3:序列化value
1.2.4:获取分区信息
1.2.5:保证记录的size小于最大的请求size,小于totalSize
1.2.6:将记录追加到RecordAccumulator中
1.2.7:wakeup sender线程,去发送数据
现在呢,我们大体流程已经都了解了,下面我们深入去看下里面涉及到的部分逻辑
3.1 ProducerInterceptors
ProducerInterceptors里面维护了一个ProducerInterceptor的list。这个类,主要是在消息被发送到Kafka服务器前,或者接收到了kafka对消息处理信息之后,进行加工处理。