深入理解kafka核心设计与实践原理_读书笔记 详解Kafka中所有的分区分配 [面试重点]

	kafka中涉及到分区分配的概念主要有三个地方:
	
		1.生产者的分区分配:指的是为每条消息指定其所要发往的分区。
		
		2.消费者的分区分配:指的是为其指定可以消费消息的分区。
		
		3.broker的分区分配: 指的是在哪个broker中创建哪些分区的副本。
	
	其中,生产者和消费者的分区分配是面试重点内容。

一、生产者的分区分配

	每一条消息被发送到 broker 之前,会根据分区规则选择存储到哪个具体的分区,

    如果分区规则设定得合理,所有的消息都可以均匀地分配到不同的分区中
     
    消息分配分区的是由分区器作用的。

1.指定了partition字段

    如果消息 ProducerRecord 中指定了partition字段,那么就不需要分区器,partition代表的就是所要发往的分区号。

2.没有指定partition字段

    (1)使用Kafka默认分区器
      
          ProducerRecord 中没有指定partition字段,会使用Kafka默认分区器。

          默认分区器是 orgapche.kafka.clients.producer.intenals.DefaultPartitioner
          
          它实现了 org.apache.kafka.clients.producer.Partitioner接口,接口中定义了2个方法:

          public int partition(String topic , Object key , byte[] keyBytes,
                              Object value , byte[] valueBytes, Cluster cluster);

          public void close();


          其中partition()方法定义了分区分配逻辑

              ①如果key 不为null。对key做哈希(MurmurHash2算法,高运算性能和低配碰撞率),得到的hash值作为分区号

                  return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;

              ②如果key为null。 消息将会以轮询的方式发往主题内的各个可用分区。(可用分区:存在leader副本)

              注意:key不为null时,计算得到的分区号是有所分区中的任意一个

                   key为null时,计算得到的分区号仅为可用分区的中任意一个

	(2)自定义分区器

          使用自定义的分区器,只需同DefaultPartitioner 一样实现Partitioner接口即可。

    (3)总结:

          1.如果指定了如果消息 ProducerRecord 中指定了partition字段,那么就不需要分区器

           2.没有指定partition

               (1)默认分区器

                    ①k如果key 不为null。对key做哈希,得到的hash值作为分区号

                       return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;

                   ②如果key为null。 消息将会以轮询的方式发往主题内的各个可用分区。

                (2)自定义分区器
DefaultPartitioner代码如下:

public class DefaultPartitioner implements Partitioner {

    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();

