Kafka002——说说Rebalance?

840 阅读8分钟

写在前面

这是一道实际的面试题,而且在实际的工作场景中也是经常遇到。在不了解Rebalance时,MQ会带来很多预期之外的问题,而当时的处理方式多少会有点头疼医头,脚痛医脚。因此这次希望能针对这个问题从根源思考,并结合实际的业务场景来重新理解这个问题。

说说Rebalance

这个问题将从以下几个方面回答。

  1. Rebalance是什么?
  2. 为什么会有Rebalance?
  3. Rebalance的原理是什么?
  4. Rebalance会有哪些问题?
  5. 如何降低Rebalance的负面影响?

Rebalance是什么?

Rebalance是一种现象,是描述MQ重新分配Partition给Consumer的动作。

Rebalance也是一种机制,是MQ保障监听Topic的Consumer都能正常消费的机制。

Rebalance还可以看做是一种协议,是MQ设计用来确保Consumer如何达成一致的消费状态,进而确保Topic里的消息顺利投送到Consumer的方法流程。

为什么会有Rebalance?

因为监听MQ的ConsumerGroup内的Consumer会动态变化,比如:新加入成员、成员离开、成员断链、成员处理超时等,还有可能MQ需要分配分区动态变化,比如增加分区,因此在不同的变化情况下,MQ为了确保Consumer都能正常消费,设计了Rebalance机制。这也能解释什么时候会出现Rebalance:

  1. ConsumerGroup成员变化:上线/下线/故障异常
  2. Partition分区数量变化:
  3. Coordinate节点异常;
  4. 当Consmer使用正则订阅Topic后,有满足正则匹配的新Topic创建之后,也会触发一次Rebalance,来保证Consumer能订阅到新Topic。

Rebalance的触发时机是什么?

以Kafka为例(目前主要看了Kafka)。 首先针对Rebalance出现的原因来看,原理可以归为以下两类:

  1. MQ分区数量调整
  2. Consumer动态变化

Partition分区数量变化

从一条消息的视角来看,一条消息会被放置在投递的Topic的某个Partition中。而为了确保消息不会被Consumer重复消费,一个Partition只能被同一个ConsumerGroup里的一个Consumer消费到。因此当MQ增加了Partition数量,意味着新的消息会放在新的Partition里,而已有的Consumer订阅的是旧的Partition,这就会有消息消费不到,所以这时需要一个Rebalance,来重新分配现有的Partition给各个Consumer。

这里可以引申两个问题:

  1. 消息是怎么决定要投放到哪个Partition中的?
  2. Kafka调整分区数量有什么需要注意的么?

简单的说明一下:

  1. 有三种方式:a. Kafka自定义投递规则,默认采用轮询的方式将生产的消息依次轮流放入Topic的各个Partition里。这样的好处是:没有热点问题,Partition消息数量分布均匀;b. 设置PartitionKey,Kafka会根据这个Key来做hash进而路由到对应的Partition上。这样的好处是:可以指定消息投递的Partition,进而保证消息局部的有序;缺点是:如果Partition分布不均,会有热点问题;c. 自定义规则。
  2. Kafka分区数量对应到消费并发能力,因为一个Partition只能被同Group的一个Consumer消费,所以Partition的数量不会超过Consumer的数量(否则会有Consumer分配不到Partition而空跑)。Kafka分区数量支持增加,但不支持减少(实现成本太高收益太少)。但还是建议预估Topic承载与消费的量级,而提前预估分区的数量。

ConsumerGroup成员变化

主动的

  1. 新的Consumer加入Group
  2. Consumer离开Group

Consumer的加入和离开Group可以以Consumer视角为主,来用另一篇文章说明Kafka的Consumer的状态流转。这里先按下不表。

被动的

Kafka主要通过三个参数来判断一个Consumer是否健康:

  1. session.timeout.ms:Consumer与Kafka连接Session的最大超时时长。在这个时长内,Kafka必须收到Consumer的心跳消息,才会认为Consumer是健康的。在Kafka3.0中,该参数默认值为45s。
  2. heartbeat.interval.ms:Consumer向Kafka发送心跳的间隔,也可理解为Consumer刷新与Kafka连接Session的间隔时间。该参数默认值为3s。
  3. max.poll.interval.ms:Consumer消费消息的最长处理时间,该参数默认为5min。

Consumer与Kafka的维活Session

Cosnumer需要每隔 heartbeat.interval.ms 向 broker上的Group Coordinator来发送心跳,同时Group Coordinator也会按照session.timeout.ms时间来等待Consumer的心跳。考虑 Group Coordinator 是否有在session.timeout.ms内收到心跳:

  1. 若收到心跳,则重置等待心跳时间,重新计算等待心跳时间直到等待时长到达 session.timeout.ms
  2. 若没有收到心跳,则 Group Coordinator 会触发一次 Rebalance。

