kafka分区器原理

2022-02-26  本文已影响0人  hello_kd

在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值相同的消息会都发往相同的分区中。若没有,则会尽量时发往同一主题的消息都落在同一分区上,当然了,这里需要结合后续的累加器作用,才能更好的理解。

上一篇下一篇

猜你喜欢

热点阅读