在Kafka中,当有新消费者加入或者订阅的topic数发生变化时,会触发Rebalance(再均衡:在同一个消费者组当中,分区的所有权从一个消费者转移到另外一个消费者)机制,Rebalance顾名思义就是重新均衡消费者消费。Rebalance的过程如下:

第一步:所有成员都向coordinator发送请求,请求入组。一旦所有成员都发送了请求,coordinator会从中选择一个consumer担任leader的角色,并把组成员信息以及订阅信息发给leader。

第二步:leader开始分配消费方案,指明具体哪个consumer负责消费哪些topic的哪些partition。一旦完成分配,leader会将这个方案发给coordinator。coordinator接收到分配方案之后会把方案发给各个consumer,这样组内的所有成员就都知道自己应该消费哪些分区了。

所以对于Rebalance来说,Coordinator起着至关重要的作用,那么怎么查看消费者对应的Coordinator呢,我们知道某个消费者组对应__consumer_offsets中的哪个Partation是通过hash计算出来的:partation=hash("test_group_1")%50=28,表示test_group_1这个消费者组属于28号partation,通过命令:

./kafka-topics.sh --zookeeper 192.168.33.11: --describe --topic __consumer_offsets

可以找到28号Partation所对应的信息:

从而可以知道coordinator对应的broker为1

在Rebalance期间,消费者会出现无法读取消息,造成整个消费者群组一段时间内不可用,假设现在消费者组当中有A,代码逻辑执行10s,如果消费者组在消费的过程中consumer B加入到了该消费者组,并且B的代码逻辑执行20s,那么当A处理完后先进入Rebalance状态等待,只有当B也处理完后,A和B才真正通过Rebalance重新分配,这样显然A在等待的过程中浪费了资源。

消费者A:

 """
consumer_rebalance_a.py a消费者
"""
import pickle
import uuid
import time
from kafka import KafkaConsumer
from kafka.structs import TopicPartition, OffsetAndMetadata
from kafka import ConsumerRebalanceListener consumer = KafkaConsumer(
bootstrap_servers=['192.168.33.11:9092'],
group_id="test_group_1",
client_id="{}".format(str(uuid.uuid4())),
enable_auto_commit=False,
key_deserializer=lambda k: pickle.loads(k),
value_deserializer=lambda v: pickle.loads(v)
) # 用来记录最新的偏移量信息.
consumer_offsets = {} class MineConsumerRebalanceListener(ConsumerRebalanceListener):
def on_partitions_revoked(self, revoked):
"""
再均衡开始之前 下一轮poll之前触发
:param revoked:
:return:
"""
print('再均衡开始之前被自动触发.')
print(revoked, type(revoked))
consumer.commit_async(offsets=consumer_offsets) def on_partitions_assigned(self, assigned):
"""
再均衡完成之后 即将下一轮poll之前 触发
:param assigned:
:return:
"""
print('在均衡完成之后自动触发.')
print(assigned, type(assigned)) consumer.subscribe(topics=('round_topic',), listener=MineConsumerRebalanceListener()) def _on_send_response(*args, **kwargs):
"""
提交偏移量涉及回调函数
:param args:
:param kwargs:
:return:
"""
if isinstance(args[1], Exception):
print('偏移量提交异常. {}'.format(args[1]))
else:
print('偏移量提交成功') try:
start_time = time.time()
while True:
# 再均衡其实是在poll之前完成的
consumer_records_dict = consumer.poll(timeout_ms=100) # 处理逻辑.
for k, record_list in consumer_records_dict.items():
for record in record_list:
print("topic = {},partition = {},offset = {},key = {},value = {}".format(
record.topic, record.partition, record.offset, record.key, record.value)
) consumer_offsets[
TopicPartition(record.topic, record.partition)
] = OffsetAndMetadata(
record.offset + 1, metadata='偏移量.'
) try:
consumer.commit_async(callback=_on_send_response)
time.sleep(10)
except Exception as e:
print('commit failed', str(e)) except Exception as e:
print(str(e))
finally:
try:
# 同步提交偏移量,在消费者异常退出的时候再次提交偏移量,确保偏移量的提交.
consumer.commit()
print("同步补救提交成功")
except Exception as e:
consumer.close()

