1.简介
kafka提供了消费者客户端参数partiton.assignment.strategy
来设置消费者与订阅主题之间的分区分配策略。默认此参数为org.apache.kafka.clients.consumer.RangeAssignor
,即采用RangeAssignor
分配策略。此外,kafka还提供RoundRobinAssignor
和StickyAssignor
这两种分配策略。分区分配策略可配置多个,彼此之间使用符号分隔。
1.RangeAssignor分配策略
-
原理
按照消费者总数
和分区总数
进行整除
运算来获得一个跨度
,然后将分区按照跨度进行平均分配
,以保证分区尽可能均匀地分配给所有的消费者。每一个主题RangeAssignor策略会将消费组内所有订阅这个主题的消费者按照名称的字典排序,然后为每个消费者划分固定的分区范围,如果不能平均分配,那么字典序靠近的消费者会被多分配一个分区。假设
n
=分区数/
消费者数量, m=分区数%
消费者数量,那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区。 -
实例
均匀分配消费组: 内有2个消费者c0和c1
主题: t0和t1,每个主题有4个分区p0、p1、p2、p3
订阅: c0和c1都订阅t0和t1
分配结果:- c0:t0p0、 t0p1、 t1p0、 t1p1
- c1:t0p2、 t0p3、 t1p2、 t1p3
不均匀分配
消费组: 内有2个消费者c0和c1
主题: t0和t1,每个主题有三个分区p0、p1、p2
订阅: c0和c1都订阅t0和t1
分配结果:- c0:t0p0、 t0p1、 t1p0、 t1p1
- c1:t0p2、 t0p2
很明显得可以看出这样的分配不均匀,如果将类似的情形扩大,则可能出现部分消费者过载的情况。
2.RoundRobinAssignor分配策略
partiton.assignment.strategy
参数值为:org.apache.kafka.clients.consumer.RoundRobinAssignor
。
-
原理
将消费组内所有消费者及消费者订阅的所有主题的分区按照字典排序,然后通过轮询方式逐个将分区依次分配给每个消费者。 -
实例
如果同一个消费组内所有的消费者的订阅信息都是相同的,那么RoundRobinAssignor分配策略的分区分配会是均匀的。消费组: c0和c1
主题: t0和t1,每个主题有三个分区p0、p1、p2
订阅: c0和c1都订阅t0和t1
分配结果:- c0:t0p0、 t0p2、 t1p1
- c1:t0p1、 t1p0、 t1p2
如果同一个消费组内所有的消费者的订阅信息是不相同的,那么执行分区分配的时候就不是完全的轮询分配,有可能导致分区分配的不均匀。如果某个消费者没有订阅消费组内的某个主题,那么在分配分区的时候此消费者将分配不到这个主题的任何分区。
消费组: c0、c1和c2
主题: t0、t1、t2,每个主题有三个分区p0、p1、p2
订阅: c0订阅t0,c1订阅t0、t1,c2订阅t0、t1、t2
分配结果:- c0:t0p0
- c1:t1p0
- c2:t1p1,t2p0,t2p1,t2p2
3.StickyAssignor分配策略
-
目的
- 分区的分配要尽可能的均匀
- 分区的分配尽可能与上次分配的保持相同。
如果两者发生冲突,
第一个目标优先
-
实例
消费组: c0、c1、c2
主题: t0、t1、t2、t3,每个主题有两个分区p0、p1
订阅: c0和c1都订阅t0和t1
分配结果:- c0:t0p0、 t1p1、t3p0
- c1:t0p1、 t2p0、 t3p1
- c2:t1p0、t2p1
4.分区分配策略比较
4.1.消费者订阅信息相同
基于1.3的实例(下面相同) 如果消费者c1脱离了消费组,那么消费者就会执行在均衡操作,进而消费分区会重新分配。
-
如RoundRobinAssignor分配策略会按照消费者c0和c2进行重新
轮询
分配:- c0:t0p0、t1p0、t2p0、t3p0
- c2:t0p1、t1p1、t2p1、t3p1
-
如StickyAssignor分配策略让分配策略具备一定的“粘性”,尽可能地让
前后两次分配相同
,尽可能减少系统资源的损耗及其他异常情况
的发生:- c0:t0p0、t1p1、t3p0、t2p0
- c2:t1p0、t2p1、t0p1、t3p1
4.2.消费者订阅信息不同
-
实例
消费组: c0、c1、c2
主题: t0(一个分区)、t1(两个分区)、t2(三个分区)
订阅: c0订阅t0,c1订阅t0、t1,c2,订阅t0、t1、t2RoundRobinAssignor分配策略分配结果:
- c0:t0p0
- c1:t1p0
- c2:t1p1、t2p0、t2p1、t2p2
StickyAssignor分配策略分配结果:
- c0:t0p0
- c1:t1p0、t1p1
- c2:t2p0、t2p1、t2p2
从结果上看,StickyAssignor分配策略比另外两者分配策略而言更加优异。
5.自定义分区分配策略
自定义分区分配策略必须实现org.apache.kafka.clients.consumer.internals.PartitionAssignor
接口。
PartitionAssignor接口定义:
// 设置消费者自身相关的subscription信息
Subscription subscription(Set<String> topics);
// 提供分区分配策略的名称,命名不能重复
String name();
// 分区分配方案实现,metadata参数表示集群的元数据信息,subscriptions表示消费组内各个消费者成员的订阅信息,
// 最终返回各个消费者的分配信息
Map<String, Assignment> assign(Cluster metadata, Map<String, Subscription> subscriptions);
// 在每个消费者收到消费者组leader分配结果时的回调函数
void onAssignment(Assignment assignment);
// 表示消费者的订阅信息
class Subscription {
private final List<String> topics; // 消费者订阅的主题列表
private final ByteBuffer userData; // 用户自定义信息
// ...
}
// 表示分配结果信息
class Assignment {
private final List<TopicPartition> partitions; // 分配到的分区集合
private final ByteBuffer userdata; // 用户自定义信息
// ...
}
kafka还提供了一个抽象类org.apache.kafka.clients.consumer.internals.AbstractPatitionAssignor
可以简化实现PartitonAssignor接口的工作,并对assign()方法进行了详细的实现,其中会将Subscription中的userData信息去掉
后在进行分配。kafka提供的三种分配策略都继承了这个抽象类。
扩展:
kafka默认一个分区只能被同一个消费组内的一个消费者消费。这个设定我们可以使用自定义分区分配策略改变,实现一个分区可以给多个消费者消费。
组内广播实现: 同一消费组内的任意消费者都可以消费订阅主题的所有分区
public class BroadcastAssignor extends AbstractPartitionAssignor {
@Override
public String name() {
return "broadcast";
}
private Map<String, List<String>> consumersPerTopic(Map<String, Subscriptin> consumerMetadata) {
// 具体实现参考RandomAssignor中的consumersPerTopic() 方法
}
@Override
public Map<String, TopicPartition> assign(Map<String, Integer> partitionsPerTopics, Map<String, Subscription> subscriptions) {
Map<String, List<String>> consumersPerTopic = consumersPerTopic(subscriptions);
Map<String, List<TopicPartition>> assignment = new HashMap<>();
subscriptions.keySet().forEach(memberId -> assignment.put(memberId, new ArrayList<>()));
// 针对每个主题,为每个订阅的消费者分配所有的分区
consumersPerTopic。entrySet().forEach(topicEntry -> {
String topic = topicEntry.getKey;
List<String> members = topicEntry.getValue();
Integer numPartitionForTopic = partitionsPerTopic.get(topic);
if (numPartitionForTopic == null || members.isEmpty()) return;
List<TopicPartition> partitions = AbstractPartitionAssignor.partitons(topic, numPartitionForTopic);
if (!partitions.isEmpty()) {
members.forEach(memberId -> assgnment.get(memberId).addAll(partitions));
}
});
return assignment;
}
}
组内广播的问题:默认的offset的提交会失败。所有的消费者都会提交它自身的offset到
_consumer_offsets
中,后提交的offset会覆盖前面提交的offset。如果要真正实现组内广播,则需要自己保存每个消费者的offset
(可以通过将offset保存到本地文件
或数据库
等方法来实现组内广播的offset)