什么是 Kafka Rebalance 以及关于 Rebalance Kafka-Python 社区客户端应该关注的地方
什么是 Rebalance?
Rebalance 为什么会发生?
Rebalance 的过程
记得之前在一段时间密集面试的时候总会问候选人这些问题。
什么是 Rebalance
重平衡 Rebalance 就是让整个 Consumer Group 下的所有的 Consumer 实例久如何消费订阅主题的所有分区达成共识的过程。在 Rebalance 的过程中,所有 Consumer 实例都需要参与进来,在 Coordinator 的帮助下完成分配。所以可以很明显的回答上面的第三个问题,在 Rebalance 的时候是无法进行消费的持续消费的。就可能会造成队列的段时间阻塞。
Rebalance 为什么会发生
1. 我们发现线上处理能力不够了,需要向 group 中增加新的 consumer ,就会触发 Rebalance 。任何新成员的「进入」和「离开」都会触发 Rebalance。消费会停下来重新给所有 Consumer 分配对应的 partiitons.
2.我们为 topic 增加分区,比如原本一个 topic 只有 10个分区,后因为性能问题需要扩展增加到 20 个分区就会发生 Rebanlace.
大的情况可以分为这两种,其实第一种新成员 「进入」 和 「离开」还可以细讲一下,因为 90% 以上的离开和进入都不是我们想要的结果。他有可能是消费超过超时时间被一脚踢出了 group 造成离开从而造成 Rebalance 。然后又因为踢出之后又去请求又意外的加入 group 从而继续引发 Rebalance 往复循环。
我们的消费者在与 broker 进行沟通的时候都是与一个叫 Coordinator 的组件进行交互, Coodinator 是专门负责管理消费者组 加入离开和位移的组件。
那么什么情况下 Coordinator 会认为某个 Consumer 实例挂了需要退组呢?
当 Rebalance 完成之后,我们的每个 Consumer 都会向 Coordinator 定时发送心跳,以表明客户端还活着。
如果某个 Consumer 没有按照约定好的规则发送请求给 Coordinator ,Coordinator就会认为这个 Consumer 已经挂了,并将其一脚踢出 Group 然后通过心跳包 response 组内其他 Consumer 尽快开始 Rebalance。这里有一点需要注意,Consumer 心跳参数 heartbeat_interval_ms 会在一个 session_timeout 周期内决定是否 Consumer 已经离开。
打个比方,如果我们的 session_timeout_ms 设置为默认的 10s 那么如果心跳三次 heartbeat_interval_ms 都失败那么就会让 Coordinator 开启重平衡。而 Coordinator 通知组内其他消费者的办法也是通过消费者发送的心跳,在 broker 对他进行 response 的时候塞进 REBALANCE_NEEDED 标志位,通知他们进行 rebalance。
Kafka broker 端有个日志叫 server.log ,在这个日志中我们可以看到 GroupCoordinator 的身影
[2019-07-17 15:20:37,266] INFO [GroupCoordinator 0]: Preparing to rebalance group answer_action with old generation 741 (__consumer_offsets-9) (kafka.coordinator.group.GroupCoordinator) [2019-07-17 15:20:37,266] INFO [GroupCoordinator 0]: Group answer_action with generation 742 is now empty (__consumer_offsets-9) (kafka.coordinator.group.GroupCoordinator) [2019-07-17 15:21:37,662] INFO [GroupCoordinator 0]: Preparing to rebalance group answer_action with old generation 742 (__consumer_offsets-9) (kafka.coordinator.group.GroupCoordinator) [2019-07-17 15:22:07,663] INFO [GroupCoordinator 0]: Stabilized group answer_action generation 743 (__consumer_offsets-9) (kafka.coordinator.group.GroupCoordinator) [2019-07-17 15:22:07,664] INFO [GroupCoordinator 0]: Assignment received from leader for group answer_action for generation 743 (kafka.coordinator.group.GroupCoordinator) [2019-07-17 15:22:37,664] INFO [GroupCoordinator 0]: Member kafka-python-1.3.5-3feb5deb-de82-40a4-8da9-1c6ec7d1ca4f in group answer_action has failed, removing it from the group (kafka.coordinator.group.GroupCoordinator)
从第一条开始可以看到是 preparing to rebalance ,group-id 是 answer_action
但是大家注意
[2019-07-17 15:22:37,664] INFO [GroupCoordinator 0]: Member kafka-python-1.3.5-3feb5deb-de82-40a4-8da9-1c6ec7d1ca4f in group answer_action has failed, removing it from the group (kafka.coordinator.group.GroupCoordinator)
因为 group 中其中有一个 consumer 的异常,被 coordinator 踢出了 group 。
所以平时 Kafka 出现的 Rebalance 相关的异常我们可以很容易的在日志中发现,并且可以得到一些细节。
Consumer 端中有一个参数
session_timeout_ms (int): The timeout used to detect failures when using Kafka's group management facilities. The consumer sends periodic heartbeats to indicate its liveness to the broker. If no heartbeats are received by the broker before the expiration of this session timeout, then the broker will remove this consumer from the group and initiate a rebalance. Note that the value must be in the allowable range as configured in the broker configuration by group.min.session.timeout.ms and group.max.session.timeout.ms. Default: 10000
Python 社区客户端 kafka-python 该参数的默认值是 10s 。
如果 Coordinator 在 10s 内没有收到 Group 下 Consumer 的实例心跳,就会认为这个 Consumer 已经挂了,就会触发 Rebalance。
除了 session.timeout.ms 还有一个控制发送心跳周期的参数 heartbeat.interval.ms
heartbeat_interval_ms (int): The expected time in milliseconds between heartbeats to the consumer coordinator when using Kafka's group management facilities. Heartbeats are used to ensure that the consumer's session stays active and to facilitate rebalancing when new consumers join or leave the group. The value must be set lower than session_timeout_ms, but typically should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances. Default: 3000
heartbeat.interval.ms 的默认周期是 3s。这里社区客户端作者也描述得非常清楚(社区客户端的文档真的很好!)应该设置这个参数小于 session_timeout_ms ,通常大家会推荐一个公式为
heartbet_inverval_ms * 3 = session_timeout_ms
为什么要这么做?
因为 session_timeout_ms 到期如果 coordinator 没有收到心跳会认为客户端死了,如果按照上述的配置,期间客户端至少有三次时间访问到 coordinator 并且刷新过期时间。这样如果中间有 1 2 次因为网络问题没有发送成功的情况也可以一定程度避免。
心跳时间不超过 session_timeout_ms 的值,但是也不应该过长,过长可能会引起 Rebalance 的检测缓慢且失去效果。如果有一个 consumer 失效了,如果他无法再恢复我们要做的迅速让他被踢出 group 中。否则严重的话可能会引起 partitions 的数据倾斜,lag 也会越来越大。
除了
session.timeout.ms
heartbeat.interval.ms
还有两个参数从客户端角度去控制 Rebalance
max_poll_records (int): The maximum number of records returned in a single call to :meth:`~kafka.KafkaConsumer.poll`. Default: 500
max_poll_interval_ms (int): The maximum delay between invocations of :meth:`~kafka.KafkaConsumer.poll` when using consumer group management. This places an upper bound on the amount of time that the consumer can be idle before fetching more records. If :meth:`~kafka.KafkaConsumer.poll` is not called before expiration of this timeout, then the consumer is considered failed and the group will rebalance in order to reassign the partitions to another member. Default 300000
max_poll_records 控制我们一次从 broker 请求多少数据过来,默认是 500 条。
max_poll_interval_ms 控制两次 poll 之间的时间间隔,如果我们请求过来的 500 条消息 300 s 都还没有消费完,没有继续调用 poll 那么 consumer 会自动向 coordinator 发出消息要求把自己踢出组内。coordinator 收到消息会开始新一轮的 Rebalance。
这里关于提交的时间还有一个需要被注意的地方。在社区 Kafka-Python 1.4.0 以下的版本中(broker 0.10.1 之前),心跳是跟随 poll 一起发送的。并没有启动一个 background 独立的线程去发送心跳包。这会造成一个什么问题呢?
之前如果我们拉取一批消息开始处理,他如果超过了设置的 session.timeout.ms 也就是 默认的 10s ,那么就会被触发 rebalance 。因为如果你不调用 poll 方法,你就无法发送心跳。Coodinator 无法收到心跳就会按照约定把你踢出 group 然后进行 rebalance .
Java 版本在发布了该功能之后,社区 kafka 版本从 1.4.0 之后开始支持了 background thread 处理心跳。详情可以参阅 reference。
请注意 max_poll_inerval_ms 这个参数是1.4.0版本以上的参数 1.3 的最后一个版本 1.3.5 不会有该参数的存在。所以如果使用 1.3.5 版本及其以下版本需要自己保证 单次拉取数据 的处理时间 < session_timeout_ms ,否则就会不停的触发 Rebalance 导致程序重复消费,严重可能引起死循环崩溃。
另外 1.3.5 及其以下版本中 session.timeout.ms 的默认值是 30s 并非现在版本的 10s ,时间比较长也是为了避免长时间处理引发的 Rebanlance ,老版本心跳的时间还是 3s 。
所以如果有要阻塞处理的任务,比如 retry 比如调用很多数据库操作,我们可以把 max_poll_interval_ms 的时间设得长一些,或者把需要处理的消息 max_poll_records 设置得少一些。这样至少不会因为正常的业务处理得慢而造成 kafka 频繁 Rebalance 从而引起其他的问题。
Rebalance 的过程
现在我们倒头回来聊聊 Rebalance 的过程。rebalance 的前提是当前消费者组 Coordinator 已经确定的情况。
在该 Consumer group 组有 Consumer 第一次请求的时候就会被分配 Coordinator broker .
计算方法为:
offset_partition = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)
得到的分区的 leader 就是对应的 Coordinator
e.g.
public static void main(String[] args) {
String c = "illidan-c";
System.out.println(Math.abs(c.hashCode() % 10));
}
这里我的 nums.partitions 是 10 所以这里我得到的数是 9 然后查看 __consumer_offsets 9 的 leader 是 broker 0
然后我手动重启进程对其进行 rebalance 得到日志输出
[2020-01-06 12:11:23,808] INFO [GroupCoordinator 0]: Preparing to rebalance group illidan-c with old generation 158651 (__consumer_offsets-9) (kafka.coordinator.group.GroupCoordinator)
[2020-01-06 12:11:30,650] INFO [GroupCoordinator 0]: Member kafka-python-1.4.7-d03089db-9df1-4464-bf06-4968df80bfa6 in group illidan-c has failed, removing it from the group (kafka.coordinator.group.GroupCoordinator)
[2020-01-06 12:11:30,960] INFO [GroupCoordinator 0]: Member kafka-python-1.4.7-50c01dbc-eab5-41aa-823b-3530d32f845d in group illidan-c has failed, removing it from the group (kafka.coordinator.group.GroupCoordinator)
[2020-01-06 12:11:30,960] INFO [GroupCoordinator 0]: Stabilized group illidan-c generation 158652 (__consumer_offsets-9) (kafka.coordinator.group.GroupCoordinator)
[2020-01-06 12:11:30,979] INFO [GroupCoordinator 0]: Assignment received from leader for group illidan-c for generation 158652 (kafka.coordinator.group.GroupCoordinator)
[2020-01-06 12:11:40,980] INFO [GroupCoordinator 0]: Member kafka-python-1.4.7-0094bb59-7cc1-4c80-9020-7c10c61bae98 in group illidan-c has failed, removing it from the group (kafka.coordinator.group.GroupCoordinator)
[2020-01-06 12:11:40,980] INFO [GroupCoordinator 0]: Preparing to rebalance group illidan-c with old generation 158652 (__consumer_offsets-9) (kafka.coordinator.group.GroupCoordinator)
可以很清楚的看到即 consumer_offsets-9 作为 coordinator 的机器 broker 0 重新开始 Rebalance
之后该 group 组内的消费者都与此 coordinator 进行通信。
这里要再次强调一下。 关于 coordinator 是分为 broker 端 group coordinator 组件和消费者组这边的 coordinator leader 同步方案分配制定人的。
通常意义上我们说的 coordinator 一般指的是 broker 端的 group coordinator 组件。
Rebalance 的时候分为两步
1. Join 加入组。这一步中,所有成员都向coordinator发送JoinGroup请求,请求入组。一旦所有成员都发送了JoinGroup请求,coordinator会从中选择一个consumer担任leader的角色,并把组成员信息以及订阅信息发给leader——注意leader和coordinator不是一个概念。leader负责消费分配方案的制定。
2. Sync 这一步leader开始分配消费方案,即哪个consumer负责消费哪些topic的哪些partition。一旦完成分配,leader会将这个方案封装进SyncGroup请求中发给coordinator,非leader也会发SyncGroup请求,只是内容为空。coordinator接收到分配方案之后会把方案塞进SyncGroup的response中发给各个consumer。这样组内的所有成员就都知道自己应该消费哪些分区了。
上面提到的当 response 发回,那么我们需要等到所有消费者都确认了 SyncGroup 才会开始 response ,kafka 会将已经先收到的请求放到一个 purgatory 的地方。
总结一下就是当所有消费者成员都发送了 Join 加入组消息之后,broker 端 coordinator 会选举一个 consumer 担任 leader 角色。
这个 leader 要做的事情就是接收 coordinator 发送的成员信息及成员订阅 topic 信息并为这个消费者组内的消费者消费什么 parititons 制定分配方案和策略。
当方案制定完成之后,ledaer 会将方案通过 SyncGroup 请求发送回 coordinator ,这时非 leader 的 consumer 也会发送 SyncGroup 请求给 coordinator 然后 coordinator 将接收到的 leader 发来的分配方案通过 SyncGroup 的 response 发送回各 consumer 从而来完成 rebalance。
最后附上一张 rebalance 的状态机图片。
Reference:
https://www.cnblogs.com/huxi2b/p/6223228.html Kafka消费组(consumer group)
https://matt33.com/2018/01/28/server-group-coordinator/ Kafka 源码解析之 GroupCoordinator 详解(十)
https://github.com/dpkp/kafka-python/releases
https://github.com/dpkp/kafka-python/pull/1266 KAFKA-3888 Use background thread to process consumer heartbeats
https://cwiki.apache.org/confluence/display/KAFKA/KIP-62%3A+Allow+consumer+to+send+heartbeats+from+a+background+thread KIP-62: Allow consumer to send heartbeats from a background thread