Kafka-分区

kafka的消息时一个个键值对,ProducerRecord对象可以包含目标主题和值,键可以设置为默认的null,不过大多数应用程序会用到键。

键有两个用途;可以作为消息的附加信息,也可以用来决定消息该被写到主题的哪个分区。拥有相同键的消息将被写到同一个分区。

如果键值为null,并且使用了默认的分区器,那么记录将被随机的发送到主题内各个可用的分区上。分区器使用轮询(Round Robin)算法将消息均衡地分不到各个分区上。

如果键不为空,并且使用了默认的分区器,那么kafka会对键进行散列(kafka自己的散列算法),然后根据散列值把消息映射到特定的分区上。同一个键总是被映射到同一个分区上,所以在进行映射时,我们会使用主题所有的分区,而不仅仅是可用的分区。所以,如果写入数据的分区是不可用的,那么就会发生错误。

如果要使用键来映射分区,那么最好在创建主题的时候就把分区规划好,而且永远不要增加新分区。

实现自定义分区策略

除了散列分区之外,有时候也需要对数据进行不一样的分区。

假设你是一个B2B供应商,有一个大客户,它是手持设备Apple的制造商。Apple占据了整体业务的10%的份额。如果使用默认的散列分区算法,Apple的账号记录将和其他账号记录一起被分配给相同的分区,导致这个分区比其它分区要大一些。服务器可能因此出现存储空间不足、处理缓慢等问题。我们需要给Apple分配单独的分区,然后使用散列分区算法处理其他账号。

 代码如下

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.InvalidRecordException;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.utils.Utils;

import java.util.List;
import java.util.Map;

/**
* @Author FengZhen
* @Date 2020-03-31 22:38
* @Description 自定义分区
*/
public class ApplePartitioner implements Partitioner {

@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (null == keyBytes || !(key instanceof String)) {
throw new InvalidRecordException("We expect all messages to have customer name as key");
}

if (key.equals("Apple")){
//分配到最后一个分区
return numPartitions - 1;
}
return Math.abs(Utils.murmur2(keyBytes)) % (numPartitions - 1);
}

@Override
public void close() {

}

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

}
}

 

测试

import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Properties;

/**
* @Author FengZhen
* @Date 2020-03-29 12:21
* @Description kafka生产者使用
*/
public class KafkaProducerTest {

private static Properties kafkaProps = new Properties();
static {
kafkaProps.put("bootstrap.servers", "localhost:9092");
kafkaProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
kafkaProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
}

public static void main(String[] args) {
KafkaProducer<String, String> producer = new KafkaProducer(kafkaProps);
ProducerRecord<String, String> record = new ProducerRecord<>("test","message_key","message_value");
// simpleSend(producer, record);
// sync(producer, record);
// aync(producer, record);
udfPartition();
}

/**
* 使用自定义分区
* ./kafka-topics.sh --create --zookeeper localhost:2181/kafka_2_4_1 --replication-factor 1 --partitions 3 --topic test_partition
*/
public static void udfPartition(){
kafkaProps.put("partitioner.class", "com.chinaventure.kafka.partition.ApplePartitioner");
KafkaProducer<String, String> producer = new KafkaProducer(kafkaProps);
for (int i = 0; i < 10; i++){
ProducerRecord<String, String> record = new ProducerRecord<>("test_partition",i % 3 == 0 ? "Apple": "Banana"+i,"我是" + i);
producer.send(record, new DemonProducerCallback());
}
while (true){
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

/**
* 最简单的方式发送,不管消息是否正常到达
* @param producer
*/
public static void simpleSend(KafkaProducer producer, ProducerRecord record){
try {
producer.send(record);
} catch(Exception e){
e.printStackTrace();
}
}

/**
* 同步发送
* @param producer
* @param record
*/
public static void sync(KafkaProducer producer, ProducerRecord record){
try {
RecordMetadata recordMetadata = (RecordMetadata) producer.send(record).get();
System.out.println("topic:" + recordMetadata.topic());
System.out.println("partition:" + recordMetadata.partition());
System.out.println("offset:" + recordMetadata.offset());
System.out.println("metaData:" + recordMetadata.toString());
} catch(Exception e){
e.printStackTrace();
}
}

/**
* 异步发送
* @param producer
* @param record
*/
public static void aync(KafkaProducer producer, ProducerRecord record){
try {
producer.send(record, new DemonProducerCallback());
while (true){
Thread.sleep(10 * 1000);
}
} catch(Exception e){
e.printStackTrace();
}
}

private static class DemonProducerCallback implements Callback {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (null != e){
e.printStackTrace();
}else{
System.out.println("topic:" + recordMetadata.topic());
System.out.println("partition:" + recordMetadata.partition());
System.out.println("offset:" + recordMetadata.offset());
System.out.println("metaData:" + recordMetadata.toString());
}

}
}
}

查看最后一个分区的日志

FengZhendeMacBook-Pro:bin FengZhen$ cat /tmp/kafka-logs/test_partition-2/00000000000000000000.log 
}??k?q1?q1???????????????$
Apple我是0$&
Apple我是3$&
Apple我是6$&
Apple我是9