Kafka消费者组与重平衡
Kafka消费者组与重平衡
Kafka消费者组
传统的消息队列处理模型主要有以下两种:
-
队列模型
类似队列的数据结构,生产者生产消息入队,消费者消费消息出队,并删除队列中的数据,不能重复消费,这种模型只能由一个消费者消费,无法让多个消费者同时消费同一条消息。
-
发布-订阅模型
发布-订阅模型,多了个主题的概念,消费者需要订阅某个主题,生产者发送消息到主题中,订阅了该主题的所有消费者都可以接收到该消息,可以满足多个消费者同时消费同一条消息。
Kafka的消费者组设计,使得Kafka可以同时实现这两种模型,同时还能对消费者组进行扩容,让消费变得易伸缩
每个消费者组中包含一个或多个消费者,这些消费者实例共享一个id,成为group id,默认创建的group id 在KAFKA_HOME/conf/consumer.properties
可以配置,默认为test-consumer-group
,消费者组中的所有成员一起订阅某个主题下的分区
注意:一个分区只能由组内的一个消费者订阅
消费者组内消费者数量
因一个分区只能由一个消费者订阅,所以一个消费者组中的消费者数量不应大于分区数量,多余的消费者不会分到任何分区,一般分区的数量为组内消费者的倍数,最好分区数和消费者数保持一致。
# 查看分组列表
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
# 查看具体到某个组的消费者情况
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group
重平衡机制
重平衡(Reblalance)机制决定了如何让消费者组中的消费者来分配topic下的分区,重平衡的触发条件有以下三个:
- 消费者组内成员发生变更,这个变更包括了增加和减少消费者。注意这里的减少有很大的可能是被动的,就是某个消费者崩溃退出了
- 主题的分区数发生变更,kafka目前只支持增加分区,当增加的时候就会触发重平衡
- 订阅的主题发生变化,当消费者组使用正则表达式订阅主题,而恰好又新建了对应的主题,就会触发重平衡
当Kafka触发重平衡之后,在进行重平衡过程中,Kafka完全处于不可用状态,消费者无法从Kafka消费消息,如果Kafka节点过多,重平衡持续时间会比较长。
三种重平衡策略
Range
具体实现位于,package org.apache.kafka.clients.consumer.RangeAssignor。
这种分配是基于每个主题的分区分配,如果主题的分区分区不能平均分配给组内每个消费者,那么对该主题,某些消费者会被分配到额外的分区。我们来看看具体的例子。
举例:目前有两个消费者C0和C1,两个主题t0和t1,每个主题三个分区,分别是t0p0,t0p1,t0p2,和t1p0,t1p1,t1p2。
那么分配情况会是:
- C0:t0p0, t0p1, t1p0, t1p1
- C1:t0p2, t1p2
我来大概解释一下,range这种模式,消费者被分配的单位是基于主题的,拿上面的例子来说,是主题t0的三个分区分配给2个消费者,t1三个分区分配给消费者。于是便会出现消费者c0分配到主题t0两个分区,以及t1两个分区的情况(一个主题有三个分区,三个分区无法匹配两个消费者,势必有一个消费者分到两个分区),而非每个消费者分配两个主题各三个分区。
RoundRobin
具体实现位于,package org.apache.kafka.clients.consumer.RoundRobinAssignor。
RoundRobin是基于全部主题的分区来进行分配的,同时这种分配也是kafka默认的rebalance分区策略。还是用刚刚的例子来看,
举例:两个消费者C0和C1,两个主题t0和t1,每个主题三个分区,分别是t0p0,t0p1,t0p2,和t1p0,t1p1,t1p2。
由于是基于全部主题的分区,那么分配情况会是:
- C0:t0p0, t0p1, t1p1
- C1:t1p0, t0p2, t1p2
因为是基于全部主题的分区来平均分配给消费者,所以这种分配策略能更加均衡得分配分区给每一个消费者。
上面说的都是同一消费者组内消费组都订阅相同主题的情况。更复杂的情况是,同一组内的消费者订阅不同的主题,那么任然可能会导致分区不均衡的情况。
还是举例说明,有三个消费者C0,C1,C2 。三个主题t0,t1,t2,分别有1,2,3个分区 t0p0,t1p0,t1p1,t2p0,t2p1,t2p2。
其中,C0订阅t0,C1订阅t0,t1。C2订阅t0,t1,t2。最终订阅情况如下:
- C0:t0p0
- C1:t1p0
- C2:t1p1,t2p0,t2p1,t2p2
这个结果乍一看有点迷,其实可以这样理解,按照序号顺序进行循环分配,t0只有一个分区,先碰到C0就分配给它了。t1有两个分区,被C1和C2订阅,那么会循环将两个分区分配出去,最后到t2,有三个分区,却只有C2订阅,那么就将三个分区分配给C2。
Sticky
Sticky分配策略是最新的也是最复杂的策略,其具体实现位于package org.apache.kafka.clients.consumer.StickyAssignor。
这种分配策略是在0.11.0才被提出来的,主要是为了一定程度解决上面提到的重平衡非要重新分配全部分区的问题。称为粘性分配策略。
听名字就知道,主要是为了让目前的分配尽可能保持不变,只挪动尽可能少的分区来实现重平衡。
还是举例说明,有三个消费者C0,C1,C2 。三个主题t0,t1,t2,t3。每个主题各有两个分区, t0p0,t0p1,t1p0,t1p1,t2p0,t2p1,t3p0,t3p1。
现在订阅情况如下:
- C0:t0p0,t1p1,t3p0
- C1:t0p1,t2p0,t3p1
- C2:t1p0,t2p1
假设现在C1挂掉了,如果是RoundRobin分配策略,那么会变成下面这样:
- C0:t0p0,t1p0,t2p0,t3p0
- C2:t0p1,t1p1,t2p1,t3p1
就是说它会全部重新打乱,再分配,而如何使用Sticky分配策略,会变成这样:
- C0:t0p0,t1p1,t3p0,t2p0
- C2:t1p0,t2p1,t0p1,t3p1
也就是说,尽可能保留了原来的分区情况,不去改变它,在这个基础上进行均衡分配,不过这个策略目前似乎还有些bug,所以实际使用也不多。
避免重平衡
结合上面说的重平衡触发条件,分区变化和topic变化大多是人为因素,这种情况不可避免,而消费者组中的消费者挂掉也不是我们能控制的,但是有些时候Kafka会误认为一个正常的消费者挂掉了,我们可以尽量避免这种情况发生。
当然如果要避免,那首先要知道哪些情况会出现错误判断挂掉的情况。在分布式系统中,通常是通过心跳来维持分布式系统的,kafka也不例外。在分布式系统中,由于网络问题你不清楚没接收到心跳,是因为对方真正挂了还是只是因为负载过重没来得及发生心跳或是网络堵塞。所以一般会约定一个时间,超时即判定对方挂了。而在kafka消费者场景中,session.timout.ms参数就是规定这个超时时间是多少。
还有一个参数,heartbeat.interval.ms,这个参数控制发送心跳的频率,频率越高越不容易被误判,但也会消耗更多资源。
此外,还有最后一个参数,max.poll.interval.ms,我们都知道消费者poll数据后,需要一些处理,再进行拉取。如果两次拉取时间间隔超过这个参数设置的值,那么消费者就会被踢出消费者组。也就是说,拉取,然后处理,这个处理的时间不能超过max.poll.interval.ms这个参数的值。这个参数的默认值是5分钟,而如果消费者接收到数据后会执行耗时的操作,则应该将其设置得大一些。
小结一下,其实主要就是三个参数,session.timout.ms控制心跳超时时间,heartbeat.interval.ms控制心跳发送频率,以及max.poll.interval.ms控制poll的间隔。这里给出一个相对较为合理的配置,如下:
- session.timout.ms:设置为6s
- heartbeat.interval.ms:设置2s
- max.poll.interval.ms:推荐为消费者处理消息最长耗时再加1分钟