kafka学习笔记(四、生产者(客户端)深入研究(一)——分区分配策略)

在这里插入图片描述


1.简介

kafka提供了消费者客户端参数partiton.assignment.strategy来设置消费者与订阅主题之间的分区分配策略。默认此参数为org.apache.kafka.clients.consumer.RangeAssignor,即采用RangeAssignor分配策略。此外,kafka还提供RoundRobinAssignorStickyAssignor这两种分配策略。分区分配策略可配置多个,彼此之间使用符号分隔。

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分配策略

  • 目的

    1. 分区的分配要尽可能的均匀
    2. 分区的分配尽可能与上次分配的保持相同。

    如果两者发生冲突,第一个目标优先

  • 实例

    消费组: 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脱离了消费组,那么消费者就会执行在均衡操作,进而消费分区会重新分配。

  1. 如RoundRobinAssignor分配策略会按照消费者c0和c2进行重新轮询分配:

    • c0:t0p0、t1p0、t2p0、t3p0
    • c2:t0p1、t1p1、t2p1、t3p1
  2. 如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、t2

    RoundRobinAssignor分配策略分配结果:

    • 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)

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值