一、发送消息
KafkaProducer<String,String> producer = new KafkaProducer<>(props);
ProducerRecord<String,String> record = new ProducerRecord<>(topic,"hello");
producer.send(record)
构建的消息对象ProducerRecord,包含了多个属性,而消息体只是其中一个value,其完整定义:
public class ProducerRecord<K,V>{
String topic //主题
Integer partition // 分区号
Headers headers //消息头部
K key
V value
Long timestamp //消息时间戳
...
}
发送消息的模式
- 发后即忘
producer.send(record)
- 同步
producer.send(record).get()
send方法的返回值实际上是Future对象,我们可以使用get方法阻塞等待。另外可以使用Future中的get(long timeout,TimeUnit unit)来实现可超时的阻塞。
KafkaProducer中一般会发生两种类型的异常:
- 可重试的异常:如网络异常、分区的Leader副本不可用
- 不可重试的异常:如消息太大
对于可重试的异常,可以配置retries参数自动重试(默认为0),超出重试次数之后再抛出异常。
- 异步
producer.send(record,new Callback(){
@Override
public void onCompletion(RecordMetadata metadata,Exception exception){
if(exception != null){
exception.printStackTrace();
} else{
System.out.println(metadata.topic() + "-
+ metadata.partition() + ":" + metadata.offset())
}
}
})
在send方法里指定一个Callback的回调函数
二、分区器
消息在通过send方法发往broker过程中,有可能需要经过拦截器(Interceptor)、序列化器(Serializer)和分区器(Partitioner)的一系列作用之后才能被真正地发往broker。
如果ProducerRecord中指定了partition字段,那么直接发往对应的分区。如果没有指定,就需要依赖分区器,根据key字段来计算partition的值。
- 如果key不为null,那么默认的分区器会对key进行哈希(MurmurHash2)算法,根据得到的哈希值计算分区号
- 如果key为null,消息将以轮询的方式发往主题内各个分区
三、生产者拦截器
生产者拦截器可以在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的信息、修改消息的内容等。
其使用就是自定义实现org.apache.kafka.clients.producer.ProducerInterceptor接口。
ProducerInterceptor接口:
public ProducerRecord<K,V> onSend(ProducerRecord<K,V> record);
public void onAcknowledgement(RecoredMetadata metadata,Exception exception);
public void close();
- onSend:消息序列化和计算分区之前
- onAcknowledgement:消息被应答之前,或者消息发送失败时(运行在Producer的IO线程中,代码逻辑越简单越好,否则影响消息的发送速度)
- close:关闭拦截器时一些资源清理
四、实现原理
整个生产者客户端由两个线程协调运行:
1. 主线程
kafkaProducer创建消息、通过拦截器、序列化器、分区器、缓存到消息收集器RecordAccumulator。存储格式:< partition , Deque < ProducerBatch >>
- RecordAccumulator
作用:缓存消息以便Sender批量发送,减少网络传输的资源消耗以提升性能。
存储:为每个分区都维护了一个双端队列,队列中的内容是ProducerBatch,即Deque< ProducerBatch >。ProducerBatch中包含一个至多个ProducerRecord
BufferPool缓存:消息的网络传输以字节byte的形式,在发送之前需要创建一块内存区域来保存对应消息。RecordAccumulator中有一个BufferPool用来实现ByteBuffer的复用。它只对特定大小(默认16k)的ByteBuffer进行管理,其他大小的byteBuffer不会被缓存。
2. Sender线程
-
从RecordAccumulator中双端队列的头部读取消息
-
将原本< partition , Deque < ProducerBatch >> 保存形式转变为< BrokerNode , List < ProducerBatch >>
对于生产者KafkaProducer的应用逻辑,也就是用户视角而言,我们只关注向哪个分区中发送哪些消息;
对于网络传输客户端来说,是与具体broker节点建立的连接,向具体的broker发送消息,并不关心属于哪个分区;
所以在这里需要做一个应用逻辑层到网络IO层的转换。
-
Sender进一步封装为< BrokerNode , Rquest >的形式
-
在发送请求之前,会将消息保存到InFilghtQequests中,存储形式为Map < BrokerNode_Id , Deque< Rquest >>,缓存已经发出去,但还没有收到响应的请求。