摘要
在这一篇文章中,我将向你介绍消费者的一些参数。
这些参数影响了每次poll()
请求的数据量,以及等待时间。
在这之后,我将向你介绍Kafka用来保证消费者扩展性以及可用性的设计——消费者组。
在消费者组的介绍中,我将重点放在了Rebalance
的过程上,因为这是一个很重要又经常发生,还会导致消费者组不可用的操作。
1 消费者参数配置
对于一个消费者来说,他要做的事情只有一件,那就是使用poll()
来拉取消息。
至于他是从哪个分区拉取,则是靠消费者组来动态的调整这个消费者所消费的分区,又或者是由开发者来自定义。
但无论如何,这个消费者都需要通过poll()
来拉取消息。
这也是这一节的内容:通过参数配置能够影响poll操作的哪些内容。
首先需要确定一点,当消费者使用poll()
拉取消息的时候,他只能拉到HW水位线及以下的消息。
1.1 分区配置
我们可以让消费者针对于某一个分区进行消费。
为了实现这个目标,我们可以用assign()
方法。
但是注意,当这个消费者不是单独的一个消费者,而是属于某个消费者组的时候,将不允许使用自定义的分区分配。
1.2 POLL操作拉取的字节数目
对应的配置分别是:
fetch.min.bytes
对于每次拉取的最小字节数,默认是1。当拉取的消息大小小于设定的这个限度时,将会等待,直到这次被拉取的消息大小大于这个值。
于是我们可以得知,当我们即将要消费的消息比较小时,可以适当的调大这个参数的值,以提高吞吐量。
但是注意,这也可能造成消息的额外延迟。
fetch.max.bytes
这个参数跟上面的一样,只不过他代表的意义是最大的字节数。
但是这存在一个问题,如果我们的消息大小全都大于这个参数的值,会发生什么情况呢?
答案是会返回即将拉取分区的第一条消息。
也就是说在这个参数中,不存在“不符合条件就不返回数据”的情况。
还有一个参数,叫做max.partition.fetch.bytes
这个参数跟上面提到的每次拉取的最大字节数工作原理是一样的,也是会保证当消息大于设定的值的时候,一定会返回数据。
而不同的地方在于,这个参数代表的是分区。也就是说,一个参数代表的是一次拉取请求,而另外一个参数代表的是针对于每一个分区的拉取请求。
1.3 拉取消息的超时时间
fetch.max.wait.ms
这个参数的意义在于:如果拉取消息的时间达到了这个参数设定的值,那么无论符不符合其他条件,都会返回数据。
那么你很容易可以猜到,这个参数跟fetch.min.bytes
是有关系的,这是为了防止当fetch.min.bytes
参数设置的过大,导致无法返回消息的情况。
当然了,这个参数还有一个意义,如果你的业务需要更小的延迟,那么应该调小这个参数。
1.4 最大拉取消息数
如果我们的最大拉取字节数设置成了非常大,那么是不是代表我们每一次的poll()
,都能直接拉到HW水位呢?
答案是否定的。
还存在一个参数:
max.poll.records
这个参数的意义在于,每次拉取消息的最大数量。
同样的,如果消息的大小都比较小,那么可以调大这个参数,以提高消费速度。
1.5 消费者组相关的参数
另外,还存在一些消费者组相关的参数,我在这里先提一下,具体更详细的解释,将在后文给出。
heartbeat.interval.ms
这个参数是设置消费者与消费者组对应的Coordinator发送心跳响应的间隔时间。
session.timeout.ms
这个参数是用于Coordinator判断多长时间没收到消费者的心跳响应而认为这个消费者已经下线的时间。
max.poll.interval.ms
这个参数用于Coordinator判断多长时间内消费者都没有拉取消息,而认为这个消费者已经下线的时间。
auto.offset.reset
这个参数其实跟消费者组的联系不是很大,但是我认为可以写在这里。
因为有这么一个场景,当消费者Rebalance之后,如果位移主题之前保存的位移已经被删除了,那么这个参数就决定了消费者该从哪里开始消费。
当然了,关于消费者还有许多的参数,不仅仅是上文提到的这些。
而上文提到的这些参数,是我认为可以让初学者更好的理解消费者的工作原理。
2 Rebalance原理
在解释Rebalance的原理之前,我想先跟你说一下我的思路,免得你看的一头雾水。
当然了,这个思路是我认为更适合我自己去理解的。你也可以先看第三大节,再有了一个大概的认识后,再来看这一节的内容。
我希望先告诉你Rebalance的过程是怎么样的,这里说的过程指的是Rebalance已经发生了,那么在Rebalance的过程中,会发生哪些事情。
在这之后,我再跟你说说Rebalance的五种状态。
那么,我们开始。
2.1 寻找Coordinator
首先,应该有一个认识。Rebalance的所有操作都是通过Coordinator的协调下完成的,组内的消费者之间并不会进行相关的通信与交流。
Coordinator你可以理解为是一个服务,位于某个broker节点上。
假设当前的消费者已经保存了这个这个节点的信息,那么将会直接进入第二步。
如果当前的消费者没有保存这个信息(比如这是一个新加入这个消费者组的消费者),那么他需要先找到这个Coordinator所在的broker节点。
这里的broker节点,是这个消费者对应的消费者组对应的位移主题的分区的leader节点。
听起来有点绕,让我来再解释一下。
消费者 -> 消费者组 -> __consumer_offsets -> partition -> leader
关于位移主题,我已经在第二篇文章中提到过了,在这里不再赘述。
但是在这里,让我们来再来回忆一遍消费者组对应的partition是怎么找到的。
- 先获取
Group ID
的hash值 - 将这个hash值,对
__consumer_offsets
的分区数取模 - 获得的数字,就是这个消费者组提交位移的分区
- 找到这个分区对应的leader副本,即为Coordinator对应的broker节点
2.2 Join Group
在找到了对应的broker节点后,第二步是发送加入Group的请求。
在这一步中,无论是之前已经在Group内的成员,还是准备加入Group的成员,都需要发送Join Group的申请。
在发起的JoinGroupRequest中,需要包含如下的数据:
-
Group id
-
Session_timeout
-
Rebalance_timeout
-
Menber_id
-
Partition assignor
需要事先说明的是,这里的名称并不严格,是为了更好的理解而这样写的。如果你想要知道更加严谨的请求内容,可以去看厮大的《深入理解Kafka》。
下面我们挨个解释:
Group ID
,消费者组ID,代表了即将加入的消费者组。
Session_timeout
,上文中提到过这个参数,用于Coordinator判断多长时间内没收到客户端的心跳包而认为这个客户端已经下线。
Rebalance_timeout
,值等同于max.poll.interval.ms
,意义在于告知Coordinator用多长的时间来等待其他消费者加入这个消费者组。
我们在上文中提到,无论之前是不是这个消费者组的成员,只要开启了Rebalance,就需要重新加入这个消费者组。因此,Coordinator需要一段时间来接受JoinGroupRequest的请求。
至于为什么需要一段时间来接受请求,以及这段时间发生了什么,我将在后面给你解释。
menber_id
,作为组内消费者的识别编号,如果是新加入组的消费者,这个字段留空。
Partition assignor
,指的是分区分配方式。因为Rebalance这个过程,就是分区分配的一个过程。每个消费者将其接受的分配方式放在这个字段中,随后由Coordinator选出每个消费者都认可的分区分配方式。
然后我们来聊聊在这个阶段,Coordinator需要做什么。
Coordinator需要一段时间来接收来自客户端的JoinGroupRequest请求,是因为Coordinator需要收集每一个成员的信息,选出leader和分区分配方式,因此,Coordinator需要足够的时间来“收集信息”。这就回答了上文说到的为什么“Coordinator需要一段时间来接受JoinGroupRequest的请求”。
选举leader的算法很简单,第一个发送请求的consumer,就是leader。
选出分区分配策略的算法也很简单,首先Coordinator会收集所有消费者都支持的分区分配方式,然后每个消费者为它支持的分配方式投上一票。注意,这里的投票行为没有经过多一次的交互,而是Coordinator选取每个消费者的JoinGroupRequest中的第一个分区分配方式,作为这个消费者所投的票。
当Coordinator选取好Leader和分区分配方式后,将返回JoinGroupResponse给各个消费者。
在返回给各个消费者的JoinGroupResponse中,包含了menber_id,分区分配方式等。而对于leader消费者来说,还将获得组内其他消费者的元数据,包含了各个消费者的menber_id,分区分配方式。
至此,JoinGroup阶段完成。
注意,每个消费者从发送JoinGroupRequest到接收到JoinGroupResponse请求这段时间,是阻塞的。
2.3 分配分区
在第二步结束之后,每个消费者已经知道了自己的menber_id
,以及Coordinator所选择的分区分配方式。
但是此时每个消费者还不知道自己应该消费哪个分区。
这个分区分配的过程,是交给Leader消费者来完成的。
但是注意,虽然说这个过程是Leader消费者完成的,但是Leader消费者并不会跟其他消费者直接通信,而是将分配方式告知Coordinator,由Coordinator来告知各个消费者。
这个过程,称为Sync_Group
。
在这个过程中,每一个消费者都会发送SyncGroupRequest给Coordinator。要注意的是,Leader消费者在这个Request中还附带了其他消费者的分区分配信息。
在Coordinator收到了这些请求后,会将这个分区分配方案等元数据保存在__consumer_offsets
主题中。
随后,Coordinator将发送响应给各个消费者。
在这个响应中,包含了各个消费者应该负责消费的分区编号。
至此,每个消费者都了解了自己应该消费的分区是哪些了。
2.4 消费并发送心跳包
在上一个阶段中,组内各个消费者已经知道了自己负责的是哪些分区。
但是还存在一个问题,消费者应该从分区的哪个位置开始消费呢?
这就用到了__consumer_offsets
主题了,这个主题保存了某个消费者组的各个分区的消费位移。
此外,每个消费者还需要不断地发送心跳包给Coordinator,以告知Coordinator自己没有下线。
这个发送心跳包的时间,就是我们设置的heartbeat.interval.ms
参数。
在每个心跳包的响应中,Coordinator就会告知这个消费者,需不需要Rebalance。
那么也就说明了,这个参数设置的越小,消费者就越早能够得知是否需要Rebalance。
而对应的session.timeout.ms
,指的就是Coordinator在这么长的时间内没收到消费者的心跳包,而认为这个消费者过期的参数。
3 消费者组的状态转移
在上面说完了Rebalance的核心原理后,我们再来聊聊消费者组的各个状态。
先来介绍一下消费者组有哪几种状态:
- Empty:组内没有任何的成员,但是保留着这些成员的元数据,比如在发生Rebalance的时候,Coordinator在心跳包的响应中告知消费者应该要进行Rebalance了,这个时候所有的消费者都离开了消费者组,那么这个消费者组就会处于Empty状态。注意,一个新创建的消费者组,也处于这个状态。
- Dead:组内没有任何的成员,并且在
__consumer_offsets
中也没有保存这个消费者组的元数据。通常发生在这个消费者组被删除了,或者__consumer_offsets
分区leader发生了改变。(至于这个状态我了解的也不是很多,如果可以的话,麻烦你评论区告诉我。) - PreparingRebalance:这个状态为Coordinator正在等待Consumer加入。这个状态对应于JoinGroup阶段,会持续
Rebalance_timeout
这么长的时间。 - CompletingRebalance:也被称为AwaitingSync,为Coordinator正在等待Leader消费者的分区分配方案。对应于SyncGroup阶段。
- Stable:到了这个阶段,消费者组已经在正常工作了。
消费者组的状态介绍大概就是这样的。
简单的来讲,当一个消费者组需要Rebalance的时候,他就会进入PreparingRebalance阶段,然后一直流转到Stable阶段。
在这个期间,如果有任何的成员变动,就会回到PreparingRebalance阶段。
在这个期间,如果Coordinator改变,或者消费者组被删除等,就会进入Dead阶段。
写到最后
首先,谢谢你能看到这里!
在这一篇文章中,我没有像介绍生产者那样介绍一遍源码。
因为对于生产者来说,他只需要将消息发送到broker中,而对于消费者来说,这个过程复杂得多,我希望能够用比较浅显易懂的方式,让你能够了解消费者组的工作方式。
在有了这样的一个认识之后,无论使用什么客户端,我认为都不会有太大的问题。
此外,在这一篇中我花了较大的笔墨去介绍Rebalance的过程,是因为Rebalance是一个很常见的现象,而且在这期间会导致Kafka消费者的不可用,所以我希望了解了Rebalance的工作原理,能够让你更容易的避免不必要的Rebalance。
当然了,因为作者才疏学浅能力有限,可能在这个过程中忽略了一些很重要的细节,又或者有一些错误的理解。如果你发现了,还请不吝指教,谢谢你!
再次谢谢你能看到这里,感恩~