点上方蓝字,将胖滚猪“设为星标”,拜托拜托~
温故:在【死磕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 partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes == null) {
int nextValue = nextValue(topic);
List 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 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原理分析!无锁原子类解决并发问题
原创声明:本文为【胖滚猪学编程】原创博文,转载请注明出处
文章都看完了不写个留言吗
原创不易,养成习惯,点个在看!