High Level Consumer
-
Consumer Group
-
Rebalance场景剖析
Low Level Consumer
很多时候,客户程序只是希望从Kafka读取数据,不太关心消息offset的处理。同时也希望提供一些语义,例如同一条消息只被某一个Consumer消费(单播)或被所有Consumer消费(广播)。因此,Kafka Hight Level Consumer提供了一个从Kafka消费数据的高层抽象,从而屏蔽掉其中的细节并提供丰富的语义
Consumer Group
1. 什么是消费者组
High Level Consumer将从某个Partition读取的最后一条消息的offset存于Zookeeper或专用Topic中。这个offset基于客户程序提供给Kafka的名字来保存,这个名字被称为Consumer Group。Consumer Group是整个Kafka集群全局的,而非某个Topic的。每一个High Level Consumer实例都属于一个Consumer Group,若不指定则属于默认的Group。组内的所有消费者协调在一起来消费订阅主题的所有分区,每个分区只能由同一个消费组内的一个Consumer来消费
o Consumer Group下可以有一个或多个Consumer instance,Consumer instance可以是一个进程,也可以是一个线程
o group.id是一个字符串,唯一标识一个Consumer Group
o Consumer Group下订阅的Topic下的每个分区只能分配给某个Group下的一个Consumer
2. 消费者位置
消费者在消费的过程中需要记录自己消费了多少数据,即消费位置信息。在Kafka中这个位置信息有个专门的术语:位移(offset)。很多消息引擎都把这部分信息保存在服务器端(Broker端)。这样做的好处当然是实现简单,但会有三个主要的问题:
- Broker从此变成有状态的,会影响伸缩性
- 需要引入应答机制(Acknowledgement)来确认消费成功
- 由于要保存很多Consumer的offset信息,必然引入复杂的数据结构,造成资源浪费
而Kafka选择了不同的方式:每个Consumer Group保存自己的位移信息,那么只需要简单的一个整数表示位置就够了;同时可以引入checkpoint机制定期持久化,简化了应答机制的实现
3. 位移管理
0. 自动VS手动
Kafka默认是定期帮你自动提交位移的(enable.auto.commit = true),你当然可以选择手动提交位移实现自己控制。另外kafka会定期把group消费情况保存起来,做成一个offset map,如下图所示:
上图中表明了test-group这个组当前的消费情况
- 位移提交
老版本的位移是提交到zookeeper中,但是Zookeeper其实并不适合进行大批量的读写操作;因此kafka提供了另一种解决方案:增加__consumer_offsets Topic,将offset信息写入这个Topic,摆脱对Zookeeper的依赖(指保存offset这件事情)。__consumer_offsets中的消息保存了每个Consumer Group某一时刻提交的offset信息。依然以上图中的Consumer Group为例,格式大概如下:
__consumers_offsets Topic配置了Compact策略,使得它总是能够保存最新的位移信息,既控制了该Topic总体的日志容量,也能实现保存最新offset的目的
4. Rebalance
0. 什么是Rebalance?
Rebalance本质上是一种协议,规定了一个Consumer Group下的所有Consumer如何达成一致来分配订阅Topic的每个分区。比如某个Group下有20个Consumer,它订阅了一个具有100个分区的Topic。正常情况下,Kafka平均会为每个consumer分配5个分区。这个分配的过程就叫Rebalance。
-
什么时候Rebalance?
这也是经常被提及的一个问题。Rebalance的触发条件有三种:
组成员发生变更(新Consumer加入组、已有Consumer主动离开组或已有Consumer崩溃了)
订阅主题数发生变更,使用正则表达式的方式进行订阅,那么新建匹配正则表达式的Topic就会触发Rebalance
订阅主题的分区数发生变更 -
如何进行组内分区分配?
Range
Range策略是对每个主题而言的,首先对同一个主题里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。然后将partitions的个数除于消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。
RoundRobin
使用RoundRobin策略有两个前提条件必须满足:1、同一个Consumer Group里面的所有消费者的num.streams必须相等;2、每个消费者订阅的主题必须相同。将所有主题的分区组成TopicAndPartition列表,按分区名Hash排序后平均分配给每一个消费者的线程
自定义可插拔式的分配策略 -
谁来执行Rebalance和Consumer Group管理?
每个Consumer Group都会被分配一个Coordinator用于组管理和位移管理,Consumer Group的第一个Consumer启动的时候,它会去和Kafka Server确定谁是它们组的Coordinator。之后该Group内的所有成员都会和该Coordinator进行协调通信 -
Rebalance Generation
Rebalance一次,更新新一届的Consumer Group成员,Generation + 1 -
协议
Rebalance本质上是一组协议。Group与Coordinator共同使用它来完成Group的Rebalance。目前Kafka提供了5个协议来处理与Consumer Group Coordination相关的问题:
Heartbeat请求:Consumer需要定期给Coordinator发送心跳来表明自己还活着
LeaveGroup请求:主动告诉Coordinator我要离开Consumer Group
SyncGroup请求:Group Leader把分配方案告诉组内所有成员
JoinGroup请求:成员请求加入组
DescribeGroup请求:显示组的所有信息,包括成员信息,协议名称,分配方案,订阅信息等。通常该请求是给管理员使用
Coordinator在Rebalance的时候主要用到了前面4种请求 -
Rebalance过程
Rebalance分为2步:Join和Sync -
Join,顾名思义就是加入组。这一步中,所有成员都向Coordinator发送JoinGroup请求,请求入组。一旦所有成员都发送了JoinGroup请求,Coordinator会从中选择一个Consumer担任Leader的角色,并把组成员信息以及订阅信息发给Leader–注意Leader和Coordinator不是一个概念。Leader负责消费分配方案的制定
-
Sync,这一步Leader开始分配消费方案,即哪个Consumer负责消费哪些Topic的哪些Partition。一旦完成分配,Leader会将这个方案封装进SyncGroup请求中发给Coordinator,非Leader也会发SyncGroup请求,只是内容为空。Coordinator接收到分配方案之后会把方案塞进SyncGroup的Response中发给各个Consumer。这样组内的所有成员就都知道自己应该消费哪些分区了
-
Consumer Group状态机
Group也做了个状态机来表明组状态的流转。coordinator根据这个状态机会对consumer group做不同的处理,如下图所示:
简单说明下图中的各个状态:
Dead:组内已经没有任何成员的最终状态,组的元数据也已经被Coordinator移除了。这种状态响应各种请求都是一个Response: UNKNOWN_MEMBER_ID
Empty:组内无成员,但是位移信息还没有过期。这种状态只能响应JoinGroup请求
PreparingRebalance:组准备开启新的Rebalance,等待成员加入
AwaitingSync:正在等待Leader Consumer将分配方案传给各个成员
Stable:Rebalance完成!可以开始消费了
Rebalance场景剖析
-
新成员加入组
-
组成员崩溃
组成员崩溃和组成员主动离开是两个不同的场景。因为在崩溃时成员并不会主动地告知Coordinator此事,Coordinator有可能需要一个完整的session.timeout周期才能检测到这种崩溃,这必然会造成Consumer的滞后。可以说离开组是主动地发起Rebalance;而崩溃则是被动地发起Rebalance -
组成员主动离组
-
提交位移
Low Level Consumer
- 使用Low Level Consumer (Simple Consumer)的主要原因是,用户希望比Consumer Group更好的控制数据的消费。比如:
o 同一条消息读多次
o 只读取某个Topic的部分Partition
o 管理事务,从而确保每条消息被处理一次,且仅被处理一次 - 与Consumer Group相比,Low Level Consumer要求用户做大量的额外工作:
o 必须在应用程序中跟踪offset,从而确定下一条应该消费哪条消息
o 应用程序需要通过程序获知每个Partition的Leader是谁
o 必须处理Leader的变化 - 使用Low Level Consumer的一般流程如下:
- 查找到一个“活着”的Broker,并且找出每个Partition的Leader
- 找出每个Partition的Follower
- 定义好请求,该请求应该能描述应用程序需要哪些数据
- Fetch数据
- 识别Leader的变化,并对之作出必要的响应