消费者与消费者组
消费者(Consumer):负责订阅Kafka中的主题(Topic),并且从订阅的主题上拉取消息。
消费者组(Consumer Group):Kafka消费者组是一个由一组消费者实例组成的逻辑概念,它可以订阅并消费Kafka集群中一个或多个主题中的消息。
每个消费者组内的消费者实例协调工作,以便每个分区中的消息只被组内的一个消费者实例消费。如果消费者组内有多个消费者实例,则分区中的消息将在组内的这些实例之间进行负载均衡。
消费者主要API
- subscribe():订阅消息,指定订阅主题
- assign():订阅消息,指定订阅主题的某些分区
- poll():拉取消息
- seek(),seekToBeginning()和seekToEnd():指定消费起始位置
- commitSync()和commitAsync():以同步和异步方式提交消费偏移量
- assignment():获取消费信息的方法,如获取分区分配关系
- position():获取下一次消费消息位置
- pause()和resume():对分区消费控制
消息投递模式
- 点对点(P2P,Point-to-Point):如果所有的消费者都隶属于同一个消费组,那么每条消息只会被一个消费者处理。
- 发布/订阅(Pub/Sub):如果所有的消费者都隶属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者处理。
消费者组协调器(ComsumerGroupCoordinator)
每个消费者组都会选择一个broker作为自己的Coordinator,它负责监控组内各消费者的心跳,并判断是否宕机,然后决定是否需要重平衡。此外组协调器还负责组内消费者的消费位移状态。
选择broker的策略是:按照组ID进行哈希并对内部主题__consumer_offsets的分区数量取模,获取到分区号,分区号所在的broker即为组协调器。

每个消费者都发送JoinGroup请求到Coordinator,Coordinator从加入的消费者中选择一个作为Leader,并把消费者组的相关信息发送给消费者Leader,由它去负责制定消费方案。消费者Leader将消费方案发送给Coordinator,由Coordinator把消费方案发送给各个消费者。
消费者Topic分区分配策略
- RangeAssignor:按照消费者总数和分区总数进行整除运算来获得一个跨度,然后单个主题的分区按照跨度来进行平均分配,尽可能保证分区均匀的分配给所有的消费者。消费者按照名称的字典顺序排序。

如果组内消费者数量不能平均分配到分区,字典靠前的消费者就会多分配一个分区。如果单个消费者订阅了多个主题,那么可能导致字典靠前的消费者会多负担一些分区的消息,造成负载不均衡。
- RoundRobinAssignor:按将消费者组内所有消费者及消费者订阅的所有主题的分区按照字典排序,然后通过轮询的方式分配给每个消费者。

如果消费者组内订阅的主题是一样,那么消费者之间分配到的主题数的差值不超过1。如果订阅的Topic列表是不同的,那么分配结果是不保证“尽量均衡”的,因为某些消费者不参与一些Topic的分配。
- StickyAssignor:在执行一次新的分配时,能在上一次分配的结果的基础上,尽量少的调整分区分配的变动,节省因分区分配变化带来的开销。其目标是:分配尽量均衡,且每一次重分配的结果尽量与上一次分配结果保持一致,强调粘性。

