【死磕kafka】(二) Kafka消息分区机制的原理及分区策略

点上方蓝字,将胖滚猪“设为星标”,拜托拜托~

温故:在【死磕Kafka系列】第一篇我们了解到:

主题是存储消息的一个逻辑概念,可以简单理解为一类消息的集合。

每个主题又可以划分成多个分区,每个分区存储不同的消息。

当消息添加至分区时,会为其分配一个位移offset(从0开始递增),并保证分区上唯一,消息在分区上的顺序由offset保证,即同一个分区内的消息是有序的。

如下图所示:

为什么主题之下需要有分区的概念呢?有啥用?

分区到底是个什么东西,怎么存储的呢?

生产者生产消息的时候怎样决定消息分配到哪个分区呢?

分区会带来哪些不利影响呢?

本文从以下几个方面为你一一解答:

分区优势

如果不先把分区的优势放上来给你瞅瞅,我觉得你就没劲学了,所以先让我来夸夸它。

????解决伸缩性问题

假如我有一套含1万小块的拼图,一个盒子放不下,我该怎么办?

你会大声告诉我 "拼图是可以拆的呀,分别放在不同的盒子里"

对了,如果拼图不能拆,那即使放不下我也没辙了。所以我要感谢拼图的这个特性:能拆。

同理,一个主题,如果有100TB的消息,一台服务器也存不下,但是如果主题能拆(拆成partition),我就可以通过扩充服务器,解决伸缩性问题。

在大数据时代,一台broker极有可能存不下一个topic的数据,所以把topic分成partition,每个partition都可以放在独立的服务器上,解决伸缩性的问题

????提供负载均衡的能力

负载均衡是啥?还是以拼图为例,如果我来蛮劲,非得把一万块丢一个盒子里,盒子可能会被我搞破。如果均衡到10个盒子里,那么可以长久保存。

同理,如果消息只能往一台服务器发送,每秒1亿次请求都到一台机器上了,那么肯定挂了。如果均衡到100台机器呢,就可以愉快的玩耍了。

不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理。因此可以达到负载均衡的目的。

????更多分区可以提高吞吐量

请务必记住这句话:Topic下的一个分区只能被同一个consumer group下的一个consumer线程来消费,但反之并不成立,即一个consumer线程可以消费多个分区的数据。

因此,consumer的并行度要受到分区数限制。

即使你有100台高配机器,如果只有一个分区,那么你也浪费了99台的资源。

因此,分区越多,可以实现的吞吐量就越高(当然也不是越多越好)。我们可以通过添加新的节点机器来增加整体系统的吞吐量。

????实现业务顺序性

我们在第一篇文章中重点强调了:kafka仅保证分区内的顺序性。

此利用分区也可以实现一些业务级别的消息顺序的问题。比如相同用户号的操作记录必须要保证顺序,那么就可以通过自定义分区来实现。

分区存储机制

每个partition对应于一个文件夹(目录在哪是根据自己的设置),partiton命名规则为topic名称+有序序号,第一个partiton序号从0开始:

partition物理上由多个segment(日志段)组成,partion会被平均分配到多个大小相等segment(段)数据文件中。

partition所在文件夹下存储Segment file的索引和数据文件,分别为后缀".index"和后缀“.log”,此2个文件一一对应,成对出现。

消息被追加写到当前最新的日志段中,当写满了一个日志段后,Kafka 会自动切分出一个新的日志段,类似于log4j的rolling log。

对于传统的message queue而言,一般会删除已经被消费的消息,而Kafka集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要),因此Kafka提供两种策略删除旧数据。一是基于时间,二是基于partition文件大小。在server.properties配置即可。

Kafka 在后台有定时任务会定期地检查老的日志段是否能够被删除,从而实现回收磁盘空间的目的。因此你需要提前规划好消息存储的时间,可别某一天哭着说"我数据没了"哦!

????索引文件

什么是索引文件?这得知道offset是什么。