考虑到Consumer向Kafka发送的心跳有可能丢失,session.timeout.ms 可以设置为 heartbeat.interval.ms 的3~5倍。

Consumer处理耗时

Consumer通过poll()方法获取批量待处理的消息后,Group Coordinator便会等到计算下一次poll()方法的耗时。考虑Group Coordinator是否有在 max.poll.interval.ms 内收到下一个poll()方法的请求:

  1. 若收到,则重置等待处理完成时间,重新计算等待处理完成时间直到等待时长到达 max.poll.interval.ms
  2. 若没有收到,则Group Coordinator移除Consumer触发一次Rebalance。

Rebalance的流程是怎样的?

考虑一个Consumer A在正常消费时,同Group有新的Consumer B加入。则:

  1. Consumer B向Group Coordinator发送JoinGroup请求,Group Coordinator准备开启一次Rebalance。
  2. Group Coordinator会在开启Rebalance之后,会通过给Consumer A的HeartBeat响应,告知Consumer A即将开启Rebalance。
  3. Consumer A收到要开启Rebalance的HeartBeat Response后,还可以在 max.poll.interval.ms 时长内处理完poll获取的消息。然后向Group Coordinator发送自己的JoinGroup请求。
  4. Group Coordinator会等待全部的Consumer发送JoinGroupRequest,Group Coordinator会根据 JoinGroup Request 中的 session.timeout.ms 和 rebalance_timeout_ms( max.poll.interval.ms)来做接受Join请求的等待。
  5. Group Coordinate会根据所有收到的JoinGroup请求,来确认Group中可用的Consumer,Group Leader和Partition的分配策略,将这些信息通过JoinGroupResponse返回给Consumer Leader
  6. Consumer A与Consumer B收到JoinGroupResponse之后,非Group Leader收到的Response为空,不作处理,等待后续流程;Group Leader收到的Response会包含所有Consumer与Partition分片策略,Group Leader会根据这些信息计算Partition分配结果。
  7. 接下来所有Consumer会进入 Synchronizing Group State 阶段。所有Consumer会向 Group Coordinator 发送 SyncGroupRequest请求,其中,非Group Leader发送的Request内包含信息为空,Group Leader发送的Request包含自己计算的Partition分配结果。
  8. Group Coordinator会将收到的来自Group Leader的Request里的Partition分配结果包装为 SyncGroupResponse 返回给所有Consumer。
  9. 所有的Consumer收到 SyncGroupResposne后,便可明确自己分配到的Partition,至此Rebalance完成。
  10. Consumer与Group Coordinator通过poll()与HeartBeat交互,等待下一次的Rebalance。

Rebalance会有哪些问题?

  1. 重复消息。Rebalance后,因为Consumer重新分配Partition,Kafka会重新投递消息,这会导致部分消息会被重复消费。
  2. Stop the world。Rebalance期间,Group内的Consumer是停止消费的,因此不合预期的Rebalance会导致Group内的消费停止,这会影响消费效率。
  3. Rebalance Storm。因为Group Coordinator在完成Rebalance时,会等待 max.poll.interval.ms,如果这时某个Consumer在处理poll()的批消息时,超过了这个时间,那么当Rebalance完成后,这个Consumer再次poll(),就又会触发一次Rebalance。那么如果这种超时响应是由于某短时间网络固定的延迟波动,那么就会导致频繁的rejoin,进而频繁的Rebalance,从而产生Rebalance Storm。

如何降低Rebalance的负面影响?

  1. 设置合理的参数。设置 session.timeout.ms 给HeartBeat维活一定的兼容性;关注 max.poll.interval.ms 与自身Consumer消费消息的处理时长,及时调整参数或者优化Consumer消费逻辑;
  2. 保证消息幂等,或者合理的消息滤重。Rebalance不可完全避免(正常的服务发布更新时的Consumer上下线,或者偶发的Consumer实例故障而触发Kafka故障转移),需要考虑将重复消息的影响降低;
  3. 考虑 Static Membership。该机制可以通过设置 Consumer的 group.instance.id 来标识Consumer为 static member。而后的Rebalance时,会将原来的Partition分配给原来的Consumer。而且 Static Membership限制了Rebalance的触发情况,会大大降低Rebalance触发的概率
  4. 升级Kafka版本,kafka2.4支持 Incremental Cooperative Rebalance,该Rebalance协议尝试将全局的Rebalance分解为多次小的Rebalance,降低Stop the world的影响。

写在后面

还需要优化:

  • 补充RocketMQ的Rebalance机制
  • 文章内容的再版与整理
  • 补充部分文字描述的图示
  • 补充Rebalance机制状态机流转说明
  • 补充实际场景中使用的MQ Rebalance是否存在其他优化思路
  • 补充Static Membership与 Incremental Cooperative Rebalance在实际MQ组件中的使用场景。

参考资料