- 自定义分区分配策略:可以通过实现
org.apache.kafka.clients.consumer.internals.PartitionAssignor接口来实现并通过partition.assignment.strategy配置指定。
Rebalance 重平衡
Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 Consumer 如何达成一致,来分配订阅 Topic 的每个分区,让组内的消费者尽可能的达到消息消费的均衡。重平衡的触发条件:
- 消费者组成员数发生变更
- 阅主题数发生变更
- 订阅主题的分区数发生变更
Rebalance 发生时,Group 下所有的 Consumer 实例都会协调在一起按照partition.assignment.strategy指定的分区策略共同参与。
Rebalance 过程对 Consumer Group 消费过程有极大的影响。重平衡过程中所有的消费者会停止消费,并且重平衡的时间较长(消费者数量越多越明显),并且按照某中策略平衡之后的效果可能与平衡之前的效果区别不大。如:在消费者数量大于分区数量时,新增消费者实例,也会触发重平衡,但重平衡的效果没有任何收益。
避免重平衡的方式
- 第一类非必要 Rebalance 是因为未能及时发送心跳,导致 Consumer 被Coordinator “踢出”Group 而引发的重平衡。设置
session.timeout.ms和heartbeat.interval.ms这两个配置保证 Coordinator 在判断消费者掉线之前至少多轮的心跳请求(一般至少3轮,推荐分别设置为6s和2s)。 - 第二类非必要 Rebalance 是 Consumer 消费时间过长导致的。合理的设置
max.poll.interval.ms拉取消息的间隔时间有位关键。一般设置为超过最大处理时间的上浮20%到30%较为合适。 - 还有一类GC停顿时间造成重平衡也有可能发生。当发生时就需要合理的进行GC的参数调优。
消费者位移管理
- 自动提交:Kafka消费者会隔一段时间向kafka提交消息的位移。开启自动提交和提交的时间间隔的配置分别为
enable.auto.commit和auto.commit.interval.ms。 - 手动提交:设置 enable.auto.commit = false。一旦设置了 false,作为 Consumer 应用开发的你就要承担起位移提交的责任。
- 同步提交:
KafkaConsumer#commitSync()该方法会一直阻塞直到位移被成功提交才会返回。如果提交过程中出现异常,该方法会将异常信息抛出。 - 异步提交:
如果设置了自动提交位移,那么KafkaConsumer在拉取消息的时候必然会调教一次自己的位移,因此它能保证不出现消费丢失的情况。但自动提交位移的一个问题在于,它可能会出现重复消费。如消费者每隔3秒自动提交一次位移,但是消费者可能在第2秒发生了重平衡,或者该消费者掉线,其他消费者就会多消费了2秒钟的数据。
手动提交位移的好处在意可以灵活控制位移提交的实际和频率。并且消费者提供了KafkaConsumer#commitAsync()方法进行异步提交保证不影响业务线程的吞吐量。但是异步提交方式不进行自动重试(自动重试没有意义,位移可能已经变化)。因此更多的情况是使用同步提交和异步提交来取得较为理想的效果。
try {
while(true) {
ConsumerRecords<String,String> records = consumer.poll(Duration.ofSeconds(1));
process(records); // 处理消息
commitAysnc(); // 使用异步提交规避阻塞
catch(Exception e){
handle(e); // 处理异常
} finally {
try{
consumer.commitSync();// 最后一次提交使用同步阻塞式提交
} finally {
consumer.close();
}
}
}
另外一个要注意的点是当消费者拉取了一大批的消息后,最好是按小批次处理后就提交一次位移。commitSync(Map<TopicPartition, OffsetAndMetadata>) 和 commitAsync(Map<TopicPartition, OffsetAndMetadata>)。
位移提交失败处理方式
CommitFailedException: Consumer 客户端在提交位移时出现了错误或异常,而且是不可恢复的严重异常。该异常出现的常见原因是:消息处理时间太长超过了max.poll.interval.ms配置,而且kafka客观地认为该消费者已经掉线因此发生了重平衡。 此时的处理方式优先级从低到高是:
- 缩短消息的处理时间:优化单挑消息的消费算法。
- 降低每次拉取消息的条数:通过
max.poll.records参数降低消费者每次拉取消息的条数 - 使用多线程来加速消费:将拉取的批量消息分发给多个线程进行消费,但是要保证位移的正确提交,这种方式更复杂。
- 增加拉取消息的间隔时长:增加
max.poll.interval.ms参数的值,让消费者有更多的时间进行消费,但是该配置会导致消息的延迟,并且该配置本身需要与其他配置协同改动才更合理,如:session.timeout.ms。
多线程消费模型
多线程消费的方式主要有两个。第一种是封闭模式,即每个消费者仅消费一个分区。这种方式实现简单,但是消费线程数受制于分区数。第二种方式是多个消费线程同时消费同一个分区,这个通过 assign()、seek()等方法实现,但是这种实现方式对于位移提交和顺序控制的处理就会变得非常复杂。

多线程消费时,也要注意位移的提交管理。一种较为常用的做法是使用一个滑动窗口记录消息的位移,并且当某条消息多次尝试消费失败之后就转入死信队列,让线程可以继续向下消费。
消费者消息消费流程

关键配置
- auto.offset.reset:用于指定消费者从某个位置开始消费消息。可选的值为latest(默认值,从最新的消息开始)、earliest(从最早的消息开始)、none(消费时会报错NoOffsetForPartitionException,可以通过消费者的
seek()方法指定从某个位置开始消费)。 - fetch.min.bytes:从Kafka中每次
poll()拉取的最小数据量,默认1B。 - fetch.max.bytes:从Kafka中每次
poll()拉取的最大数据量,默认50MB。 - fetch.max.wait.ms:用于指定从Kafka中每次
poll()拉取的等待时间,默认值为500(ms)。 - max.poll.records:配置消费者在一次拉取请求中拉取的最大消息数,默认值为500(条)。
- connections.max.idle.ms:空闲连接的在经过多久之后进行关闭。默认值9分钟。
- isolation.level:配置消费者的事务隔离级别。有效值为
read_uncommitted和read_committed。
409

被折叠的 条评论
为什么被折叠?