消息在分区中的位移offset,代表一个偏移量,表示分区中每条消息的位置信息,是一个单调递增且不变的值。分区位移总是从 0 开始,假设一个生产者向一个空分区写入了 10 条消息,那么这 10 条消息的位移依次是 0、1、2、…、9。

索引文件的命名规则就是根据offset。partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。

[root@zk-kafka-02 bin]# ./kafka-dump-log.sh help --files /opt/kafka/logs/lyl_kafka_source-0/00000000000000002610.log      
Dumping /opt/kafka/logs/lyl_kafka_source-0/00000000000000002610.log
Starting offset: 2610

索引文件即是元信息的存储。记录<相对位移,起始地址>映射关系。

# 查看该分片索引文件的前10条记录
bin/kafka-dump-log.sh --files /tmp/kafka-logs/nginx_access_log-1/00000000000003257573.index |head -n 10
Dumping /tmp/kafka-logs/nginx_access_log-1/00000000000003257573.index
offset: 3257687 position: 17413
offset: 3257743 position: 33770
offset: 3257799 position: 50127
offset: 3257818 position: 66484
offset: 3257819 position: 72074
offset: 3257871 position: 87281
offset: 3257884 position: 91444

????数据文件

数据文件就是用来存储消息的。segment data file由许多message组成,下面详细说明message物理结构如下:

了解几个重要参数:

????索引文件与数据文件的关系

既然它们是一一对应成对出现,必然有关系。索引文件中元数据指向对应数据文件中message的物理偏移地址。

以索引文件中元数据3,497为例,依次在数据文件中表示第3个message(在全局partiton表示第368772个message)、以及该消息的物理偏移地址为497。

????在partition中如何通过offset查找message

既然知道了索引文件与数据文件的关系,就可以解决一个经典问题了?如何查找我的message在哪里呢?例如读取offset=368776的message,

1、首先确定segment file。
我们知道segment file命名规则跟offset有关,文件名是上一个segment文件最后一条消息的offset值。

上图为例,其中00000000000000000000.index表示最开始的文件,起始偏移量(offset)为0。第二个文件00000000000000368769.index的消息量起始偏移量为368770 = 368769 + 1.同样,第三个文件00000000000000737337.index的起始偏移量为737338=737337 + 1,其他后续文件依次类推,以起始偏移量命名并排序这些文件,只要根据offset二分查找文件列表,就可以快速定位到具体文件。当offset=368776时定位到00000000000000368769.index|log

2、第二步通过segment file查找message
通过第一步定位到segment file,当offset=368776时,依次定位到00000000000000368769.index的元数据物理位置和00000000000000368769.log的物理偏移地址,然后再通过00000000000000368769.log顺序查找直到offset=368776为止。

segment index file采取稀疏索引存储方式,它减少索引文件大小,通过mmap可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。


???? Kafka高效文件存储设计特点

kafka具有以下几个特性,保证了高效的文件存储:

  • Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。

  • 通过索引信息可以快速定位message。

  • 通过index元数据全部映射到memory,避免segment file的IO磁盘操作。

  • 通过索引文件稀疏存储,大幅降低index文件元数据占用空间大小。

分区策略

所谓分区策略是决定生产者将消息发送到哪个分区的算法。Kafka 为我们提供了默认的分区策略,同时它也支持你自定义分区策略。

???? 默认分区策略

