目录

1 kafka为什么要分区

2 kafka数据分配策略

2.1 轮询策略

2.2 随机策略

2.3 消息按Key保序策略

2.4 如何使用

2.5 kafka自定义分区策略

3 小结


1 kafka为什么要分区

kafka本质上是一个消息数据库,最开始kafka是因为消息队列起家的,但已发展到今天,kafka实质上已经成为一个消息数据库或消息引擎。为什么这么说呢?因为就是kafka的分区机制。kafka为什么要做分区?从kafka的数据存储结构来看,kafka是 Topic->Partition->Message的。主题下的每条消息只会保存在某一个分区中,而不会在多个分区中被保存多份。官网上的这张图非常清晰地展示了 Kafka 的三级结构,如下所示:

kafka为什么只有一个分区消费 kafka为什么要分区_自定义

  实际上对于一个分布式的数据库而言,必须支持水平可扩展性,弹性伸缩等,kafka为了满足这样的条件支持分片操作,便有了分区的概念,事实上对于多个数据库都有这个概念,在MPP数据库中叫shard分片,HBase中叫Region,Spark中的RDD的Partition,在 Cassandra 中又被称作 vnode。从表面看起来它们实现原理可能不尽相同,但对底层分区(Partitioning)的整体思想却从未改变。

  综上:Kafka实现分区的主要目的是为了实现系统的高伸缩性(Scalability),实现并行处理的能力,在Kafka中分区还有一个目的就是为了实现分区内数据有序。

2 kafka数据分配策略

2.1 轮询策略

也称 Round-robin 策略,即顺序分配。比如一个主题下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2,以此类推。当生产第 4 条消息时又会重新开始,即将其分配到分区 0,如下图展示的那样。

kafka为什么只有一个分区消费 kafka为什么要分区_自定义_02

这就是所谓的轮询策略。轮询策略是 Kafka Java 生产者 API 默认提供的分区策略。如果你未指定partitioner.class参数,那么你的生产者程序会按照轮询的方式在主题的所有分区间均匀地“码放”消息。 轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。

2.2 随机策略

也称 Randomness 策略。所谓随机就是我们随意地将消息放置到任意一个分区上,如下面这张图所示。

kafka为什么只有一个分区消费 kafka为什么要分区_kafka_03

如果要实现随机策略版的 partition 方法,很简单,只需要两行代码即可:

List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);

return ThreadLocalRandom.current().nextInt(partitions.size());

先计算出该主题总的分区数,然后随机地返回一个小于它的正整数(随机数左闭右开)。

本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以如果追求数据的均匀分布,还是使用轮询策略比较好。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。

2.3 消息按Key保序策略

Kafka 允许为每条消息定义消息键,简称为 Key。这个 Key 的作用非常大,它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务 ID 等;也可以用来表征消息元数据。特别是在 Kafka 不支持时间戳的年代,在一些场景中,工程师们都是直接将消息创建时间封装进 Key 里面的。一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略,如下图所示。

kafka为什么只有一个分区消费 kafka为什么要分区_kafka为什么只有一个分区消费_04

实现这个策略的 partition 方法同样简单,只需要下面两行代码即可:

List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);

return Math.abs(key.hashCode()) % partitions.size();

2.4 如何使用

要将 producer 发送的数据封装成一个 ProducerRecord 对象

通过new出ProducerRecord 对象,传入参数值即可,如下所示:

kafka为什么只有一个分区消费 kafka为什么要分区_数据库_05

内部数据结构:
-- Topic (名字)
-- PartitionID ( 可选)
-- Key[( 可选 )
-- Value

提供三种构造函数形参:

-- ProducerRecord(topic, partition, key, value)

-- ProducerRecord(topic, key, value)

-- ProducerRecord(topic, value)

  • 1、如果指定的partition,那么直接进入该partition
  • 2、如果没有指定partition,但是指定了key,使用key的hash选择partition
  • 3、如果既没有指定partition,也没有指定key,使用轮询的方式进入partition

2.5 kafka自定义分区策略

自定义分区:类似于Hive中的UDF函数的定义、mr中的partition自定义等

核心类

public interface Partitioner extends Configurable, Closeable {
/**
* 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 key序列之后的字节数组的形式
* @param value The value to partition on or null
* @param valueBytes value序列之后的字节数组的形式
* @param cluster The current cluster metadata 当前cluster的元数据信息
*/
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[]
valueBytes, Cluster cluster);
/**
* This is called when partitioner is closed.

随机分区方式
注册使用
hash分区方式
分区结束之后被调用
*/
public void close();
}

public interface Configurable {
/**
* Configure this class with the given key-value pairs
指定当前producer的配置信息
*/
void configure(Map<String, ?> configs);
}

随机分区方式 

public class RandomPartitioner implements Partitioner {
public void close() {
}
public void configure(Map<String, ?> configs) {
}
private Random random = new Random();
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[]
valueBytes, Cluster cluster) {
Integer partitionCount = cluster.partitionCountForTopic(topic);//返回当前topic的partition个数
int partition = random.nextInt(partitionCount);
System.out.println("partition: " + partition);
return partition;
}
}

注册使用

 partitioner.class值为自定义分区类的完整包名,这样生产者就会选择自定义的分区策略。

props.put("partitioner.class", "xx.xx.RandomPartitioner");

说明:1.客户端测试环境中,自定义分区类跟生产者类在一个项目中,不需要其他操作;

           2.想要自定义的分区放到kafka的服务器端环境时,需要将自定义的分区类生成jar包放到kafka环境的lib下,同样配置文件中指定完整包名。 

hash分区方式

public class HashPartitioner implements Partitioner {
public void close() {
}
public void configure(Map<String, ?> configs) {
}

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[]
valueBytes, Cluster cluster) {
Integer partCount = cluster.partitionCountForTopic(topic);
int partition = Math.abs(key.hashCode()) % partCount;
System.out.println("key: " + key + "partition: " + partition);
return partition;
}
}

轮询分区方式 

public class RoundRobinPartitioner implements Partitioner {
public void close() {
}
public void configure(Map<String, ?> configs) {
}
//定义一个原子计数器
private AtomicInteger count = new AtomicInteger();
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[]
valueBytes, Cluster cluster) {
int parCount = cluster.partitionCountForTopic(topic);
int partition = count.getAndIncrement() % parCount;
System.out.println("key: " + key + "\tpartition: " + partition);
return partition;
}
}

3 小结

Java客户端默认的生产者分区策略的实现类为org.apache.kafka.clients.producer.internals.DefaultPartitioner。默认策略为:如果指定了partition就直接发送到该分区;如果没有指定分区但是指定了key,就按照key的hash值选择分区;如果partition和key都没有指定就使用轮询策略。

kafka为什么只有一个分区消费 kafka为什么要分区_big data_06