Kafka生产者分区器的规则详解
1、介绍
在开发中,由于Kafka配置的地方被他人改动过,所以有些数据出现了往固定分区集中的现象,所以这篇文章重点研究下Kafka生产者分区器的规则。
2、原因
我们通常开多线程、使用多个分区来提高Kafka的消费速度,分区不均匀会导致线程闲置,消费速度过慢,进而导致消息积压。
消息写入哪个分区是由生产者
决定的,在调用kafkaTemplate.send()
方法时,可以指定分区,否则使用默认分区器DefaultPartitioner
计算。因为分区可能会调整,通常我们不会指定固定分区,而是依靠分区计算器。
查看DefaultPartitioner
代码可以得知:当指定了key
每次都会计算出固定的分区,否则会自动计算出一个可用分区。
3、解决方案
不再指定key
- 自定义分区器,每次计算出不同的分区
4、 具体规则
Kafka中的每个Topic一般会分配N个Partition,那么生产者(Producer
)在将消息记录(ProducerRecord
)发送到某个Topic
对应的Partition
时采用何种策略呢?Kafka中采用了分区器(Partitioner
)来为我们进行分区路由的操作。Kafka给我们提供的分区器实现DefaultPartitioner
,我们也可以实现自定义的分区器,只需要实现Partitioner
接口。
生产者生产一个消息send到topic分区器,分区器会根据消息里面的分区参数key值把消息分到对应的partition。
Partitioner接口
Partitioner
接口中有一个最主要的方法:
/**
* 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 The serialized key to partition on( or null if no key)
* @param value The value to partition on or null
* @param valueBytes The serialized value to partition on or null
* @param cluster The current cluster metadata
*/
int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
cluster能够根据指定topic,获取该topic所对应的分区的信息。
DefaultPartitioner
生产者在发送消息时选择分区在KafkaProducer
类的partition
方法中。
/**
* computes partition for given record.
* if the record has partition returns the value otherwise
* calls configured partitioner class to compute the 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);
}
首先判断ProducerRecord
中的partition
字段是否有值,即是否在创建消息记录的时候直接指定了分区,如果指定了分区,则直接将该消息发送到指定的分区,否则调用分区器的partition
方法,执行分区策略。如果用户配置了分区器,则使用用户指定的分区器,否则使用默认的分区器,即DefaultPartitioner
,我们可看该默认实现是如何进行分区选择的。
下面是Kafka对消息分配分区 DefaultPartitioner.java 类的核心代码。
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();
}
总结
生产者发送消息时整个分区路由的步骤如下:
- 判断消息中的
partition
字段是否有值,有值的话即指定了分区,直接将该消息发送到指定的分区就行。- 如果没有指定分区,则使用分区器进行分区路由,首先判断消息中是否指定了
key
。- 如果指定了
key,
则使用该key
进行hash操作,并转为正数,然后对topic
对应的分区数量进行取模操作并返回一个分区。- 如果没有指定
key,
则通过先产生随机数,之后在该数上自增的方式产生一个数,并转为正数之后进行取余操作。如果该topic
有可用分区,则优先分配可用分区,如果没有可用分区,则分配一个不可用分区。这与第3点中key
有值的情况不同,key
有值时,不区分可用分区和不可用分区,直接取余之后选择某个分区进行分配。
简单来说就是:
如果没有指定key值并且可用分区个数大于0时,在就可用分区中做轮询决定改消息分配到哪个partition。
如果没有指定key值并且没有可用分区时,在所有分区中轮询决定改消息分配到哪个partition。
如果指定key值,对key做hash分配到指定的partition,所以当同一个key的消息会被分配到同一个partition中。消息在同一个partition处理的顺序是FIFO,这就保证了消息的顺序性。