Kafka学习-----Kafka消费者Consumer:消费方式,分区分配策略,Range&RoundRobin

一.消费方式

consumer 采用 pull(拉)模式从 broker 中读取数据。

push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。 它的目标是尽可能以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息,


二.消费者的分配模式

一个 consumer group 中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及 到 partition 的分配问题,即确定那个 partition 由哪个 consumer 来消费。
Kafka 有两种分配策略,一是 RoundRobin,一是 Range。

在这里插入图片描述

1.分配时机?

在 Kafka 内部存在两种默认的分区分配策略:Range 和 RoundRobin。当以下事件发生时,Kafka 将会进行一次分区分配:

  1. 同一个 Consumer Group 内新增消费者
  2. 消费者离开当前所属的Consumer Group,包括shuts down 或 crashes
  3. 订阅的主题新增分区

将分区的所有权从一个消费者移到另一个消费者称为重新平衡(rebalance),如何rebalance就涉及到下面提到的分区分配策略。下面我们将详细介绍 Kafka 内置的两种分区分配策略。本文假设我们有个名为 T1 的主题,其包含了10个分区,然后我们有两个消费者(C1,C2)来消费这10个分区里面的数据。


2.Range策略

Range策略是对每个主题而言的,首先对同一个主题里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。在我们的例子里面,排完序的分区将会是0, 1, 2, 3, 4, 5, 6, 7, 8, 9;消费者排完序将会是C1, C2。然后将partitions的个数除于消费者的总数来决定每个消费者消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。


几个例子:

  • 情况1:有10个分区,2个消费者, 10 / 2 = 5,那么消费者 C1和消费者C2 将会消费同样多的分区,所以最后分区分配的结果看起来是这样的:

C1 将消费 0, 1, 2, 3, 4 分区

C2 将消费 5, 6, 7, 8, 9 分区

  • 情况2:有11个分区,那么最后分区分配的结果看起来是这样的:

C1 将消费 0, 1, 2, 3, 4, 5 分区

C2 将消费 6, 7, 8, 9, 10分区

  • 情况3:有2个主题(T1和T2),分别有11个分区(0,1,2,…10),那么最后分区分配的结果看起来是这样的:

C1 将消费 T1主题的 0, 1, 2, 3, 4, 5 分区以及 T2主题的 0, 1, 2, 3, 4, 5分区,加起来一共

C2 将消费 T1主题的 6, 7, 8, 9, 10 分区以及 T2主题的 5, 6, 7, 8, 9, 10分区


可以看出,C1 消费者比C2消费者多消费了2个分区,这就是Range strategy的一个很明显的弊端。


2.RoundRobin 策略

使用RoundRobin策略有两个前提条件必须满足:

  1. 同一个Consumer Group里面的所有消费者的num.streams必须相等;
  2. 每个消费者订阅的主题必须相同。

例子:

假如按照 hashCode 排序完的topic-partitions组依次为T1-5, T1-3, T1-0, T1-8, T1-2, T1-1, T1-4, T1-7, T1-6, T1-9,我们的消费者线程排序为C1-0, C1-1, C2-0, C2-1,最后分区分配的结果为:

C1-0 将消费 T1-5, T1-2, T1-6 分区;

C1-1 将消费 T1-3, T1-1, T1-9 分区;

C2-0 将消费 T1-0, T1-4 分区;

C2-1 将消费 T1-8, T1-7 分区;

在这里插入图片描述


三.代码解释

RangeAssignor:
  • 该策略会把主题的若干个连续的分区分配给消费者(kafka默认用该策略)。

