Kafka分区机制与代码示例 Kafka分区机制与代码示例

Kafka分区机制与代码示例

Kafka中,topic是逻辑上的概念,而partition是物理上的概念。不用担心,这些对用户来说是透明的。生产者(producer)只关心自己将消息发布到哪个topic,而消费者(consumer)只关心自己订阅了哪个topic上的消息,至少topic上的消息分布在哪些partition节点上,它本身并不关心。

如果没有分区的概念,那么topic的消息集合将集中于某一台服务器上,单节点的存储性能马上将成为瓶颈,当访问该topic存取数据时,吞吐也将成为瓶颈。

介于此,kafka的设计方案是,生产者在生产数据的时候,可以为每条消息人为的指定key,这样消息被发送到broker时,会根据分区规则,选择消息将被存储到哪一个分区中。如果分区规则设置合理,那么所有的消息将会被均匀/线性的分布到不同的分区中,这样就实现了负载均衡和水平扩展。另外,在消费者端,同一个消费组可以多线程并发的从多个分区中 同时消费数据。

上述分区规则,实际上是实现了kafka.producer.Partitioner接口的一个类,这个实现类可以根据自己的业务规则进行自定义制定,如根据hash算法指定分区的分布规则。

如以下这个类,我们先获取key的hashcode值,再跟分区数量(配置文件中为numPartitions)做模运算,结果值作为分区存储位置,这样可以实现数据均匀线性的分布。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

自定义Partitioner的实现如下:

import kafka.producer.Partitioner;

/**
 * Created by david on 17-3-27.
 */
public class SimplePartitioner implements Partitioner {
    @Override
    public int partition(Object key, int numPartitions) {
        int partition = 0;

        String k = (String) key;

        partition = Math.abs(k.hashCode()) % numPartitions;

        return partition;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

bin目录下的kafka-topics.sh中,设置分区数的命令行参数为:

--partitions <Integer: # of partitions> The number of partitions for the topic 
                                          being created or altered (WARNING:   
                                          If partitions are increased for a    
                                          topic that has a key, the partition  
                                          logic or ordering of the messages    
                                          will be affected
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

此外,亦可以在配置文件server-XXXX.properties中,使用配置参数num.partitions,来进行全局默认的分区数的设置(如果命令行或代码中也设置,则会overwrite全局默认参数)。

但有一点需要注意,为Topic创建分区时,分区数最好是broker数量的整数倍,这样才能是一个Topic的分区均匀的分布在整个Kafka集群中,假设我的Kafka集群由4个broker组成,以下图为例:

现在创建一个topic “liuwei0376”,为该topic指定4个分区,那么这4个分区将会在每个broker上各分布一个:

./kafka-topics.sh \
--create \
--zookeeper localhost:2181,localhost:2182,localhost:2183 \
--replication-factor 1 \
--partitions 4 \
--topic test_diy_partition
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样所有的分区就均匀分布在集群中,如果创建topic时候指定了3个分区,那么就有一个broker上没有该topic的分区。

生产者根据自定义分区规则进行分区

现在用一个生产者示例(PartitionerProducer),向Topic lxw1234中发送消息。该生产者使用的分区规则,就是上面的SimplePartitioner。从0-10一共11条消息,每条消息的key 为”key”+index,消息内容为”key”+index+”–value”+index。比如:key0–value0、key1– value1、、、key10–value10。

package com.david.test.kafka.partition;

import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;

import java.util.Properties;

/**
 * Created by david on 17-3-27.
 *
 * 生产者使用自定义hash分区器,向kafka的broker分区中均匀写入数据
 */
public class PartitionerProducer {

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("serializer.class", "kafka.serializer.StringEncoder");
        props.put("metadata.broker.list", "localhost:9091,localhost:9092,localhost:9091,localhost:9092");
        props.put("partitioner.class", "com.david.test.kafka.partition.SimplePartitioner");

        Producer<String, String> producer = new Producer<String, String>(new ProducerConfig(props));

        String topic = "test_diy_partition";
        for (int i = 0; i <= 10; i++) {
            String k = "key" + i;
            String v = k + "--value" + i;
            producer.send(new KeyedMessage<String, String>(topic, k, v));
        }
        producer.close();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

理论上来说,生产者在发送消息的时候,会按照SimplePartitioner的规则,将key0做hashcode,然后和分区数(4)做模运算,得到分区索引:

hashcode(“key0”) % 4 = 1

hashcode(“key1”) % 4 = 2

hashcode(“key2”) % 4 = 3

hashcode(“key3”) % 4 = 0

     ……
  • 1
  • 2

对应的消息将会被发送至相应的分区中。

消费者消费自定义分区数据

下面的消费者代码用来验证,在消费数据时,打印出消息所在的分区及消息内容:

package com.david.test.kafka.partition;


import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;
import kafka.message.MessageAndMetadata;

/**
 * Created by david on 17-3-27.
 *
 * 消费者轮询消费kafka的broker分区中的数据
 */
public class PartitionerConsumer {
    public static void main(String[] args) {
        String topic = "test_diy_partition";
        ConsumerConnector consumer =
                Consumer.createJavaConsumerConnector(createConsumerConfig());

        Map<String, Integer> topicMap = new HashMap<String, Integer>();

        topicMap.put(topic, new Integer(1));

        Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap =
                consumer.createMessageStreams(topicMap);

        KafkaStream<byte[], byte[]> stream = consumerMap.get(topic).get(0);

        ConsumerIterator<byte[], byte[]> it = stream.iterator();

        while (it.hasNext()) {
            MessageAndMetadata<byte[], byte[]> mam = it.next();
            System.out.println("消费者消费数据: 【分区号: [" + mam.partition() + "], 存储的消息: [" + new String(mam.message()) + "] 】");
        }
    }


    private static ConsumerConfig createConsumerConfig() {
        Properties props = new Properties();
        props.put("group.id", "test_diy_part_group");
        props.put("zookeeper.connect", "localhost:2181,localhost:2182,localhost:2183");
        props.put("zookeeper.session.timeout.ms", "400");
        props.put("zookeeper.sync.time.ms", "200");
        props.put("auto.commit.interval.ms", "1000");
        props.put("auto.offset.reset", "smallest");

        return new ConsumerConfig(props);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

打印一下结果,可以验证上述hash分区落地位置的推断:

消费者消费数据: 【分区号: [2], 存储的消息: [key1--value1] 】
消费者消费数据: 【分区号: [2], 存储的消息: [key5--value5] 】
消费者消费数据: 【分区号: [2], 存储的消息: [key9--value9] 】
消费者消费数据: 【分区号: [2], 存储的消息: [key10--value10] 】
消费者消费数据: 【分区号: [0], 存储的消息: [key3--value3] 】
消费者消费数据: 【分区号: [0], 存储的消息: [key7--value7] 】
消费者消费数据: 【分区号: [1], 存储的消息: [key0--value0] 】
消费者消费数据: 【分区号: [1], 存储的消息: [key4--value4] 】
消费者消费数据: 【分区号: [1], 存储的消息: [key8--value8] 】
消费者消费数据: 【分区号: [3], 存储的消息: [key2--value2] 】
消费者消费数据: 【分区号: [3], 存储的消息: [key6--value6]

猜你喜欢

转载自blog.csdn.net/wangshuminjava/article/details/80223314