Kafka分区策略

1,149 阅读4分钟

Kafka为我们提供了默认的分区策略和自定义消息分区策略两种方式管理producer端消息分区。

一、默认分区

Kafka默认分区使用的是DefaultPartitioner,它实现了Partitioner接口。 首先我们看一下Partitioner接口。

Partitioner接口

在2.4.0之前,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
*/
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);

/**
* This is called when partitioner is closed.
*/
public void close();

很显然,partition方法就是用来计算消息分区的。

在2.4.0版本上,Kafka增加了onNewBatch方法,这个方法主要用来更新一个新的发送batch分区数的,下面会详细讲解。

/**
* Notifies the partitioner a new batch is about to be created. When using the sticky partitioner,
* this method can change the chosen sticky partition for the new batch. 
* @param topic The topic name
* @param cluster The current cluster metadata
* @param prevPartition The partition previously selected for the record that triggered a new batch
*/
default public void onNewBatch(String topic, Cluster cluster, int prevPartition) {
}

DefaultPartitioner

看一下DefaultPartitioner实现,主要看一下partition方法,它实现如何计算partition值。 Kafka 2.4.0之前代码:

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
  //根据topic获取partition数量
  List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
  int numPartitions = partitions.size();
  if (keyBytes == null) {
      int nextValue = nextValue(topic);
      //获取可用的分区数量
      List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
      if (availablePartitions.size() > 0) {
          //partition = nextValue值对可用分区数取模,nextValue是一个自增的AtomicInteger,这就相当于没有指定key时,轮训所有可用分区
          int part = Utils.toPositive(nextValue) % availablePartitions.size();
          return availablePartitions.get(part).partition();
      } else {
          //没有可用分区,随机分配到一个partition        		
          return Utils.toPositive(nextValue) % numPartitions;
      }
  } else {
      //消息指定了key,partition = key的hash值对分区数取模 
      return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
  }
}

private int nextValue(String topic) {
  //根据topic获取counter值,没有就创建一个新的,有就自增返回
  AtomicInteger counter = topicCounterMap.get(topic);
  if (null == counter) {
      counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
      AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
      if (currentCounter != null) {
          counter = currentCounter;
      }
  }
  //自增返回
  return counter.getAndIncrement();
}

从partition方法中可以看到,如果消息指定了key,就采用key的hash值对分区数取模的方式得到分区;如果没有指定key,则采用轮训的方式得到分区数。

在Kafka 2.4.0之后,Kafka对未指定key的分区逻辑做了修改,引入了新的粘分区缓存类StickyPartitionCache,没有key时,程序会去缓存中获取分区数。

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster,
                     int numPartitions) {
    //没有key,去粘分区中获取partition
    if (keyBytes == null) {
        return stickyPartitionCache.partition(topic, cluster);
    }
    // hash the keyBytes to choose a partition
    //有key,去序列化后的key的hash,模分区数
    return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}

public void onNewBatch(String topic, Cluster cluster, int prevPartition) {
	//更新粘分区缓存中的分区数,详情看nextPartition方法。
    stickyPartitionCache.nextPartition(topic, cluster, prevPartition);
}

看一下StickyPartitionCache代码:

public int partition(String topic, Cluster cluster) {
	//缓存中有,直接返回,如果缓存中没有,就用nextPartition方法更新缓存
    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;
    if (oldPart == null || oldPart == prevPartition) {
        List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
        //可用分区数小于1,则使用总分区数随机分配分区
        if (availablePartitions.size() < 1) {
            Integer random = Utils.toPositive(ThreadLocalRandom.current().nextInt());
            newPart = random % partitions.size();
        } else if (availablePartitions.size() == 1) {
            //可用分区为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();
            }
        }
        //cache中没有分区数,或者是新的batch时,更新分区数
        if (oldPart == null) {
            indexCache.putIfAbsent(topic, newPart);
        } else {
            indexCache.replace(topic, prevPartition, newPart);
        }
        return indexCache.get(topic);
    }
    return indexCache.get(topic);
}

上面的nextPartition方法时用来更新缓存的,两种情况会用到,一是缓存丢失或者第一次还没有缓存时,二是当accumulator中产生新的batch时,我们看一下,它是怎么被调用的:

private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
	... ...
	if (result.abortForNewBatch) { //产生新的batch
    	int prevPartition = partition;
        //当record进入一个新的batch的时候,修改粘分区的partition数
        partitioner.onNewBatch(record.topic(), cluster, prevPartition);
        partition = partition(record, serializedKey, serializedValue, cluster);
        ... ...
    }
    ... ...
}

StickyPartitionCache作用

StickyPartitionCache是为了保证不产生过多的小batch导致producer发送过多的请求。

老版本分区方式,各个消息拥有不同的分区号,会被轮训到每一个分区中,因为发送时一个batch中分区号必须相同,这样就会产生很更多的网络请求。而StickyPartitionCache保证了没有可以的消息使用相同的分区号,发送时以一个整体发送网络请求到相同的分区中,可以提高Kafka的吞吐量。

二、自定义Partitioner

自定义Partitioner只需要实现Partitioner接口,重写partition方法就行,具体的分区逻辑在partition方法中实现,example:

public class CustomPartitioner implements Partitioner {

    @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes1, Cluster cluster) {
        if (Integer.parseInt((String)key)%3==1) {
            return 0;
        }else if (Integer.parseInt((String)key)%3==2) {
            return 1;
        }else{
            return 2;
        }
    }

    @Override public void close() {

    }

    @Override public void configure(Map<String, ?> map) {

    }
}