kafka(一):生产者

生产者在消息发送的过程中,涉及到了 两个线程 ——main 线程和Sender 线程。
main线程创建 Producer 对象,调用 send 函数发送消息的经过:
在这里插入图片描述

1.创建ProducerRecord:

应用程序首先将待发送的数据封装成一个ProducerRecord对象。这个对象包含了消息的Topic、Partition、可选的Key和Value,以及时间戳等信息。

2.拦截器:

生产者拦截器的使用主要是通过自定义实现org.apache.kafka.clients.producer.ProducerInterceptor接口来完成。这个接口定义了两个主要的方法:onSend()和onCompletion()。onSend()方法在消息被序列化以及计算分区前调用,可以在这个方法中对消息进行修改或过滤等操作。onCompletion()方法则在消息从RecordAccumulator成功发送到Kafka Broker,或者在发送失败时调用,可以在这个方法中进行一些发送后的处理,比如统计发送状态等。

此外,Kafka生产者允许用户指定多个拦截器,这些拦截器会按照指定的顺序形成一个拦截链。在消息发送过程中,拦截链中的每个拦截器都会按照顺序对消息进行处理。

3.序列化:

Kafka是面向字节的系统,因此ProducerRecord中的Key和Value需要序列化成字节序列。这通常通过序列化器(Serializer)来完成,例如StringSerializer或ByteArraySerializer。

4.分区器:

Kafka使用分区机制来保证消息的顺序性和扩展性。Producer根据Key(如果提供)和分区器(Partitioner)来决定将消息发送到哪个分区。

  • 如果ProducerRecord对象中指定了Partition,则会优先使用该Partition
  • 如果ProducerRecord对象中没有指定Partition,但是有Key,则会根据Key的hashcode与分区数取余得到具体分区号
  • 如果没有指定Partition,也没有Key,则会才用黏性分区的方法,会随机选择一个分区,并尽可能一直使用这个分区,待该分区的batch,size满了或者linger.ms时间到了,再随机选择一个分区(绝对不是上一个)进行使用

5.缓存到RecordAccumulator :

序列化后的消息被缓存到RecordAccumulator中。这是一个内存缓冲区,用于收集将要发送的消息批次,以Topic为分组。每一组内有拆分成多个批次,每个批次的默认大小是16K。每个Deque是一个双端队列,创建队列都是在内存里完成的,默认是32M,主线程append时,往队尾插入,sender线程取出时,从队头取出。这样做的好处是减少网络IO的请求,增加吞吐量。

由于RecordAccumulator的缓存空间有限,如果空间被占满,那么当我们再次调用KafkaProducer的send(…)方法的时候,就会出现阻塞(默认60秒,可以通过参数max.block.ms来配置),如果阻塞超时,则会抛出异常。

6.Sender线程从队列里拉取数据:

sender线程从队列中拉取在RecordAccumulator中等待的数据

  • 每次批处理batch.size的大小默认为 16k
  • 延迟时间 linger.ms 默认为 0ms,没有延迟

这个两个条件达到任意一个就可以发送数据。值得注意的是,send线程再拉去数据后,会按照Broker为组,重新把ProderBatch进行分组。因为不同的Topic也可能是在同一个Broker上的,所以拉取之后的数据结构依然是Deque<ProderBatch>,只不过这个Deque里面充斥了不同的Topic producerBatch。

注意:拉取RecordAccumulator中的数据,但是并不会从RecordAccumulator中删除这部分数据。

7.封装成ProducerRequest:

消息发送前,还会对Deque再度进行封装成ProducerRequest,然后以Broker为组发送到NewWorkClient。

8.缓存到InFightRequest:

InFightRequest(在途请求缓冲区)内维护了一个key是Broker,value是Deque<NetworkClient.InFlightRequest>>的缓存map(实际上ProducerRequest在发送到InFightRequest前,会先被封装成NetworkClient.InFlightRequest类),用于存储队每个Broker节点的在途请求。什么是在途请求,是指发送了,但是还没有收到Broker Ack的响应。等收到Broker 的Ack响应后,会从队列前端移除对应的请求,并处理响应。

InFightRequest 类中有一个属性 maxInFlightRequestsPerConnection, 标识一个Broker节点最多可以缓存多少个请求。该默认值为 5, 可通过 max.in.flight.requests.per.connection 进行配置, 需要注意的是 InFlightRequests 对象是在创建 KafkaProducer 时就会被创建。

9.使用NIO发送:

