消费者票选分区分配策略实现
消费者的分区分配策略是由 PartitionAssignor 接口定义,主要有以下三个实现类:
RangeAssignor (默认):range
RoundRobinAssignor:roundrobin
StickyAssignor:sticky
不清楚的同学可以参考:三种分区分配策略
若上述三种分配方式不满足需求,我们也可以自定义实现PartitionAssignor接口。
若自定义分配时只与partitions相关,与用户数据userData无关(参考Assignment类),也可直接继承AbstractPartitionAssignor实现
怎么配置PartitionAssignor ?
Properties props = new Properties(); assignors.add(RangeAssignor.class.getCanonicalName()); assignors.add(RoundRobinAssignor.class.getCanonicalName()); assignors.add(StickyAssignor.class.getCanonicalName());
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, assignors);
在消费者客户端创建KafkaConsumer的属性配置的时候指定,并且可以指定多个PartitionAssignor
分区分配策略由谁确定?多个PartitionAssignor 到底哪个生效?
分区分配策略由服务端确定
一个消费组可以有多个消费者,每个消费者都可以指定一个或者多个PartitionAssignor(分区分配策略),不指定默认是RangeAssignor
可以简单回顾一下,消费者加入消费组流程可以大致分为三步:
(1)查找GroupCoordinator阶段:找一个负载最低的broker发起GroupCoordinatorRequest请求,然后阻塞直到收到GroupCoordinatorResponse响应,来确定这个消费者应该连接哪个broker的GroupCoordinator
(2)join Group阶段:消费者向GroupCoordinator发送加入组的JoinGroupRequest请求,服务端收集消费者信息产生JoinGroupResponse响应。这个过程会确定一个leader消费者,选择一个PartitionAssignor
(3)sync Group阶段:消费者进行join响应回调,leader消费者会在本地进行分区(分区策略在join阶段由服务端指定),然后发起sync请求到服务端将分区分配信息带过去。其他follow消费者也会发起sync请求,sync响应返回分区分配信息。
服务端选择PartitionAssignor 流程发生在上述join阶段,关键调用链如下:
- GroupCoordinator.onCompleteJoin
-- GroupMetadata.initNextGeneration
---- GroupMetadata.selectProtocol
def selectProtocol: String = { if (members.isEmpty) throw new IllegalStateException("Cannot select protocol for empty group") // select the protocol for this group which is supported by all members val candidates = candidateProtocols // let each member vote for one of the protocols and choose the one with the most votes val votes: List[(String, Int)] = allMemberMetadata .map(_.vote(candidates)) .groupBy(identity) .mapValues(_.size) .toList votes.maxBy(_._2)._1 }
selectProtocol方法主要有两步:
第一步是通过candidateProtocols方法找到分区分配策略候选项, 前面说过消费组中的每个消费者都可以设置自己的分区分配策略,这里的候选项是所有消费者共同支持的分区分配策略。
第二步是每个消费者从候选项中投票,找到票选最大值,选为当前消费者组的分区分配策略。
找候选集:GroupMetadata.candidateProtocols
private def candidateProtocols = { // get the set of protocols that are commonly supported by all members allMemberMetadata .map(_.protocols) .reduceLeft((commonProtocols, protocols) => commonProtocols & protocols) }
所有消费者成员依次左规约找到共同的分区分配策略得到候选集candidates
所有消费者成员从候选集票选:MemberMetadata.vote
def vote(candidates: Set[String]): String = { supportedProtocols.find({ case (protocol, _) => candidates.contains(protocol)}) match { case Some((protocol, _)) => protocol case None => throw new IllegalArgumentException("Member does not support any of the candidate protocols") } }
MemberMetadata管理连接过来的消费者元数据,包括心跳数据和支持的protocols(对应前面所说的分区分配策略)
find会依次迭代消费者支持的protocols集合里的数据,找到的第一个在候选集中的protocol就返回,相当于投票成功
再回到selectProtocol的votes得到的是一个List[(String, Int)],String表示protocol,Int表示票数。
votes.maxBy(_._2)._1 找到票数最大的protocol返回。也就是前面说的分区分配策略。
举个例子:
有一个消费者组 group-test,有三个消费者c1、c2、c3,支持的分区分配策略列表分别如下
c1:{ range, roundrobin, custom }
c2:{ range, roundrobin, sticky }
c3:{ roundrobin, range, sticky }
第一步选择候选集:候选集是c1、c2、c3的交集 { range,roundrobin }
第二步投票:c1投range、c2投range、c3投roundrobin,选票结果为{(range, 2), (roundrobin, 1)}
最后消费组group-test选择的分区分配策略为range
文中源代码来自kafka 1.0.0版本