生产端
生产端采用异步方式发送消息,消息发送过程主要涉及两个线程和一个消息缓冲区。
主线程
主线程包含消息格式(ProducerRecord)、拦截器、序列化器、分区器等组件。
获取Topic元数据信息
生产端获取元数据流程,如图所示:
主要步骤:
- 1)生产端主线程调用KafkaProcduer.send()方法发送消息时,如果消息记录指定的Topic中分区数为空或者分区值不为空但大于Topic的分区总数,则触发拉取元数据标识并唤醒Sender线程调用元数据拉取请求进行元数据拉取,主线程同步阻塞等待元数据更新。
- 2)Sender线程通过元数据更新组件(NetworkClient.DefaultMetadataUpdater)向集群中最空闲的Broker发送获取元数据请求拉取最新的元数据信息,获取成功(失败会一直重试直到成功)后更新元数据版本号,并唤醒主线程执行后续逻辑。
参数:
- metadata.max.age.ms:生产者缓存元数据时长,默认为5分钟。
ProducerRecord
ProducerRecord对象可以声明主题(Topic)、分区(Partition)、键(Key)以及值(Value)等属性,其中主题和值必须声明,分区和键可以不用指定。
拦截器(可选)
Kafka拦截器分为生产者拦截器和消费者拦截器。
- 生产者拦截器允许在消息发送前以及Producer回调逻辑前植入拦截器逻辑。
- 消费者拦截器允许在消费消息前以及提交位移后植入拦截器逻辑。
这两种拦截器都支持拦截器链的方式,即:可以将一组拦截器组成一个拦截器链,按照添加顺序依次执行拦截器逻辑。
生产者拦截器接口为org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法包括:
- onSend(ProducerRecord):该方法封装进KafkaProducer.send()方法中(即:运行在用户主线程中),Producer确保在消息被序列化和计算分区前调用该方法。用户可以在该方法中对消息做任何操作,但最好不要修改消息所属的Topic和分区,否则会影响目标分区的计算。
- onAcknowledgement(RecordMetadata, Exception):该方法会在消息被应答之前或消息发送失败时调用,并且通常是在Producer回调逻辑触发之前。onAcknowledgement运行在Producer的IO线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢Producer的消息发送效率。
- close:关闭Interceptor,主要是执行一些资源清理工作。
序列化器
创建ProducerRecord时,必须指定序列化器,kafka默认提供了StringSerializer和IntegerSerializer、ByteArraySerializer(默认值)等序列化器,推荐使用序列化框架Avro(推荐)、Thrift、ProtoBuf等,不推荐自己创建序列化器。
分区器
Kafka中的每个Topic包含多个分区,发送消息时,如果在ProducerRecord对象中指定了partition参数值,则将消息发送到partition参数值对应的分区,否则需要按照分区器指定的策略将消息发送到目标分区,分区策略有:
- 黏性分区策略(2.4.0版本后默认值):既没有partition值又没有key值的情况下,Kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直 使用该分区,待该分区的batch已满或者已完成,Kafka再随机选一个分区进行使用(和上一次的分区不同)。
- 轮询(2.4.0版本前默认值):指的是在没有指明partition值也不存在Key值的情况下,通过先产生随机正数,之后在该数上自增的方式产生一个数,并使用该数与Topic的分区数进行取余返回一个分区值,即:轮询。
- 按消息键保序:指的是在没有指明partition值但存在Key值的情况下,使用该Key的hash值与Topic的分区数进行取模返回一个分区值。
此外,用户可以根据需要自定义分区策略,自定义分区器需要实现org.apache.kafka.clients.producer.Partitioner接口。
Sender线程
Sender线程是一个守护线程,主要作用是从消息缓冲区抽取消息发送给Broker。Sender线程实现了Runnable接口,主要业务逻辑在run()方法中实现,处理流程如图所示:
处理步骤:
- 1)从消息缓冲区(RecordAccumlator)中抽取达到发送条件的消息(若存在无路由信息的Topic,则会向Broker发送更新元数据请求),按照K-V格式存储到变量batches(Map<Integer, List<ProducerBatch>>:Key为BrokerId,Value为批次集合)中,注意:抽取后的ProducerBatch将不能再追加消息。
- 2)将抽取后的消息放入待发送的消息批次inFlightBatches(Map<TopicPartition, List<ProducerBatch>>,Key为Topic+Patition,Value为批次集合)中,并从inFlightBatches和RecordAccumlator.batches(即:RecordAccumlator中所有批次)中筛选出已过期(超时)的批次,返回发送失败给生产端。
- 3)将剩余待发送的消息批次inFlightBatches根据BrokerId构建发送请求(ClientRequest),调用NetworkClient#send(ClientRequest request, long now)方法构建inFlightRequest对象,将inFlightRequest对象加入到inFlightRequests集合中,并调用Selector.send()方法将Send和对应的KafkaChannel进行绑定,保存到NetworkSend中。
- 4)调用NetworkClient#poll()方法将消息发送给Broker,并向生产端返回消息发送结果和元数据信息。
消息缓冲区(RecordAccumulator)
RecordAccumlator是记录收集器,收集记录到MemoryRecords(多个消息的集合)的队列中。它的主要作用是缓存消息以便Sender线程可以批量发送,从而减少网络传输的资源消耗,提升性能。
RecordAccumulator内存模型
RecordAccumlator通过一个ConcurrentMap(数据结构:CopyOnWriteMap)记录Topic、Partition、ProducerBatch(消息批次)之间的关系,其中每个分区对应一个双端队列(Deque),用于存储消息批次(ProducerBatch)。
ConcurrentMap<TopicPartition, Deque<ProducerBatch>> batches;
如图所示:
图中:
- RecordAccumlator默认大小为32M,可以通过生产端参数buffer.memory进行设置。如果RecordAccumlator内存已满,调用append()追加新的消息时将被阻塞,除非显示的设置消除该限制。
- RecordAccumlator内部有一个BytePool缓冲池,用于实现对ByteBuffer的复用,但只是针对特定大小的ByteBuffer进行管理。ByteBuffer的大小可以通过生产端参数batch.size进行设定,默认值为16K。
RecordAccumlator主要流程如图所示:
处理逻辑:
- 1)生产端通过doSend()方法向消息缓冲区(RecordAccumlator)发送消息。
- 2)消息经过拦截器处理后进行序列化,根据分区策略调用RecordAccumlator.append()方法向指定分区对应的双端队列尾部追加消息。
- 3)追加消息时,先用synchronized对双端队列加锁,获取双端队列中最后一个批次数据,如果该批次数据不为空且该批次空间足够存储该消息,则追加该消息到批次中,如果该批次空间不足以存储该消息,则根据消息大小创建一个新的批次进行存储,即:如果消息大小未超过可复用批次大小(默认值16K),则复用BufferPool中闲置的批次进行存储,否则根据消息大小创建自定义批次进行存储,最后将该批次追加到双端队列的队尾(Sender线程从队首开时抽取)。
- 4)返回消息追加结果。
消息发送流程
生产端消息发送流程,如图:
处理步骤:
- 1)生产端通过配置的properties属性值获取Broker连接信息。
- 2)生产端将消息封装为ProducerRecord对象,调用kafkaProducer.send()方法向指定的Topic发送消息,消息经过拦截器处理后进行序列化,根据分区策略发送到消息缓冲区(RecordAccumlator)中按批次进行存储。
- 3)Sender线程从消息缓冲区中抽取已达到发送条件的消息批次,按格式组装成待发送消息批次。
- 4)从待发送的消息批次和消息缓冲区中所有批次中筛选出已过期(超时)批次,返回发送失败给生产端。
- 5)将剩余待发送的消息批次inFlightBatches根据BrokerId构建发送请求(ClientRequest),调用NetworkClient#send(ClientRequest request, long now)方法构建inFlightRequest对象,将inFlightRequest对象加入到inFlightRequests集合中,并调用Selector.send()方法将Send和对应的KafkaChannel进行绑定,保存到NetworkSend中。
- 6)调用NetworkClient#poll()方法将消息发送给Broker,并向生产端返回响应结果。
注意:
- NetworkSend是对NIO中写Buffer的封装,用来缓存发送的数据。
- NetworkReceive是对NIO中读Buffer的封装,用来缓存接收的数据。
生产者线程安全问题
Kafka生产者是线程安全的,可以在多个线程中共享一个Kafka生产者实例。Kafka生产者内部使用了一些同步机制来保证线程安全,例如:生产端发送消息时,每个Topic分区在消息缓冲区中对应一个双端队列,多个线程向同一个Topic的同一个分区中追加消息时,通过synchronized(同步锁)对双端队列加锁,保证线程安全。