美文网首页
kafka分区器原理

kafka分区器原理

作者: hello_kd | 来源:发表于2022-02-26 19:05 被阅读0次

    在kafka中,生产者发送的消息最终会落在主题下的某个分区,但是很多开发者在使用的过程中其实并没有指定消息发往哪个分区,那么kafka是如何处理的呢?

    在kafka中,消息主要由两部分组成,一部分是key,消息的键,一部分是value,消息的载体,也是实际要处理的消息内容。其中key的作用就是起到路由的作用,决定了value发往哪个分区,但前提是生产者消息对象没有明确指定消息发往哪个分区,key的路由才会发挥作用,而key的路由最终就是分区器处理的。

    本文就以基于Java的kafka客户端,通过分析下源码,来了解下分区器的原理和使用

    首先,通过kafka的shell命令创建一个名称为testTopic,分区数为3的topic

    ./kafka-topics.sh --create --topic testTopic --partitions 3 --replication-factor 1  
    --bootstrap-server localhost:9092
    

    接着,启动一个消费者对主题为testTopic,partition为0的分区进行消息的监听

    ./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic testTopic  --partition 0
    

    然后,用Java客户端代码进行消息的发送,下面我们省略了大部分代码,只关注消息的构建和消息的发送

    ProducerRecord<String, String> record = new ProducerRecord<>("myTopic", 0,null,"hello world");
    KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
    producer.send(record);
    

    在上述这段代码中,ProducerRecord就是表示消息对象,构造参数有4个值,分别为主题、分区、key、value,在这段代码中,明确指定了分区为0,因此在消费者端会有hello world的输出


    consumer.png

    当将ProducerRecord对象的分区数设置为1,再发送,这时发现消费者端看不到新发送的消息对象,因为明确指定了发往的分区是1,而消费者监听的分区为0,因此收不到。

    因此,这边可以得出一个结论,若消息对象显示指定了消息所属的分区,那么消息就会往这个分区发送

    接着,对消息对象进行下修改,不指定分区,设置一个key值,然后来跟踪下源码,看下消息会最终发往哪个分区

    ProducerRecord<String, String> record = new ProducerRecord<>("testTopic", "home","hello world");
    

    通过跟进send方法,会看到如下源码

    private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        TopicPartition tp = null;
        try {
            省略....
            Cluster cluster = clusterAndWaitTime.cluster;
            byte[] serializedKey;
            try {
                serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
            } catch (ClassCastException cce) {
                throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() +
                        " to class " + producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() +
                        " specified in key.serializer", cce);
            }
            byte[] serializedValue;
            try {
                serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
            } catch (ClassCastException cce) {
                throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() +
                        " to class " + producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() +
                        " specified in value.serializer", cce);
            }
            int partition = partition(record, serializedKey, serializedValue, cluster);
            tp = new TopicPartition(record.topic(), partition);
    
            省略...
        } catch (Exception e) {
            省略...
        }
    }
    

    这段代码中,这边列举出关键的部分,首先获取到集群的信息Cluster对象,然后对消息的key和value序列化成字节数组,再调用partition方法,此方法便是获取消息发送的分区序号

    private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
        Integer partition = record.partition();
        return partition != null ?
                partition :
                partitioner.partition(
                        record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
    }
    

    这段代码就可以很明确的看出,首先会拿到消息对象的partition属性值,若该值不为空,则直接返回;当值为空的时候才会调用分区器的分区方法,换句话说,分区器只有在消息对象的partition属性值为null的情况下才起作用,而本文的重点就是关注分区器的作用,在kafka中分区器是一个接口,需要具体的实现,而kakfa提供一个默认的分区器实现可以直接使用,无需开发者自己手动实现,当然可以根据实际的业务需求,来实现具体的分区器

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        if (keyBytes == null) {
            return stickyPartitionCache.partition(topic, cluster);
        } 
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        // hash the keyBytes to choose a partition
        return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
    }
    

    默认的分区器中,首先会根据是key的字节数组是否为空,若不为空,先获取集群中该主题的总分区数量,然后再根据字节数组数的一个32位hash值和总分区数进行取模运算,得到最终的分区序号,该方式也比较简单,比较复杂的是当key为null的情况,处理如下

    public int partition(String topic, Cluster cluster) {
        Integer part = indexCache.get(topic);
        if (part == null) {
            return nextPartition(topic, cluster, -1);
        }
        return part;
    }
    
    public int nextPartition(String topic, Cluster cluster, int prevPartition) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        Integer oldPart = indexCache.get(topic);
        Integer newPart = oldPart;
        // Check that the current sticky partition for the topic is either not set or that the partition that 
        // triggered the new batch matches the sticky partition that needs to be changed.
        if (oldPart == null || oldPart == prevPartition) {
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() < 1) {
                Integer random = Utils.toPositive(ThreadLocalRandom.current().nextInt());
                newPart = random % partitions.size();
            } else if (availablePartitions.size() == 1) {
                newPart = availablePartitions.get(0).partition();
            } else {
                while (newPart == null || newPart.equals(oldPart)) {
                    Integer random = Utils.toPositive(ThreadLocalRandom.current().nextInt());
                    newPart = availablePartitions.get(random % availablePartitions.size()).partition();
                }
            }
            // Only change the sticky partition if it is null or prevPartition matches the current sticky partition.
            if (oldPart == null) {
                indexCache.putIfAbsent(topic, newPart);
            } else {
                indexCache.replace(topic, prevPartition, newPart);
            }
            return indexCache.get(topic);
        }
        return indexCache.get(topic);
    }
    

    首先,当消息没指定key时,kafka会尽量使同一批次的消息都发往同一个分区,称之为粘性分区(sticky partition),而且会将粘性分区缓存在一个map中,这样下次可以直接从缓存取而无需重新计算分区号。
    当从缓存map中查不到主题对应的粘性分区号时,调用nextPartition方法获取一个新的分区号,并将其放入到缓存中。

    总结,分区器的主要作用是在消息对象没有明确指定partition值时起的作用,并且会根据key是否有值来计算。若有,则使用取模运算,因此,key值相同的消息会都发往相同的分区中。若没有,则会尽量时发往同一主题的消息都落在同一分区上,当然了,这里需要结合后续的累加器作用,才能更好的理解。

    相关文章

      网友评论

          本文标题:kafka分区器原理

          本文链接:https://www.haomeiwen.com/subject/nypmxrtx.html