消费者B:

 """
consumer b.py 消费者B
""" import pickle
import uuid
import time
from kafka import KafkaConsumer
from kafka.structs import TopicPartition, OffsetAndMetadata
from kafka import ConsumerRebalanceListener consumer = KafkaConsumer(
bootstrap_servers=['192.168.33.11:9092'],
group_id="test_group_1",
client_id="{}".format(str(uuid.uuid4())),
enable_auto_commit=False, # 设置为手动提交偏移量.
key_deserializer=lambda k: pickle.loads(k),
value_deserializer=lambda v: pickle.loads(v)
) consumer_offsets = {} # 用来记录最新的偏移量信息. class MineConsumerRebalanceListener(ConsumerRebalanceListener):
def on_partitions_revoked(self, revoked):
"""
再均衡开始之前 下一轮poll之前触发
:param revoked:
:return:
"""
print('再均衡开始之前被自动触发.')
print(revoked, type(revoked))
consumer.commit_async(offsets=consumer_offsets) def on_partitions_assigned(self, assigned):
"""
再均衡完成之后 即将下一轮poll之前 触发
:param assigned:
:return:
""" print('在均衡完成之后自动触发.')
print(assigned, type(assigned)) consumer.subscribe(topics=('round_topic',), listener=MineConsumerRebalanceListener()) def _on_send_response(*args, **kwargs):
"""
提交偏移量涉及回调函数
:param args:
:param kwargs:
:return:
""" if isinstance(args[1], Exception):
print('偏移量提交异常. {}'.format(args[1]))
else:
print('偏移量提交成功') try:
start_time = time.time()
while True:
# 再均衡其实是在poll之前完成的
consumer_records_dict = consumer.poll(timeout_ms=100) record_num = 0
for key, record_list in consumer_records_dict.items():
for record in record_list:
record_num += 1
print("---->当前批次获取到的消息个数是:{}".format(record_num)) # 处理逻辑.
for k, record_list in consumer_records_dict.items():
for record in record_list:
print("topic = {},partition = {},offset = {},key = {},value = {}".format(
record.topic, record.partition, record.offset, record.key, record.value)
) consumer_offsets[
TopicPartition(record.topic, record.partition)
] = OffsetAndMetadata(record.offset + 1, metadata='偏移量.') try:
# 轮询一个batch 手动提交一次
consumer.commit_async(callback=_on_send_response)
time.sleep(20)
except Exception as e:
print('commit failed', str(e)) except Exception as e:
print(str(e))
finally:
try:
# 同步提交偏移量,在消费者异常退出的时候再次提交偏移量,确保偏移量的提交.
consumer.commit()
print("同步补救提交成功")
except Exception as e:
consumer.close()

消费者A和消费者B是同一个消费者组(test_group_1)的两个消费者,用time.sleep的方式模拟执行时间,A:10s,B:20s;首先A开始消费,当B新加入消费者组的时候会触发Rebalance,可以通过实现再均衡监听器(RebalanceListener)中的on_partitions_revoked和on_partitions_assigned方法来查看再均衡触发前后的partition变化情况,依次启动消费者A和B之后:

消费者A:
再均衡开始之前被自动触发.
{TopicPartition(topic='round_topic', partition=0), TopicPartition(topic='round_topic', partition=1), TopicPartition(topic='round_topic', partition=2)} <class 'set'>
<----------------------------------------
---------------------------------------->
在均衡完成之后自动触发.
{TopicPartition(topic='round_topic', partition=0), TopicPartition(topic='round_topic', partition=1)} <class 'set'>
<---------------------------------------- 消费者B:
再均衡开始之前被自动触发.
set() <class 'set'>
<----------------------------------------
---------------------------------------->
在均衡完成之后自动触发.
{TopicPartition(topic='round_topic', partition=2)} <class 'set'>
<----------------------------------------

在等待B的逻辑执行完后,A和B进入再均衡状态;再均衡前A处于partition 0、1、 2三个分区,B不占有任何partition;当再均衡结束后,A占有partition 0、1,B占有partition 2;然后A和B分别开始消费对应的partition。

在上述消费者A和B的代码中重写了RebalanceListener,主要是为了在发生再均衡之前提交最后一个已经处理记录的偏移量,因为再均衡时消费者将失去对一个分区的所有权,如果消费者已经消费了当前partition还没提交offset,这时候发生再均衡会使得消费者重新分配partition,可能使得同一个消息先后被两个消费者消费的情况,实现MineConsumerRebalanceListener再均衡前提交一次offset,确保每一个消费者在触发再均衡前提交最后一次offset:

 class MineConsumerRebalanceListener(ConsumerRebalanceListener):
def on_partitions_revoked(self, revoked):
"""
再均衡开始之前 下一轮poll之前触发
:param revoked:
:return:
"""
print('再均衡开始之前被自动触发.')
print(revoked, type(revoked))
consumer.commit_async(offsets=consumer_offsets) def on_partitions_assigned(self, assigned):
"""
再均衡完成之后 即将下一轮poll之前 触发
:param assigned:
:return:
""" print('在均衡完成之后自动触发.')
print(assigned, type(assigned))

再均衡发生的场景有以下几种:

1. 组成员发生变更(新consumer加入组、已有consumer主动离开组或已有consumer崩溃了)
2. 订阅主题数发生变更,如果你使用了正则表达式的方式进行订阅,那么新建匹配正则表达式的topic就会触发rebalance
3. 订阅主题的分区数发生变更
鉴于触发再均衡后会造成资源浪费的问题,所以我们尽量不要触发再均衡

