kafka存储

生产者发送流程

image.png producer发送消息到broker后,消息是保存在分区中的,每条消息都是追加到分区末尾的,顺序写入磁盘,所以同一分区内的消息是有序的

image.png 从这里可以看出来,分区可以提高扩展性,我们可以增加分区来应对日益增长的数据量;分区可以提高并发性,以partition为读写,多个消费者可以同时消费数据,提升了吞吐量。

类比服务器负载均衡,我们在向服务器发送请求的时候,服务器会对我们的请求进行负载,将流量分发到不同的服务器。kafka也是一样的。
如果一个topic有多个分区,生产者是怎么将数据发送到哪个partition呢?

有以下几个规则:

  • 发送消息时指定某个分区partition0,则消息会发送到partition0;
  • 如果没有指定partition,设置了消息的key,则会根据key进行hash计算出一个partition发送;
  • 如果没有指定partition,也没设置数据的key,会使用默认的随机策略,随机将消息发送到不同的partition中。

自定义分区器

Kafka 有着默认的分区机制:

  • 如果键值为 null, 则使用轮询 (Round Robin) 算法将消息均衡地分布到各个分区上;
  • 如果键值不为 null,那么 Kafka 会使用内置的散列算法对键进行散列,然后分布到各个分区上。

某些情况下,你可能有着自己的分区需求,这时候可以采用自定义分区器实现。这里给出一个自定义分区器的示例:

3.1 自定义分区器

``

/**
 * 自定义分区器
 */
public class CustomPartitioner implements Partitioner {
    private int passLine;
    @Override
    public void configure(Map<String, ?> configs) {
        /*从生产者配置中获取分数线*/
        passLine = (Integer) configs.get("pass.line");
    }
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, 
                         byte[] valueBytes, Cluster cluster) {
        /*key 值为分数,当分数大于分数线时候,分配到 1 分区,否则分配到 0 分区*/
        return (Integer) key >= passLine ? 1 : 0;
    }
    @Override
    public void close() {
        System.out.println("分区器关闭");
    }
}
复制代码

``

需要在创建生产者时指定分区器,和分区器所需要的配置参数:

``

public class ProducerWithPartitioner {
    public static void main(String[] args) {
        String topicName = "Kafka-Partitioner-Test";
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop001:9092");
        props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        /*传递自定义分区器*/
        props.put("partitioner.class", "com.heibaiying.producers.partitioners.CustomPartitioner");
        /*传递分区器所需的参数*/
        props.put("pass.line", 6);
        Producer<Integer, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i <= 10; i++) {
            String score = "score:" + i;
            ProducerRecord<Integer, String> record = new ProducerRecord<>(topicName, i, score);
            /*异步发送消息*/
            producer.send(record, (metadata, exception) ->
                    System.out.printf("%s, partition=%d, \n", score, metadata.partition()));
        }
        producer.close();
    }
}
复制代码
扫描二维码关注公众号,回复: 13278277 查看本文章

3.2 测试

需要创建一个至少有两个分区的主题:

 bin/kafka-topics.sh --create \
                    --bootstrap-server hadoop001:9092 \
                     --replication-factor 1 --partitions 2 \
                     --topic Kafka-Partitioner-Test​
复制代码

``

此时输入如下,可以看到分数大于等于 6 分的都被分到 1 分区,而小于 6 分的都被分到了 0 分区。

score:6, partition=1, 
score:7, partition=1, 
score:8, partition=1, 
score:9, partition=1, 
score:10, partition=1, 
score:0, partition=0, 
score:1, partition=0, 
score:2, partition=0, 
score:3, partition=0, 
score:4, partition=0, 
score:5, partition=0, 
分区器关闭​
复制代码

``

存储

kafka的数据存储在磁盘中,可能我们会认为写入磁盘是比较耗时的操作,kafka初始会单独开辟一块磁盘空间,顺序写入数据,效率比随机写入高很多。
我们知道,topic可以分为一个或多个partition,partition物理形式就是文件夹,每个分区下面是多组文件,称为segment文件;每组segment文件包含
.index文件、.log文件、.timeindex文件;log文件是实际存储消息的地方,index和timeindex是索引文件,用于检索消息。

image.png 如上图,有n组segment文件,每个log文件大小是一样的,单存储的消息数量不一定一样的,每条消息的大小不一样,文件的命名是以该组最小的offset命名的,如0.index存储offset 0-372855的消息,我们会发现,每组的文件名是单调递增的,可以通过2分法来查询,kafka就是利用分段+索引的方式来提高查询效率。
log文件里存储实际的消息,消息主要包含消息体,消息大小,offset,压缩类型等。
offset:一个8字节有序数,可以认为是一个指针,可以唯一确定一个消息在partition的位置。
消息大小:描述消息的大小,4字节。
消息体:存放实际的消息数据

存储策略

消息一直堆积在消息队列中,无论消息是否被消费,kafka会保存所有的消息,但如果一直存储,肯定也是不行的(成本),kafka有相应的过期删除策略;

  • 基于时间,默认是168小时,也就是7天过期;
  • 基于大小,默认是1073741824(1gb)。

kafka读取特定消息的时间复杂度是O(1),删除数据并不会提高性能。

消费数据

消费者订阅topic,就可以消费生产者发送到topic的数据了;消费者主动取leader节点拉取消息。多个消费者可以组成一个消费组,每个消费组都有一个组id,同一个消费组的消费者可以消费同一topic下的不同分区的消息,同一topic下的同一分区只能被同一消费组内的一个消费者消费。
如下图所示:

image.png

我们知道消费者消费partition中的数据,那是怎么查找要消费的数据呢?前面提到partition对应了文件夹,每组segment都包含了index、log文件;消费者消费的时候会带上offset来拉取数据,而offset可以唯一确定一条消息在partition中的位置,即只要根据offset来定位目标位置获取数据,那问题就变成如何通过offset来查找要消费的消息。

image.png

如图所示,如果我们要查offset=11的消息,文件名是以当前segment的最小偏移量设置的,segment文件的数字都是递增的,使用2分查找定位到消息在segment2,即10.index,继续进行2分查找查询到11的位置是pos=110(即消息在log文件的位置),这样就可以快速定位消息在log中的位置了;基本思路是利用分段+有序+2分查找算法高效查找数据。

猜你喜欢

转载自juejin.im/post/7032620019267665956