版权声明:本文为转载文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_39468305/article/details/106846814
摘要:Consumer Group 指多个消费者实例组成一个组来共同消费一组主题,它可谓大名鼎鼎,不仅可以加速消费端 TPS,还具有扩展性和容错性等优势。而组成员之间如何达成一致来分配订阅 Topic,又成为了 Rebalance(重平衡)的重头大戏,不得不夸赞 Rebalance 在均衡和协调方面的丰功伟绩,但是它同样臭名昭著,带来了非常多让人恶心的影响,因此我们必须学会避免它。本文主要从以下几个方面来讲述消费组和重平衡的那些事:
Consumer Group 的概念剖析和意义
Consumer Group 的位移管理
Rebalance 的概览剖析、优势和劣势
Rebalance 的触发场景,如何避免
如果有 1000 个面包,让一个人解决,他需要至少一年的时间,而让 1000 个人解决,只需要一分钟的时间。同理,100000 条消息如果你只让一个消费者来消费,可能需要一个小时,而如果你让 50 个消费者来消费,只需要 5 分钟。这 50 个消费者实例组成的集合就是消费组。
Kafka 消费者组,指的是多个消费者实例组成一个组来消费一组主题。
为什么要有它的存在呢?首先根据面包那个例子,很容易得出是为了效率问题,多个消费者实例同时消费,加速整个消费端的吞吐量(TPS)。当然它的作用不仅仅是瓜分订阅主题的数据,加速消费。它们还能彼此协助。假设组内某个实例挂掉了,Kafka 能够自动检测到,然后把这个 Failed 实例之前负责的分区转移给其他活着的消费者,这个过程称之为重平衡(Rebalance),因此也可以得出 Consumer Group 是 Kafka 提供的可扩展且具有容错性的消费者机制。
如何设计消费组
在了解了 Consumer Group 以及它的设计亮点之后,你可能会有这样的疑问:在实际使用场景中,我怎么知道一个 Group 下该有多少个 Consumer 实例呢?理想情况下,Consumer 实例的数量应该等于该 Group 订阅主题的分区总数。
这个答案是不难得出的,你必须记住的一个特性是一个分区只能被消费者组中的其中一个消费实例去消费。如果你有 100 个分区,200 个消费者组成一个消费组,由于 1 个消费者只能消费 1 个分区的数据,那么就有 100 个消费者没事干吃干饭了。所以一般情况下组成员数不比分区数量多,否则造成资源浪费。可以少于分区数,比如只有 2 个消费者,那么它们会均摊这 100 个分区进行消费。但如果时效性要求较高的场景,最好保持一致,这样可以提高吞吐量。千万不要只给一个消费者,这样一旦它挂了,消费就等于停止了,不符合可用性要求。
关于位移管理,在 kafka 消费位移那些事已经作了详细说明,如果不清楚希望你再去看一遍。这里只是想展示一下消费组消费进度查看的命令,通过这个命令,你可以知道当前消费组消费了哪些 topic,每个分区的消费进度如何,以及每个分区是由哪个消费者进行消费的。从下面的信息也可以得出,一个分区只会被一个消费者消费。
查看消费组消费进度
> bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group test
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
kafka_test 2 7290 7292 2 consumer-3-7edccfc6-ab05-4871-b1ea-ad06ed5038c1 /192.168.109.1 consumer-3
kafka_test 1 7258 7258 0 consumer-2-50840b10-5550-4b9e-a357-f3667520af08 /192.168.109.1 consumer-2
kafka_test 3 7437 7437 0 consumer-4-0a30fa36-5a3c-40b6-84b8-c3f67553b27c /192.168.109.1 consumer-4
kafka_test 0 7399 7399 0 consumer-1-696cc434-d9f0-4f95-8ac5-2202dee19d2d /192.168.109.1 consumer-1
消费者组还是很好理解的,但是它带来的重平衡可是个比较复杂的东西了。本人初次接触重平衡是这样一个异常:
org.apache.kafka.clients.consumer.CommitFailedException: Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member.
大概意思是说这个消费组已经重平衡了,分配给了其他成员。
以这个错为引言是因为它比较容易碰到,我当时会抛出这个异常的原因是,我对消息进行业务逻辑处理的时间比较久,而max.poll.interval.ms
参数设置的过短,该参数限定了 Consumer 端应用程序两次调用 poll 方法的最大时间间隔,如果超时无法消费完 poll 方法返回的消息,那么 Consumer 会主动发起“离开组”的请求,Coordinator 也会开启新一轮 Rebalance。是不是现在听起来有点绕了?那就让我们从头开始揭开重平衡的神秘面纱吧。
首先为何要有重平衡?听完我刚刚那个例子你可能会觉得这只是个坏事儿的东西,我只不过处理时间长了点你就给我抛异常!不,其实它是大名鼎鼎的,只不过它大名鼎鼎的同时也臭名昭著。
文章开头我们说到:假设组内某个实例挂掉了,Kafka 能够自动检测到,然后把这个 Failed 实例之前负责的分区转移给其他活着的消费者,这个过程称之为重平衡(Rebalance)。这无疑是非常有用的一个东西,可以保障高可用性。除此之外,它协调着消费组中的消费者分配和订阅 topic 分区,比如某个 Group 下有 20 个 Consumer 实例,它订阅了一个具有 100 个分区的 Topic。正常情况下,Kafka 平均会为每个 Consumer 分配 5 个分区。这个分配的过程也叫 Rebalance。再比如此刻新增了消费者,得分一些分区给它吧,这样才可以负载均衡以及利用好资源,那么这个过程也是 Rebalance 来完成的。
综上:Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 Consumer 如何达成一致,来分配订阅 Topic 的每个分区。
在 Rebalance 过程中,所有 Consumer 实例共同参与,在协调者组件(Coordinator,专门为 Consumer Group 服务,负责为 Group 执行 Rebalance 以及提供位移管理和组成员管理)的帮助下,完成订阅主题分区的分配。
现在是否对重平衡的态度 180° 大转弯,觉得它实在是太好了!不,现在就要开始说它臭名昭著的那些事儿了。
第一:Rebalance 影响 Consumer 端 TPS,对 Consumer Group 消费过程有极大的影响。我们知道 JVM 的垃圾回收机制,那可怕的万物静止的收集方式,即 stop the world,所有应用线程都会停止工作,整个应用程序僵在那边一动不动。类似,在 Rebalance 期间,Consumer 会停下手头的事情,什么也干不了。
第二:Rebalance 很慢。如果你的 Group 下成员很多,就一定会有这样的痛点。某真实案例:Group 下有几百个 Consumer 实例,Rebalance 一次要几个小时。万物静止几个小时是非常可怕的一件事了,老板可能要提大刀来相见了。
为什么会这么慢呢?因为目前 Rebalance 的设计是让所有 Consumer 实例共同参与,全部重新分配所有分区。其实应该尽量保持之前的分配,目前 kafka 社区也在对此进行优化,在 0.11 版本提出了 StickyAssignor,即有粘性的分区分配策略。所谓的有粘性,是指每次 Rebalance 时,该策略会尽可能地保留之前的分配方案。不过不够完善,有 bug,暂时不建议使用。
现在是否再次对重平衡的态度 180° 大转弯,觉得它实在是太坏了!可是没办法,如果你没有能力改造它,那么你只能选择了解它并且避免它。
既然要避免,肯定要知道 Consumer Group 何时会触发 Rebalance 呢?
组成员数发生变更。比如有新的 Consumer 实例加入组或者离开组,抑或是有 Consumer 实例崩溃被“踢出”组。
订阅主题数发生变更。
订阅主题的分区数发生变更。
刚刚说到三个触发机制,后面两者一般是用户主动操作,这不可避免,所以我们应该重点关注第一个场景,当然消费实例增加也是出于伸缩性的需求,所以其实我们只需要避免实例减少的情况就行了。
现在可能你会说,那还不简单,我尽量不减少实例就好了。可必须要先明白一点,不是你主动 kill 消费成员,或者机器宕机那种情况,才算是被踢出组,现在回去看看我开头那个例子,消费时间过长,也是会被踢的。不仅如此,某些情况会让 Coordinator 错误地认为 Consumer 实例“已停止”从而被“踢出”Group。如果是这个原因导致的 Rebalance,我们就不能不管了。
那么,现在我们的侧重点已经很清晰了,我们需要归纳出哪些情景会让协调者认为消费者实例已经死亡并把他们踢出组。
一、未能及时发送心跳
Consumer 实例未能及时发送心跳,导致 Coordinator 误认为它已经死亡。心跳这东西,在大数据领域应该司空见惯,比如 HBase,Hdfs 都有定时发送心跳的概念,目的都是为了通知其他组件"我还活着"。在 kafka 的世界中,每个 Consumer 都会定期发送心跳给 Coordinator 的,表明它还存活着。
那么发送心跳必然有一个阈值和频率的概念。阈值是 Coordinator 最长能接受的心跳间隔,默认 10s,即超过 10s 还没收到心跳才认定 consumer 死亡,从而将其从 Group 中移除,然后开启新一轮 Rebalance。频率是指 Consumer 发送心跳的频率。它俩对应的参数分为叫做session.timeout.ms
,heartbeat.interval.ms
,因此你需要合理的设置这两个参数。
千万不要无脑的觉得我把频率调高点,阈值也调高点,比如我 1s 发一次心跳,并设置超过 1 分钟才可以认定为死亡,就完美避免了未能及时收到心跳请求而误认为死亡。可你别忘了,发送心跳的目的就是为了及时通知协调者自己是否健康。最近的新冠肺炎,要求我们每天上报一次健康状况,如果每小时上报一次,未免太浪费人力了,如果一周上报一次,又会带来严重后果。所以session.timeout.ms
这个参数,不宜最长,毕竟,我们还是希望能尽快揪出那些“尸位素餐”的 Consumer,早日把它们踢出 Group。heartbeat.interval.ms
这个值也不宜过短,频繁地发送心跳请求会额外消耗带宽资源。
推荐设置 session.timeout.ms = 6s
。
推荐设置 heartbeat.interval.ms = 2s
。
保证 Consumer 实例在被判定为“dead”之前,能够发送至少 3 轮的心跳请求,因此上面推荐的配置是一个三倍的关系。
二、消费时间过长
又回到开头那个场景,我初次遇到重平衡就是因为我业务逻辑复杂,Consumer 因为处理这些消息的时间太长而引发 Rebalance 了。
Consumer 端有一个参数,用于控制 Consumer 实际消费能力对 Rebalance 的影响,即 max.poll.interval.ms
参数。它限定了 Consumer 端应用程序两次调用 poll 方法的最大时间间隔。它的默认值是 5 分钟,表示你的 Consumer 程序如果在 5 分钟之内无法消费完 poll 方法返回的消息,那么 Consumer 会主动发起“离开组”的请求,Coordinator 也会开启新一轮 Rebalance。因此你最好将该参数值设置得大一点,比你的下游最大处理时间稍长一点。还可以配置一个参数max.poll.records
,它代表批量消费的 size,如果一次性 poll 的数据量过多,导致一次 poll 的处理无法在指定时间内完成,则会 Rebalance。因此,你需要预估你的业务处理时间,并正确的设置这两个参数。
三、GC 问题
按照上面的推荐数值恰当地设置了这几个参数,却发现还是出现了 不必要的 Rebalance,那么可能是 Consumer 端的 GC 导致的,比如是否出现了频繁的 Full GC 导致的长时间停顿,从而引发了 Rebalance。
本文从消费组的概念和优势出发,带你看到了它大名鼎鼎的一方面,但由它导致的重平衡问题却是众多开发者心中的毒瘤,它为何会出现,重平衡的意义在哪,什么时候会触发、又该如何避免呢。我们一定要避免因为各种参数或逻辑不合理而导致的组成员意外离组或退出的情形,与之相关的主要参数有:
session.timeout.ms
heartbeat.interval.ms
max.poll.interval.ms
max.poll.records
GC 参数
恰当地设置这些参数,你一定能够大幅度地降低生产环境中的 Rebalance 数量,从而真正享受消费组和重平衡带来的利好。
参考文档:
https://time.geekbang.org/column/article/106904 http://kafka.apache.org/22/documentation.html
http://kafka.apache.org/22/javadoc/index.html?org/apache/kafka/clients/consumer/KafkaConsumer.html
END