Kafka消费者组再均衡问题的更多相关文章

  1. 详细解析kafka之 kafka消费者组与重平衡机制

    消费组组(Consumer group)可以说是kafka很有亮点的一个设计.传统的消息引擎处理模型主要有两种,队列模型,和发布-订阅模型. 队列模型:早期消息处理引擎就是按照队列模型设计的,所谓队列 ...

  2. Kafka消费者组静态成员(static consumer member)

    Kafka 2.3发布后官网的Consumer参数中增加了一个新的参数:group.instance.id.下面是这个参数的解释: A unique identifier of the consume ...

  3. Kafka消费者-从Kafka读取数据

    (1)Customer和Customer Group (1)两种常用的消息模型 队列模型(queuing)和发布-订阅模型(publish-subscribe). 队列的处理方式是一组消费者从服务器读 ...

  4. kafka消费者客户端

    Kafka消费者 1.1 消费者与消费者组 消费者与消费者组之间的关系 ​ 每一个消费者都隶属于某一个消费者组,一个消费者组可以包含一个或多个消费者,每一条消息只会被消费者组中的某一个消费者所消费.不 ...

  5. 带你涨姿势的认识一下 Kafka 消费者

    之前我们介绍过了 Kafka 整体架构,Kafka 生产者,Kafka 生产的消息最终流向哪里呢?当然是需要消费了,要不只产生一系列数据没有任何作用啊,如果把 Kafka 比作餐厅的话,那么生产者就是 ...

  6. 5.Kafka消费者-从Kafka读取数据(转)

    http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/14/kafka-consumer.ht ...

  7. Kafka系列3:深入理解Kafka消费者

    上面两篇聊了Kafka概况和Kafka生产者,包含了Kafka的基本概念.设计原理.设计核心以及生产者的核心原理.本篇单独聊聊Kafka的消费者,包括如下内容: 消费者和消费者组 如何创建消费者 如何 ...

  8. 谈一谈 Kafka 的再均衡?

    在Kafka中,当有新消费者加入或者订阅的topic数发生变化时,会触发Rebalance(再均衡:在同一个消费者组当中,分区的所有权从一个消费者转移到另外一个消费者)机制,Rebalance顾名思义 ...

  9. kafka消费组、消费者

    consumer group consumer instance 一个消费组可能有一个或者多个消费者.同一个消费组可以订阅一个或者多个主题.主题的某一个分区只能被消费组的某一个消费者消费.那么分区和消 ...

随机推荐

  1. 程序人生 | 35岁以上的 iOS 程序员都到哪里去了?

    1.网上流传华为公司正在清理 34 岁以上的员工. " 中国区开始集中清理 34 + 的交付员工,...... 去向是跟海外服务部门交换今年新毕业的校招员工,也就是进新人,出旧人. 这些旧人 ...

  2. Java:文件字符流和字节流的输入和输出

    最近在学习Java,所以就总结一篇文件字节流和字符流的输入和输出. 总的来说,IO流分类如下: 输入输出方向:     输入流(从外设读取到内存)和输出流(从内存输出到外设) 数据的操作方式: 字节流 ...

  3. Linux Centos6.9下安装部署VNC的实操详述

    VNC (Virtual Network Console)是虚拟网络控制台的缩写.它 是一款优秀的远程控制工具软件,由著名的AT&T的欧洲研究实验室开发的.VNC 是在基于 UNIX和 Lin ...

  4. freetype之PC机体验

    目录 freetype之PC机体验 引入 中文教程 官方教程 代码结构 字体概念 PC上安装 官方例子 宽字符保存显示中文 坐标框架体系 字符坐标信息获取 title: freetype之PC机体验 ...

  5. 同一个tomcat部署多个项目导致启动失败

    内容描述在同一个tomcat部署多个打包成war包的项目导致启动失败,报错如下: 报错信息Error starting ApplicationContext. To display the condi ...

  6. 洛谷红名+AC150祭

    emmmm没什么想说的,随便放个图吧23333(逃~

  7. 电梯系列——OO Unit2分析和总结

    一.摘要 本文是BUAA OO课程Unit2在课程讲授.三次作业完成.自测和互测时发现的问题,以及倾听别人的思路分享所引起个人的一些思考的总结性博客.主要包含设计策略.代码度量.BUG测试和心得体会等 ...

  8. table-tree 表格树、树形数据处理、数据转树形数据

    前言 公司想搞个表格树的展示页面,看着element有个表格树,还以为可以用. 用出来只用表格没有树,研究半天没研究个所以然,只能从新找个 npm里找到一个:vue-table-with-tree-g ...

  9. MVC加载分布页的三种方式

               第一种: @Html.Partial("_分部页")            第二种: @{ Html.RenderPartial("分部页" ...

  10. vue.js学习系列-第一篇

    VUE系列一 简介    vue是一个兴起的前端js库,是一个精简的MVVM.从技术角度讲,Vue.js专注于 MVVM 模型的 ViewModel 层.它通过双向数据绑定把 View 层和 Mode ...