想了解kafka默认分区策略,直接看源码就很清楚了

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 (keyBytes == null) {
int nextValue = nextValue(topic);
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
            } else {
// no partitions are available, give a non-available partition
return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

通过源码我们可以阅读到以下信息:

1、首先kafka会判断有没有key,这里的key就是你为每条消息定义的消息键,发消息的时候在ProducerRecord(String topic, K key, V value)中指定的key。这个 Key 的作用非常大,它可以是一个有着明确业务含义的字符串,比如用户id。

2、如果没有key,会采用轮询策略,也称 Round-robin 策略,即顺序分配。比如一个主题下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2,以此类推。当生产第 4 条消息时又会重新开始上述轮询。轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略。

3、如果有key,那么就按消息键策略,这样可以保证同一个 Key 的所有消息都进入到相同的分区里面,这样就保证了顺序性了。

???? 自定义分区策略

如果要自定义分区策略,你需要显式地配置生产者端的参数:props.put("partitioner.class","kafka.producer.UserPartitioner"); UserPartitioner类需要实现:
org.apache.kafka.clients.producer.Partitioner接口。这个接口也很简单,举例如下:

/**
 * @description:根据用户id分区 相同用户id的数据肯定在一个分区 保证有序性
 */
public class UserPartitioner implements Partitioner {
    @Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        UserDomain userDomain = JSON.parseObject(value.toString(),UserDomain.class);
        Integer keyObj = userDomain.getId();
        List<PartitionInfo> partitionInfoList = cluster.availablePartitionsForTopic(topic);
int partitionCount = partitionInfoList.size();
        System.out.println("keyObj=" + keyObj + " partition="+keyObj % partitionCount);
return keyObj % partitionCount;
    }
}

这里的topic、key、keyBytes、value和valueBytes都属于消息数据,cluster则是集群信息(比如当前 Kafka 集群共有多少主题、多少 Broker 等)。根据自身情况重写partition方法即可。

分区劣势

凡事有利必有弊,分区虽然有负载均衡和高吞吐量的优势,但是它的劣势我们也必须了解下。

首先很容易想到的是,分区需要更多的内存。因此,不适合资源比较紧张的环境中。

其次,分区可能导致不可用性。试想,假如一个broker有1000个leader replica,那么当这个broker非正常停止的情况下,这些leader需要迁移,假设一个需要5ms,那么1000个就需要5s,在这段时间将会有1000个分区不可用。通常,这种故障很少见。但是,如果非常关心可用性,最好将每个代理的分区数限制为两到四千个,而将群集中的分区总数限制为低一万个。

再者,分区可能导致端到端的延迟。Kafka中的端到端延迟指的是生产者发布消息到消费者读取消息的时间。Kafka仅在提交消息后(即,将消息复制到所有同步副本时)才将消息公开给消费者。因此,提交消息的时间是端到端延迟的重要部分。默认情况下,对于在两个broker 之间共享副本的所有分区,Kafka代理仅使用单个线程复制数据。根据实验表明,将1000个分区从一个Broker复制到另一个Broker会增加大约20毫秒的延迟,这意味着端到端延迟至少为20毫秒。对于某些实时应用程序来说,这可能太高了。

因此,劲酒虽好,可不要贪杯哦!合理设置分区的数量,才能达到最佳效果,可以从以下方面考虑,第一:资源是否足够;第二:需要达到什么样的吞吐量;第三:考虑时效性和可用性。

总结

本文从分区的优势出发,让读者先了解了kafka为什么要有分区这个东西的存在,它能给我们带来哪些实际的意义,主要有四点:扩展性、高吞吐量、负载均衡、顺序性。

然后深入讲解了分区的存储机制,让读者对分区这个东西从抽象的概念过渡到真实的感官中,其实分区对应就是服务器上的文件夹,它是有segment file组成,其中又包括索引文件和日志文件。

接下来,再看了kafka具体的分区策略,有消息键策略、轮询策略、也可自定义策略;分区策略决定了生产者消息会发送到哪个分区,分区策略的选择很重要,尤其是在负载均衡和顺序性问题上至关重要。

最后,凡事有利必有弊,劲酒虽好,可不要贪杯!读者也应当了解分区的弊端,合理设置分区的数量,才能达到最佳效果。

(关注【胖滚猪学编程】公众号发送:kafka, 获取kafka全系列完整思维导图)

END

点击查看往期内容回顾

面试官:说出八种消息队列的应用场景

数据中台全景架构及模块解析

【漫画】CAS原理分析!无锁原子类解决并发问题

原创声明:本文为【胖滚猪学编程】原创博文,转载请注明出处

文章都看完了不写个留言吗

原创不易,养成习惯,点个在看!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值