Sender线程使用Java NIO(非阻塞I/O)机制,将消息批次异步发送到对应的Broker。NIO允许在单个线程内处理多个网络连接,从而提高网络操作的效率。

10.发送到Broker集群&回调处理:

Kafka集群在收到响应后会返回Ack,有两种情况

  • 返回成功,则会执行剩余流程
  • 返回失败,则会进行retry重试机制,默认重试次数是int的最大值,

同时Kafka提供了三种Ack的应答级别

  • 0 Partition的leader接收到消息还没有写入磁盘就已经返回ack,当Leader故障时有可能丢失数据。
  • 1 Partition的Leader落盘成功后返回Ack。如果在ISR队列里面的节点同步成功之前,Leader故障了,尽管 Leader 已经落盘成功,将会丢失 ISR内Follower 还未同步 Leader的 那部分数据。
  • -1 Partition的Leader和ISR队列里面所有节点全部落盘成功后才返回Ack。

Q1:Leader收到数据,但是有一个Follower因为故障, 一直没有同步消息,怎么解决?

A1:首先明确一下ISR的概念,Leader维护了一个动态队列,意为和Learder保持同步的Follower+Leader的集合(leader:0 Isr:0,1,2)。如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.ag.ime.max.ms参数设定,默认30s。例如2超时(leader:0 isr:0,l)

如果分区副本设置为1个,或者ISR里应答的最小副本数量(min.insync.replicas默认为1)设置为1,和ack=l的效果是一样的,仍然有丢数的风险(leader:0,isr:0)。

数据完全可靠条件=ACK级别设置为-1+分区副本大于等于2+ISR里应答的最小副本数量大于等于2

注意,这里的“副本”并不是指的 Follower;在 Kafka 中,副本分为 Leader 副本和 Follower 副本。Leader 副本负责处理消息,而 Follower 副本则简单地复制 Leader 副本的数据。也就是一个分区至少要有 1 个 Leader 和 1 个 Follower,ISR 队列最少也要有 1 个 Leader 和 1 个 Follower。

Q2:如果在Follower同步完成后,Broker发送Ack之前,leader发生故障,即选举出新的 Leader,新的 Leader 将再次落盘一次,那么会造成数据重复,怎么解决?

A2:Kafka0.11版本以后,引入了一项重大特性:幂等性。

生产者在发送每条消息Batch消息的时候,会为消息分配一个唯一标识<PID,Partition,SeqNumber>,PID是Kafka每次重启都会分配一个新的;Partition表示分区号;Sequence Number是单调自增的。Borker端接受到之后会有一个ProducerStateManager 实例会缓存每个 PID 在每个 Topic-Partition 上发送的最近 5 个batch 数据。然后会根据Sequence Number来判断。如果新来数据Sequence Number是已经在缓存内的,那么会直接丢弃,说明是重复数据

所以幂等性只能保证的是在单分区单会话内不重复。而且因为ProducerStateManager只会缓存最近5个的数据,所以在途请求区的数量也要设置成不能大于5,否则依旧会出现重复的现象。

Q3:每条batch数据都是带有Sequence Number的,比如有1~5条消息,其中序号是2的消息因为网络原因,Broker没有响应Ack,因为重试机制序号2就会重新发送。此时消息的顺序就变成了2->5->4->3->1,产生了乱序,这种情况应该如何解决?

A3:ProducerStateManager会根据Sequence Number的连续性来判断,如果不连续或者新来数据的Sequence Number大于缓存的最大Sequence Number,那说明有数据漏掉了。则会报错,让Producer从缓冲区冲重新推送 完整的数据

配置上:1.Ack=-1 2.enable.idempotence 3.在途请求区大小设置成5

Q4:但是对于幂等性而言是有一个缺陷的,Producer一旦重启,分配的PID就会变化,依然会导致幂等性的问题

A4:这就需要引入事务,事务是解决跨会话的分区的问题,对于Producer,需要设置transactional.id属性。(事务部分比较复杂,有机会单独写一期来讲)

每条消息发送后,Broker回复响应,以便后续处理
org.apache.kafka.clients.producer.internals.Sender#handleProduceResponse

11.回调:

Producer根据Acks配置等待Broker的确认。

  • 如果发送失败,根据retries和retry.backoff.ms配置,Producer可能会重试发送消息
  • 如果发送成功,则移除在途缓冲区内的Request

12.回调处理:

如果消息被成功Ack,则清理掉RecordAccumulator中的缓存数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值