    /**
     * Compute the partition for the given record.
     *
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes serialized key to partition on (or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes serialized value to partition on or null
     * @param cluster The current cluster metadata
     */
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        /* 首先通过cluster从元数据中获取topic所有的分区信息 */
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        //拿到该topic的分区数
        int numPartitions = partitions.size();
        //如果消息记录中没有指定key
        if (keyBytes == null) {
            //则获取一个自增的值
            int nextValue = nextValue(topic);
            //通过cluster拿到所有可用的分区(可用的分区这里指的是该分区存在首领副本)
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            //如果该topic存在可用的分区
            if (availablePartitions.size() > 0) {
                //那么将nextValue转成正数之后对可用分区数进行取余
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                //然后从可用分区中返回一个分区
                return availablePartitions.get(part).partition();
            } else { // 如果不存在可用的分区
                //那么就从所有不可用的分区中通过取余的方式返回一个不可用的分区
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else { // 如果消息记录中指定了key
            // 则使用该key进行hash操作,然后对所有的分区数进行取余操作,这里的hash算法采用的是murmur2算法,然后再转成正数
            //toPositive方法很简单,直接将给定的参数与0X7FFFFFFF进行逻辑与操作。
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

    //nextValue方法可以理解为是在消息记录中没有指定key的情况下,需要生成一个数用来代替key的hash值
    //方法就是最开始先生成一个随机数,之后在这个随机数的基础上每次请求时均进行+1的操作
    private int nextValue(String topic) {
        //每个topic都对应着一个计数
        AtomicInteger counter = topicCounterMap.get(topic);
        if (null == counter) { // 如果是第一次,该topic还没有对应的计数
            //那么先生成一个随机数
            counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
            //然后将该随机数与topic对应起来存入map中
            AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
            if (currentCounter != null) {
                //之后把这个随机数返回
                counter = currentCounter;
            }
        }
        //一旦存入了随机数之后,后续的请求均在该随机数的基础上+1之后进行返回
        return counter.getAndIncrement();
    }

二、消费者的分区分配

    Kafka通过消费者客户端参数partition.assignment.strategy 来设置消费者与订阅主题之间的分区分配策略。

    默认值是org.apache.kafka.clients.consumer.RangeAssignor,即是采用RangeAssignor分配策略

    此外还有另外两种分配策略: RoundRobinAssignor 和 StickyAssignor。

1. RangeAssignor分配策略

(1) RangeAssignor分配策略原理:

    按照分区总数 ➗  消费者总数  = 跨度,然后将分区按照跨度进行平均分配。

     对于每一个主题,RangeAssignor策略会将 消费者组内 订阅这个主题的消费者 按照名称的字典序排序

     然后为每个分区划分固定的分区范围,如果不能平均分配,字典序靠前的消费者会分配一个分区。

     假设  跨度 n = 分区数 / 消费者总数 , m = 分区数 % 消费者数量,

     那么 前m个消费者 每个分配 n+1个分区, 后面的(消费者数-m)个消费者 每个分配 n 个分区

(2)示例

    ① 假设消费组内有2个消费者C0,C1 ,都订阅了主题tO,t1 并且每个主题都有4个分区
    
        订阅的所有分区可以标识为:t0p0, t0p1, t0p2, t0p3, t1p0, t1p1, t1p2, t1p3。

        最终的分配结果为:

            消费者 C0: t0p0, t0p1, t1p1, t1p1
            消货者 C1: t0p2, t0p3, t1p2, t1p3

        分配过程详解:

            首先 分配主题分区的时候,是一个主题一个主题来分配的(千万不能2个消费者 8个分区 这样去计算)

            针对t0主题,4个分区t0p0, t0p1, t0p2, t0p3, 一个消费者组 两个消费者C0,C1按字典排序

            那么 n = 分区数4 / 消费者总数2 = 2 。 m = 分区数4 % 消费者总数2 = 0 

            即前 m(0)个分配n+1(3)个分区,后面{消费者总数2 - m(0)=2}个消费者 分配 n(2)个分区

            所以 C1 先配消费 t0p0, t0p1 ; C2 消费 t0p2, t0p3。

            针对t1主题,消费情况 和 t0相似

            所以 C1分配t1p0, t1p1 ; C2消费 t1p2,t1p3。

            最终分配情况就如上所示。

    ② 假设消费组内有2个消费者C0,C1 ,都订阅了主题tO,t1 并且每个主题都只有3个分区
    
        订阅的所有分区可以标识为:t0p0, t0p1, t0p2, t1p0, t1p1, t1p2。

        最终的分配结果为:

            消费者 C0: t0p0, t0p1, t1p0, t1p1
            消货者 C1: t0p2, t1p2

        分配过程详解:

            针对t0主题,3个分区t0p0, t0p1, t0p2, 一个消费者组 两个消费者C0,C1按字典排序

            那么 n = 分区数3 / 消费者总数2 = 1 。 m = 分区数4 % 消费者总数2 = 1 

            即 前 m(1)个消费者(t1)分配 n+1(2)个分区, 后面1个消费者分配 n(1)个分区

            所以 C1 先配消费 t0p0, t0p1 ; C2 消费 t0p2

            针对t1主题,消费情况 和 t0相似

            所以 C1分配t1p0, t1p1 ; C2消费 t1p2

            最终分配情况就如上所示,C0分配四个分区, C1最终分配2个分区

(3) 策略缺点

	如上示例②所示,可以明显的看到存在分配不均匀的情况,如果将类似的情况扩大,则有可能出现部分消费者过载的情况。

2. RoundRobinAssignor分配策略

(1) RoundRobinAssignor策略原理

    将所有消费者组内的 消费者 及 所有订阅的主题分区按照字典顺序排序,然后通过轮询的方式分配给每个消费者。 
    (按顺序 一人一个)  

(2)示例

    ① 如果同一个消费组内所有的消费者的订阅信息都是相同的,那么分区分配会是均匀的。

       假设消费组中有2个消费者 ,都订阅了主题tO,t1 ,并且每个主题都有3个分区,
     
       那么订阅的所有分区可以标识为:t0p0, t0p1, t0p2, t1p0, t1p1, t1p2。
    
       最终的分配结果为
        
        消费者 C0: t0p0 , t0p2 , t1p1
        消货者 C1: t0p1 , t1p0 , t1p2 

    ② 如果同一个消费组内的消费者订阅的信息是不相同的,那么分区分配的时候就不是完全的轮询分配,有可能导致分区分配得不均匀。

        如果某个消费者没有订阅消费组内的某个主题,那么在分配分区的时候此消费者将分配不到这个主题的任何分区。

        举个例子,假设消费组内有3个消费者C0, C1, C2,它们共订阅了 3个主题t0, t1, t2
        
        3个主题分别有1,2,3个分区,即整个消费组订阅了 t0p0, t1p0 ,t1p1, t2p0, t2p1,t2p2个分区 

        具体而言,消费者C0,订阅的是主题t0;消费者C1,订阅的是主题t0,t1;消费者C2订阅的是主题 t0,t1,t2 

        那么最终的分配结果为

            消费者 C0: t0p0
            消费者 C1: t1p0
            消费者 C2: t1p1, t2p0, t2p1,t2p2

(3) 策略缺点

	从上面②可以看到 RoundRobinAssignor 策略也不是十分完美,这样分配其实并不是最优解,
	因为完全可以将分区 t1p1 分配给消费者C1

3.StickyAssignor分配策略

(1) StickyAssignor分配策略原理

    sticky 这个单词可以翻译为“粘性的”, Kafka0.11.x 本开始引入这种分配策略,它主要有两个目的
    
        ①分区的分配要尽可能均匀 

        ②分区的分配尽可能与上次分配的保持相同。
    
    当两者发生冲突时,第一个目标优先于第二个目标。

(2)示例

    ① 假设消费组内有3个消费者C0, C1, C2,它们共订阅了 4个主题t0, t1, t2,t3,并且每个主题有2个分区

      也就是说,整个消费组订阅了 t0p0, t0p1 ,t1p0, t1p1, t2p0,t2p1, t3p0, t3p1 这个8个分区 

      最终的分配结果如下:
            
        消费者 C0: t0p0, t1p1, t3p0
        消费者 C1: t0p1, t2p0, t3p1
        消费者 C2: t1p0, t2p1 

      分配结果看上去和 RoundRobinAssignor分配结果一样。但是假设消费者C1脱离了消费者组,执行重分配后呢?请看示例②


    ② 假设此时消费者 C1脱离了消费组,那么消费组就会执行再均衡操作,进而消费分区会重新分配。

       如果采用 RoundRobinAssignor 分配策略,那么此时的分配结果如下:

            消费者 C0:t0p0, t1p0, t2p0, t3p0
            消费者 C2:t0p1, t1p1, t2p1, t3p1

       但是如果此时使用的是 StickyAssignor 分配策略,那么分配结果为:

            消费者 C0:t0p0, t1p1, t3p0, t2p0
            消费者 C2:t1p0, t2p1, t0p1, t3p1

       可以看到分配结果中保留了上一次分配中对消费者C0, C2 的所有分配结果,

       并将原来消费者C1的 "负担"分配给了剩余的两个消费者 C0、C2, 最终 C0和C2 的分配还保持了均衡。

       如果发生分区重分配,那么对于同一个分区而言,有可能之前的消费者和新指派的消费者

       不是同一个,之前消费者进行到一半的处理还要在新指派的消费者中再次复现一遍,这显然很浪费系统资源。 

       StickyAssignor 分配策略,尽可能地让前后两次分配相同,进而减少系统资源的损耗及其他异常情况的发生。

    ③ 以上分析的都是消费者的订阅信息都是相同的情况,下面看一下订阅信息不同的情况下的处理。
        
       假设消费组内有3个消费者C0, C1, C2,它们共订阅了 3个主题t0, t1, t2
        
       3个主题分别有1,2,3个分区,即整个消费组订阅了 t0p0, t1p0 ,t1p1, t2p0, t2p1,t2p2个分区 

       具体而言,消费者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

4.自定义分区分配策略

    用户除了使用Kafka 提供3种分配策略,还可自定义分配策略来实现更多选的功能 。
    
    自定义的分配策略需要实现org.apache.kafka.clients.consumer.intemals.PartitionAssignor

三、broker分区分配

    这里的broker分区分配是指为集群制定创建主题时的分区副本分配方案,即在哪个 broker 中创建哪些分区的副本。
    
    在创建主题时,如果使用了 replica-assignment 参数,那么就按照指定的方案来进分区副本的创建;

    如果没有使用 replica-assignment 参数,那么就需要按照内部的逻辑来计算分配方案了 。

    内部分配逻辑按照机架信息划分成两种策略: 未指定机架信息 和 指定机架信息 

    如果集群中所有的 broker节点都没有配置broker.rack 参数,或者使用 disable-rack-aware 参数来创建建主题,

    那么采用的就是未指定机架信息的分配策略,否则采用的就是指定机架信息的分配策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值