假设消费者 C1 和消费者 C2 同时 订阅了主题 T1 和主题 T2,并且每个主题有 3 个分区。那么消费者 C1 有可能分配到这 两个主题的分区 0 和分区 1,而消费者 C2 分配到这两个主题的分区 2。因为每个主题 拥有奇数个分区,而分配是在主题内独立完成的,第一个消费者最后分配到比第二个消 费者更多的分区。只要使用了 RangeAssignor 策略,而且分区数量无法被消费者数量整除,就会 出现这种情况。

        public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsPerTopic,
                                                        Map<String, List<String>> subscriptions) {

            Map<String, List<String>> consumersPerTopic = consumersPerTopic(subscriptions);
            Map<String, List<TopicPartition>> assignment = new HashMap<>();//key为consumerID,value为分配给该consumer的TopicPartition
            for (String memberId : subscriptions.keySet())//对于每一个consumer
                assignment.put(memberId, new ArrayList<TopicPartition>());//初始化

            //对于每一个topic,进行分配
            for (Map.Entry<String, List<String>> topicEntry : consumersPerTopic.entrySet()) {
                String topic = topicEntry.getKey();
                List<String> consumersForTopic = topicEntry.getValue();

                //这个topic的partition数量
                Integer numPartitionsForTopic = partitionsPerTopic.get(topic);
                if (numPartitionsForTopic == null)//partition数量为null,直接跳过,忽略
                    continue;

                Collections.sort(consumersForTopic);//对consumer进行排序

                //计算每个consumer分到的partition数量
                int numPartitionsPerConsumer = numPartitionsForTopic / consumersForTopic.size();
                //计算平均以后剩余partition数量
                int consumersWithExtraPartition = numPartitionsForTopic % consumersForTopic.size();

                //从0开始作为Partition Index, 构造TopicPartition对象
                List<TopicPartition> partitions = AbstractPartitionAssignor.partitions(topic, numPartitionsForTopic);
                for (int i = 0, n = consumersForTopic.size(); i < n; i++) {//对于当前这个topic的每一个consumer
                    //一定是前面几个consumer会被分配一个额外的TopicPartitiion
                    int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition);
                    int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1);
                    assignment.get(consumersForTopic.get(i)).addAll(partitions.subList(start, start + length));
                }
            }
            return assignment;
        }

RoundRobinAssignor

该策略把主题的所有分区逐个分配给消费者。如果使用 RoundRobinAssignor 策略来给消费者 C1 和消费者 C2 分配分区,那么消费者 C1 将分到主题 T1 的分区 0 和分区 2 以及主题 T2 的分区 1,消费者 C2 将分配到主题 T1 的分区 1 以及主题 T2 的分区 0 和分区 2。一般来说,如果所有消费者都订阅相同的主题(这种情况很常见),RoundRobin 策略会给所有消费者分配相同数量的分区(或最多就差一个分区) 。


     @Override
        public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsPerTopic,
                                                        Map<String, List<String>> subscriptions) {
            Map<String, List<TopicPartition>> assignment = new HashMap<>();
            for (String memberId : subscriptions.keySet())
                assignment.put(memberId, new ArrayList<TopicPartition>());//初始化分配规则

            CircularIterator<String> assigner = new CircularIterator<>(Utils.sorted(subscriptions.keySet()));//assigner存放了所有的consumer
            for (TopicPartition partition : allPartitionsSorted(partitionsPerTopic, subscriptions)) {//所有的consumer订阅的所有的TopicPartition的List
                final String topic = partition.topic();
                while (!subscriptions.get(assigner.peek()).contains(topic))// 如果当前这个assigner(consumer)没有订阅这个topic,直接跳过
                    assigner.next();
                //跳出循环,表示终于找到了订阅过这个TopicPartition对应的topic的assigner
                //将这个partition分派给对应的assigner
                assignment.get(assigner.next()).add(partition);
            }
            return assignment;
        }

RoundRobinAssignor与RangeAssignor最大的区别,是进行分区分配的时候不再逐个topic进行,即不是为某个topic完成了分区分派以后,再进行下一个topic的分区分派, 而是首先将这个group中的所有consumer订阅的所有的topic-partition按顺序展开,然后,依次对于每一个topic-partition,在consumer进行round robin,为这个topic-partition选择一个consumer。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值