Kafka核心原理之生产端

生产端

生产端采用异步方式发送消息,消息发送过程主要涉及两个线程和一个消息缓冲区。

主线程

主线程包含消息格式(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(同步锁)对双端队列加锁,保证线程安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值