目录
Kafka背景
Kafka简介
Kafka层级与api架构
Kafka基础概念
生产者,消费者,队列
partition,broker,集群
消费者组,offset,Zookeeper
Kafka整体架构
Kafka底层文件结构
数据日志文件
偏移量索引文件
稀疏索引
时间戳索引文件
根据offset查找消息
根据时间戳查找消息
相关配置
segment切分
日志段的写入
日志段的读取
关于 Kafka 和其他消息队列的区别
Kafka与CAP原理
使用ISR方案的原因
注意:本文参考 《浅入浅出》-Kafka
Kafka 学习笔记(一) :为什么需要 Kafka? - ScalaCool
Kafka 架构设计
《我想进大厂》之kafka夺命连环11问
Kafka消息底层存储结构介绍_刘Java的博客-CSDN博客_kafka底层存储
《面试八股文》之kafka21卷 - 知乎
面试官:你对Kafka比较熟? 那说说kafka日志段如何读写的吧?
Kafka常见面试题总结 | JavaGuide
首先我们得去官网看看是怎么介绍Kafka的:
https://kafka.apache.org/intro
Kafka最早是由LinkedIn公司开发的,作为其自身业务消息处理的基础,后LinkedIn公司将Kafka捐赠给Apache,现在已经成为Apache的一个顶级项目了,Kafka作为一个高吞吐的分布式的消息系统,目前已经被很多公司应用在实际的业务中了,并且与许多数据处理框架相结合,比如Hadoop,Spark等。
在实际的业务需求中,我们需要处理各种各样的消息,比如Page View,日志,请求等,那么一个好的消息系统应该拥有哪些功能呢?
拥有消息发布和订阅的功能,类似于消息队列或者企业消息传送系统;
能存储消息流,并具备容错性;
能够实时的处理消息;
以上3点是作为一个好的消息系统的最基本的能力。
那么Kafka为什么会诞生呢?
其实在我们工作中,相信有很多也接触过消息队列,甚至自己也写过简单的消息系统,它最基本应该拥有发布/订阅的功能,如下图所示:
其中消费者A与消费者B都订阅了消息源A和消息源B,这种模式很简单,但是相对来说也有弊端,比如以下两点:
该模式下消费者需要实时去处理消息,因为这里消息源和消费者都不会维护一个消息队列(维护代价太大),这将会导致消费者若是暂时没有能力消费,则消息会丢失,当然也就不能获得历史的消息;
消息源需要维护原本不属于它的工作,比如维护订阅者(消费者)的信息,向多个消费者发送消息,亦或者有些还需要处理消息反馈,这是原本纯粹的消息源就会变得越来越复杂;
当然这些问题都是可以改进的,比如我们可以在消息源和消费者中间增加一个消息队列,如下图所示:
从图中我们可以看出,现在消息源只需要将消息发送到消息队列中就行,至于其他就将给消息队列去完成,我们可以在消息队列持久化消息,主动推消息给已经订阅了该消息队列的消费者,那么这种模式还有什么缺点吗?
答案是有,上图只是两个消息队列,我们维护起来并不困难,但是如果有成百上千个呢?那不得gg,其实我们可以发现,消息队列的功能都很类似,无非就是持久化消息,推送消息,给出反馈等功能,结构也非常类似,主要是消息内容,当然如果要通用化,消息结构也要尽可能通用化,与具体平台具体语言无关,比如用JSON格式等,所有我们可以演变出以下的消息系统:
这个方式看起来只是把上面的队列合并到了一起,其实并不那么简单,因为这个消息队列集合要具备以下几个功能:
能统一管理所有的消息队列,不是特殊需求不需要开发者自己去维护;
高效率的存储消息;
消费者能快速的找到想要消费的消息;
当然这些只是最基本的功能,还有比如多节点容错,数据备份等,一个好的消息系统需要处理的东西非常多,很庆幸,Kafka帮我们做到了。
kafka是一个流式数据处理平台,他具有消息系统的能力,也有实时流式数据处理分析能力,只是我们更多的偏向于把他当做消息队列系统来使用。
主要功能体现于三点:
消息系统:kafka与传统的消息中间件都具备系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。与此同时,kafka还提供了大多数消息系统难以实现的消息顺序性保障及回溯性消费的功能。
存储系统:kafka把消息持久化到磁盘,相比于其他基于内存存储的系统而言,有效的降低了消息丢失的风险。这得益于其消息持久化和多副本机制。也可以将kafka作为长期的存储系统来使用,只需要把对应的数据保留策略设置为“永久”或启用主题日志压缩功能。
流式处理平台:kafka为流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理框架,比如窗口、连接、变换和聚合等各类操作。
Kafka 主要有两大应用场景:
消息队列 :建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。
数据处理: 构建实时的流数据处理程序来转换或处理数据流。
如果说按照容易理解来分层的话,大致可以分为3层:
第一层是Zookeeper,相当于注册中心,他负责kafka集群元数据的管理,以及集群的协调工作,在每个kafka服务器启动的时候去连接到Zookeeper,把自己注册到Zookeeper当中
第二层里是kafka的核心层,这里就会包含很多kafka的基本概念在内:
record:代表消息
topic:主题,消息都会由一个主题方式来组织,可以理解为对于消息的一个分类
producer:生产者,负责发送消息
consumer:消费者,负责消费消息
broker:kafka服务器
partition:分区,主题会由多个分区组成,通常每个分区的消息都是按照顺序读取的,不同的分区无法保证顺序性,分区也就是我们常说的数据分片sharding机制,主要目的就是为了提高系统的伸缩能力,通过分区,消息的读写可以负载均衡到多个不同的节点上
Leader/Follower:分区的副本。为了保证高可用,分区都会有一些副本,每个分区都会有一个Leader主副本负责读写数据,Follower从副本只负责和Leader副本保持数据同步,不对外提供任何服务
offset:偏移量,分区中的每一条消息都会根据时间先后顺序有一个递增的序号,这个序号就是offset偏移量
Consumer group:消费者组,由多个消费者组成,一个组内只会由一个消费者去消费一个分区的消息
Coordinator:协调者,主要是为消费者组分配分区以及重平衡Rebalance操作
Controller:控制器,其实就是一个broker而已,用于协调和管理整个Kafka集群,他会负责分区Leader选举、主题管理等工作,在Zookeeper第一个创建临时节点/controller的就会成为控制器
第三层则是存储层,用来保存kafka的核心数据,他们都会以日志的形式最终写入磁盘中。
Kafka是运行在一个集群上,所以它可以拥有一个或多个服务节点;
Kafka集群将消息存储在特定的文件中,对外表现为Topics;
每条消息记录都包含一个key,消息内容以及时间戳;
从上面几点我们大致可以推测Kafka是一个分布式的消息存储系统,那么它就仅仅这么点功能吗,我们继续看下面。
Kafka为了拥有更强大的功能,提供了四大核心接口:
Producer API允许了应用可以向Kafka中的topics发布消息;
Consumer API允许了应用可以订阅Kafka中的topics,并消费消息;
Streams API允许应用可以作为消息流的处理者,比如可以从topicA中消费消息,处理的结果发布到topicB中;
Connector API提供Kafka与现有的应用或系统适配功能,比如与数据库连接器可以捕获表结构的变化;
它们与Kafka集群的关系可以用下图表示:
众所周知,Kafka是一个消息队列,把消息放到队列里边的叫生产者,从队列里边消费的叫消费者。
一个消息中间件,队列不单单只有一个,我们往往会有多个队列,而我们生产者和消费者就得知道:把数据丢给哪个队列,从哪个队列消息。我们需要给队列取名字,叫做topic(相当于数据库里边表的概念)
现在我们给队列取了名字以后,生产者就知道往哪个队列丢数据了,消费者也知道往哪个队列拿数据了。我们可以有多个生产者往同一个队列(topic)丢数据,多个消费者往同一个队列(topic)拿数据
为了提高一个队列(topic)的吞吐量,Kafka会把topic进行分区(Partition)
所以,生产者实际上是往一个topic名为Java3y中的分区(Partition)丢数据,消费者实际上是往一个topic名为Java3y的分区(Partition)取数据
一台Kafka服务器叫做Broker,Kafka集群就是多台Kafka服务器:
一个topic会分为多个partition,实际上partition会分布在不同的broker中,举个例子:
由此得知:Kafka是天然分布式的。
现在我们已经知道了往topic里边丢数据,实际上这些数据会分到不同的partition上,这些partition存在不同的broker上。分布式肯定会带来问题:“万一其中一台broker(Kafka服务器)出现网络抖动或者挂了,怎么办?”
Kafka是这样做的:我们数据存在不同的partition上,那kafka就把这些partition做备份。比如,现在我们有三个partition,分别存在三台broker上。每个partition都会备份,这些备份散落在不同的broker上。
红色块的partition代表的是主分区,紫色的partition块代表的是备份分区。生产者往topic丢数据,是与主分区交互,消费者消费topic的数据,也是与主分区交互。
备份分区仅仅用作于备份,不做读写。如果某个Broker挂了,那就会选举出其他Broker的partition来作为主分区,这就实现了高可用。
另外值得一提的是:当生产者把数据丢进topic时,我们知道是写在partition上的,那partition是怎么将其持久化的呢?(不持久化如果Broker中途挂了,那肯定会丢数据嘛)。
Kafka是将partition的数据写在磁盘的(消息日志),不过Kafka只允许追加写入(顺序访问),避免缓慢的随机 I/O 操作。
Kafka也不是partition一有数据就立马将数据写到磁盘上,它会先缓存一部分,等到足够多数据量或等待一定的时间再批量写入(flush)。
上面balabala地都是讲生产者把数据丢进topic是怎么样的,下面来讲讲消费者是怎么消费的。既然数据是保存在partition中的,那么消费者实际上也是从partition中取数据。
生产者可以有多个,消费者也可以有多个。像上面图的情况,是一个消费者消费三个分区的数据。多个消费者可以组成一个消费者组。
本来是一个消费者消费三个分区的,现在我们有消费者组,就可以每个消费者去消费一个分区(也是为了提高吞吐量)
按图上所示的情况,这里想要说明的是:
如果消费者组中的某个消费者挂了,那么其中一个消费者可能就要消费两个partition了
如果只有三个partition,而消费者组有4个消费者,那么一个消费者会空闲
如果多加入一个消费者组,无论是新增的消费者组还是原本的消费者组,都能消费topic的全部数据。(消费者组之间从逻辑上它们是独立的)
有的同学可能会产生疑问:消费者是怎么知道自己消费到哪里的呀?Kafka不是支持回溯吗?那是怎么做的呀?
比如上面也提到:如果一个消费者组中的某个消费者挂了,那挂掉的消费者所消费的分区可能就由存活的消费者消费。那存活的消费者是需要知道挂掉的消费者消费到哪了,不然怎么玩。
这里要引出offset
了,Kafka就是用offset
来表示消费者的消费进度到哪了,每个消费者会都有自己的offset
。说白了offset
就是表示消费者的消费进度。
在以前版本的Kafka,这个offset
是由Zookeeper来管理的,后来Kafka开发者认为Zookeeper不合适大量的删改操作,于是把offset
在broker以内部topic(__consumer_offsets
)的方式来保存起来。
每次消费者消费的时候,都会提交这个offset
,Kafka可以让你选择是自动提交还是手动提交。
既然提到了Zookeeper,那就多说一句。Zookeeper虽然在新版的Kafka中没有用作于保存客户端的offset
,但是Zookeeper是Kafka一个重要的依赖。
探测broker和consumer的添加或移除。
负责维护所有partition的领导者/从属者关系(主分区和备份分区),如果主分区挂了,需要选举出备份分区作为主分区。
维护topic、partition等元配置信息
1、Kafka 为实时日志流而生,要处理的并发和数据量非常大。可见,Kafka 本身就是一个高并发系统,它必然会遇到高并发场景下典型的三高挑战:高性能、高可用和高扩展。
2、为了简化实现的复杂度,Kafka 最终采用了很巧妙的消息模型:它将所有消息进行了持久化存储,让消费者自己各取所需,想取哪个消息,想什么时候取都行,只需要传递一个消息的 offset 进行拉取即可。
最终 Kafka 将自己退化成了一个「存储系统」。因此,海量消息的存储问题就是 Kafka 架构设计中的最大技术难点。
下面我们再接着分析下:Kafka 究竟是如何解决存储问题的?
面对海量数据,单机的存储容量和读写性能肯定有限,大家很容易想到一种存储方案:对数据进行分片存储。这种方案在我们实际工作中也非常常见:
1、比如数据库设计中,当单表的数据量达到几千万或者上亿时,我们会将它拆分成多个库或者多张表。
2、比如缓存设计中,当单个 Redis 实例的数据量达到几十个 G 引发性能瓶颈时,我们会将单机架构改成分片集群架构。
类似的拆分思想在 HDFS、ElasticSearch 等中间件中都能看到。
Kafka 也不例外,它同样采用了这种水平拆分方案。在 Kafka 的术语中,拆分后的数据子集叫做 Partition(分区),各个分区的数据合集即全量数据。
我们再来看下 Kafka 中的 Partition 具体是如何工作的?举一个很形象的例子,如果我们把「Kafka」类比成「高速公路」:
1、当大家听到京广高速的时候,知道这是一条从北京到广州的高速路,这是逻辑上的叫法,可以理解成 Kafka 中的 Topic(主题)。
2、一条高速路通常会有多个车道进行分流,每个车道上的车都是通往一个目的地的(属于同一个Topic),这里所说的车道便是 Partition。
这样,一条消息的流转路径就如下图所示,先走主题路由,然后走分区路由,最终决定这条消息该发往哪个分区。
其中分区路由可以简单理解成一个 Hash 函数,生产者在发送消息时,完全可以自定义这个函数来决定分区规则。如果分区规则设定合理,所有消息将均匀地分配到不同的分区中。
通过这样两层关系,最终在 Topic 之下,就有了一个新的划分单位:Partition。先通过 Topic 对消息进行逻辑分类,然后通过 Partition 进一步做物理分片,最终多个 Partition 又会均匀地分布在集群中的每台机器上,从而很好地解决了存储的扩展性问题。
因此,Partition 是 Kafka 最基本的部署单元。本文之所以将 Partition 称作 Kafka 架构设计的任督二脉,基于下面两点原因:
1、Partition 是存储的关键所在,MQ「一发一存一消费」的核心流程必然围绕它展开。
2、Kafka 高并发设计中最难的三高问题都能和 Partition 关联起来。
因此,以 Partition 作为根,能很自然地联想出 Kafka 架构设计中的各个知识点,形成可靠的知识体系。
下面,请大家继续跟着我的思路,以 Partition 为线索,对 Kafka 的宏观架构进行解析。
接下来,我们再看看 Partition 的分布式能力究竟是如何实现的?它又是怎么和 Kafka 的整体架构关联起来的?
前面讲过 Partition 是 Topic 之下的一个划分单位,它是 Kafka 最基本的部署单元,它将决定 Kafka 集群的组织方式。
假设现在有两个 Topic,每个 Topic 都设置了两个 Partition,如果 Kafka 集群是两台机器,部署架构将会是下面这样:
可以看到:同一个 Topic 的两个 Partition 分布在不同的消息服务器上,能做到消息的分布式存储了。但是对于 Kafka 这个高并发系统来说,仅存储可扩展还不够,消息的拉取也必须并行才行,否则会遇到极大的性能瓶颈。
那我们再看看消费端,它又是如何跟 Partition 结合并做到并行处理的?
从消费者来看,首先要满足两个基本诉求:
1、广播消费能力:同一个 Topic 可以被多个消费者订阅,一条消息能够被消费多次。
2、集群消费能力:当消费者本身也是集群时,每一条消息只能分发给集群中的一个消费者进行处理。
为了满足这两点要求,Kafka 引出了消费组的概念,每个消费者都有一个对应的消费组,组间进行广播消费,组内进行集群消费。此外,Kafka 还限定了:每个 Partition 只能由消费组中的一个消费者进行消费。
最终的消费关系如下图所示:假设主题 A 共有 4 个分区,消费组 2 只有两个消费者,最终这两个消费组将平分整个负载,各自消费两个分区的消息。
如果要加快消息的处理速度,该如何做呢?也很简单,向消费组 2 中增加新的消费者即可,Kafka 将以 Partition 为单位重新做负载均衡。当增加到 4 个消费者时,每个消费者仅需处理 1 个 Partition,处理速度将提升两倍。
到这里,存储可扩展、消息并行处理这两个难题都解决了。但是高并发架构设计上,还遗留了一个很重要的问题:那就是高可用设计。
在 Kafka 集群中,每台机器都存储了一些 Partition,一旦某台机器宕机,上面的数据不就丢失了吗?
此时,你一定会想到对消息进行持久化存储,但是持久化只能解决一部分问题,它只能确保机器重启后,历史数据不丢失。但在机器恢复之前,这部分数据将一直无法访问。这对于高并发系统来说,是无法忍受的。
所以 Kafka 必须具备故障转移能力才行,当某台机器宕机后仍然能保证服务可用。
如果大家去分析任何一个高可靠的分布式系统,比如 ElasticSearch、Redis Cluster,其实它们都有一套多副本的冗余机制。
没错,Kafka 正是通过 Partition 的多副本机制解决了高可用问题。在 Kafka 集群中,每个 Partition 都有多个副本,同一分区的不同副本中保存的是相同的消息。
副本之间是 “一主多从” 的关系,其中 leader 副本负责读写请求,follower 副本只负责和 leader 副本同步消息,当 leader 副本发生故障时,它才有机会被选举成新的 leader 副本并对外提供服务,否则一直是待命状态。
现在,我假设 Kafka 集群中有 4 台服务器,主题 A 和主题 B 都有两个 Partition,且每个 Partition 各有两个副本,那最终的多副本架构将如下图所示:
很显然,这个集群中任何一台机器宕机,都不会影响 Kafka 的可用性,数据仍然是完整的。
理解了上面这些内容,最后我们再反过来看下 Kafka 的整体架构:
1、Producer:生产者,负责创建消息,然后投递到 Kafka 集群中,投递时需要指定消息所属的 Topic,同时确定好发往哪个 Partition。
2、Consumer:消费者,会根据它所订阅的 Topic 以及所属的消费组,决定从哪些 Partition 中拉取消息。
3、Broker:消息服务器,可水平扩展,负责分区管理、消息的持久化、故障自动转移等。
4、Zookeeper:负责集群的元数据管理等功能,比如集群中有哪些 broker 节点以及 Topic,每个 Topic 又有哪些 Partition 等。
很显然,在 Kafka 整体架构中,Partition 是发送消息、存储消息、消费消息的纽带。吃透了它,再去理解整体架构,脉络会更加清晰。
总结:
1、Kafka 通过巧妙的模型设计,将自己退化成一个海量消息的存储系统。
2、为了解决存储的扩展性问题,Kafka 对数据进行了水平拆分,引出了 Partition(分区),这是 Kafka 部署的基本单元,同时也是 Kafka 并发处理的最小粒度。
3、对于一个高并发系统来说,还需要做到高可用,Kafka 通过 Partition 的多副本冗余机制进行故障转移,确保了高可靠。
向kafka的某个topic中写消息,实际上就是向内部的某个partition写消息,读取消息也是从某个partition上读取消息。
一个topic的实际物理存储实际上对应着一个个的文件夹,文件夹名字是按照topic-num进行目录划分的,topic就是topic名字,num就是partition编号,从0开始。
partition文件夹下面也不仅仅是一个日志文件,而是有多个文件,这些多个文件可以从逻辑上将文件名一致的文件集合就称为一个 LogSegment日志段文件组。partion相当于一个巨型文件,被平均分配到多个大小相等的LogSegment数据文件中,每个LogSegment内部的消息数量不一定相等,这种分段存储的特性特性方便旧的LogSegment file快速被删除,同时加快了文件查找速度。
每个逻辑segment段主要包含:消息日志文件(以log结尾)、偏移量索引文件(以index结尾)、时间戳索引文件(以timeindex结尾)、快照文件(以.snaphot结尾)等等文件。
如下表示一个某个topic文件夹下的主要文件结构:
每组 LogSegment 都有一个基准偏移量,用来表示当前 LogSegment 中第一条消息的 offset。消息offset是一个 64 位的长整形数,固定20位十进制整数,长度未达到,用 0 进行填补。每个partition的消息offset单独维护,一个partition下的全局offset从0开始。
换个角度来看,每组 LogSegment 的基准偏移量就是上一组LogSegment的最大消息偏移量+1,每一个LogSegment的索引文件和日志文件都由自己的基准偏移量作为文件名,因此第一组LogSegment的索引文件和日志文件名都是00000000000000000000。
log文件作为日志文件,存储了消息内容以及该消息相关的元数据。Kafka会将消息以追加的方式顺序持久化到partition下面的最新的日志段下面的日志文件中,即只有最后一个logSegment文件才能执行写入操作。
每一个条消息日志的主要包含offset,MessageSize,data三个属性(还有一些其他属性):
offset :8字节,表示Message在这个partition中的全局偏移量,这是一个逻辑值而不是实际无力偏移量,它唯一确定了partition中的一条Message所在的逻辑位置,可以看作是partition中Message的id,offset从0开始。
MessageSize :4字节,表示消息内容的大小;
data :Message的具体内容,大小不固定。
index文件作为偏移量索引文件,主要用于加快查找消息的速度。该文件中的每一条索引记录都对应着log文件中的一条消息记录。
一条索引记录包含相对offset和position两个属性(均为4字节):
相对offset :因为数据文件分段以后,每个数据文件的起始offset不为0,相对offset表示这条Message相对于其对应的数据文件中最小offset(也就是基准offset)的大小。举例,分段后的一个数据文件的offset是从20开始,那么offset为25的Message在index文件中的相对offset就是25-20 = 5。存储相对offset可以减小索引文件占用的空间。
position :表示该条Message在数据文件中的物理位置。只要打开对应log文件并移动文件指针到这个position就可以读取对应的Message内容了。
相对offset大小为4字节,因此最大值为Integer.MAX_VALUE。在写入消息时会对要写入的相对offset进行校验,超过该值时将会自动进行日志段切分。
假设某个LogSegment段中的数据文件名1234.log,索引文件中某个索引条目为(3,497)为例,那么这个索引条目对应的消息就是在数据文件中的第4个消息,该消息全局offset为1238,该消息的物理偏移地址(相对数据文件)为497。
index索引文件中并没有为数据文件中的每条Message建立索引,而是采用了稀疏索引,默认每间隔4k的数据建立一条索引,时间戳索引文件(以timeindex结尾)也是这个规则。通过log.index.interval.bytes属性可以更新间隔数量大小。
稀疏索引避免了索引文件占用过多的空间,从而可以将索引文件长期保留在内存中,但缺点是没有建立索引的Message也不能一次性定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。
某个index文件内容可能如下:
offset:0 position:0
offset:20 position:320
offset:43 position:1220
Kafka还采用了mmap的方式直接将 index 文件映射到内存中(Java中就是MappedByteBuffer),这样对 index 的操作就不需要操作磁盘 IO,大大的减少了磁盘IO次数和时间。
和index偏移量索引文件一样,timeindex时间戳索引文件也是采用稀疏索引,默认每写入4k数据量的间隔记录一次时间索引项。
每个时间戳索引分为2部分,共占12个字节:
timestamp :当前日志分段文件中建立索引的消息的时间戳;
relativeoffset :时间戳对应消息的相对偏移量;
某个timeindex文件内容可能如下:
timestamp: 1627378362898 relativeoffset:0
timestamp: 1627378362998 relativeoffset:20
timestamp: 1627378363198 relativeoffset:43
假设kafka要根据offset来查找消息:
根据 offset 的值,查找对应的index 索引文件。由于索引文件命名是以该文件的第一个绝对offset 进行命名的,所以,使用二分查找能够根据offset 快速定位到对应的索引文件。
找到index索引文件后,根据相对offset进行定位,找到索引文件中小于等于当前offset对应的相对offset的最大索引,并得到position,即message的物理偏移地址。比如该offset的相对偏移量为100,而索引文件中存在的相对偏移量有(50,80,110),那么最后找到的就是相对偏移量为80的消息的物理偏移地址。
得到position以后,再到对应的log文件中,直接从对应position位置开始,查找offset对应的消息,向后依次顺序遍历,将每条消息的offset与目标offset进行比较,直到找到消息。
可以看到,由于index是稀疏索引,因为可能在索引文件中并不能直接定位到所要查找的消息的位置,还是可能需要去log文件中做一次遍历查找,但是这次扫描的范围就很小了。
如果我们根据时间戳来查找消息,那么我们需要横跨三个文件:timeindex时间戳索引文件,index偏移量索引文件,log日志数据文件。因此相比于使用offset来查找消息要更慢一些。
假设我们需要根据1627378362999这个时间戳来查询消息。
将 1627378362999和每个日志分段中最大时间戳 largestTimeStamp 逐一对比,直到找到一个不小于1627378362999的日志分段。日志分段中的 largestTimeStamp 的计算是先查询该日志分段所对应时间戳索引文件,找到最后一条索引项,若最后一条索引项的时间戳字段值大于 0 ,则取该值,否则去查找该日志分段的最近修改时间。
找到相应日志分段之后,使用二分法进行定位,与偏移量索引方式类似,找到不大于 1627378362999最大索引项,也就是 [1627378362998 20]。
拿着偏移量为 320到偏移量索引文件中使用二分法找到不大于 20最大索引项,即 [20,320] 。
日志文件中从 320 的物理位置开始顺序查找时间戳不小于1627378362998数据。
配置项 | 默认值 | 说明 |
log.index.interval.bytes | 4096 (4K) | 增加索引项的写入日志字节间隔大小, 该值会影响索引文件中的区间密度和查询效率 |
log.segment.bytes | 1073741824 (1G) | 一个日志段的日志文件最大大小 |
log.roll.ms | 当前日志分段中消息的最大时间戳与 当前系统的时间戳的差值允许的最大范围,毫秒维度 |
|
log.roll.hours | 168 (7天) | 当前日志分段中消息的最大时间戳与 当前系统的时间戳的差值允许的最大范围,小时维度 |
log.index.size.max.bytes | 10485760 (10MB) | 触发偏移量索引文件或 时间戳索引文件分段字节限额 |
当日志分段文件的大小超过broker参数log.segment.bytes,默认值为1073741824,即1GB。
当日志分段中的最大时间戳与当前系统的差值大于log.roll.ms或log.roll.hours,前者优先级高。默认只配置了log.roll.hours,值为168,即7天。
偏移量索引文件或者时间戳索引文件大小超过broker参数log.index.size.max.bytes,默认大小为10485760,即10MB。
新追加消息的offset与当前日志段的基础offset(baseOffset)插值大于Integer.MAX_VALUE时,也就是相对位移量超过了最大值。
1、判断下当前日志段是否为空,空的话记录下时间,来作为之后日志段的切分依据
2、确保位移值合法,最终调用的是AbstractIndex.toRelative(..)
方法,即使判断offset是否小于0,是否大于int最大值。
3、append消息,实际上就是通过FileChannel
将消息写入,当然只是写入内存中及页缓存,是否刷盘看配置。
4、更新日志段最大时间戳和最大时间戳对应的位移值。这个时间戳其实用来作为定期删除日志的依据
5、更新索引项,如果需要的话(bytesSinceLastIndexEntry > indexIntervalBytes)
最后再来个流程图
1、根据第一条消息的offset,通过OffsetIndex
找到对应的消息所在的物理位置和大小。
2、获取LogOffsetMetadata
,元数据包含消息的offset、消息所在segment的起始offset和物理位置
3、判断minOneMessage
是否为true
,若是则调整为必定返回一条消息大小,其实就是在单条消息大于maxSize
的情况下得以返回,防止消费者饿死
4、再计算最大的fetchSize
,即(最大物理位移-此消息起始物理位移)和adjustedMaxSize
的最小值(这波我不是很懂,因为以上一波操作adjustedMaxSize
已经最小为一条消息的大小了)
5、调用 FileRecords
的 slice
方法从指定位置读取指定大小的消息集合,并且构造FetchDataInfo
返回
再来个流程图:
Kafka 的时候就已经默认它是一个非常优秀的消息队列了,我们也会经常拿它跟 RocketMQ、RabbitMQ 对比。我觉得 Kafka 相比其他消息队列主要的优势如下:
1 极致的性能 :基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。
2 生态系统兼容性无可匹敌 :Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。
实际上在早期的时候 Kafka 并不是一个合格的消息队列,早期的 Kafka 在消息队列领域就像是一个衣衫褴褛的孩子一样,功能不完备并且有一些小问题比如丢失消息、不保证消息可靠性等等。当然,这也和 LinkedIn 最早开发 Kafka 用于处理海量的日志有很大关系,哈哈哈,人家本来最开始就不是为了作为消息队列滴,谁知道后面误打误撞在消息队列领域占据了一席之地。
随着后续的发展,这些短板都被 Kafka 逐步修复完善。所以,Kafka 作为消息队列不可靠这个说法已经过时
Kafka的设计目标是高吞吐量,那它与其它消息队列的区别就显而易见了:
1、Kafka操作的是序列文件I / O(序列文件的特征是按顺序写,按顺序读),为保证顺序,Kafka强制点对点的按顺序传递消息,这意味着,一个consumer在消息流(或分区)中只有一个位置。
2、Kafka不保存消息的状态,即消息是否被“消费”。一般的消息系统需要保存消息的状态,并且还需要以随机访问的形式更新消息的状态。而Kafka 的做法是保存Consumer在Topic分区中的位置offset,在offset之前的消息是已被“消费”的,在offset之后则为未“消费”的,并且offset是可以任意移动的,这样就消除了大部分的随机IO。
3、Kafka支持点对点的批量消息传递。
4、Kafka的消息存储在OS pagecache(页缓存,page cache的大小为一页,通常为4K,在Linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问)。
RabbitMQ:分布式,支持多种MQ协议,重量级
ActiveMQ:与RabbitMQ类似
ZeroMQ:以库的形式提供,使用复杂,无持久化
Redis:单机、纯内存性好,持久化较差
Kafka:分布式,消息不是使用完就丢失【较长时间持久化】,吞吐量高【高性能】,轻量灵活
ISR实现CAP中,可用性A和数据一致性C的动态平衡
CAP理论是指,分布式系统中,一致性、可用性和分区容忍性最多只能同时满足两个。
首先看一下CAP理论,以及常见的C和A怎么做平衡:
一致性
通过某个节点的写操作结果对后面通过其它节点的读操作可见
如果更新数据后,并发访问情况下后续读操作可立即感知该更新,称为强一致性
如果允许之后部分或者全部感知不到该更新,称为弱一致性
若在之后的一段时间(通常该时间不固定)后,一定可以感知到该更新,称为最终一致性
可用性
任何一个没有发生故障的节点必须在有限的时间内返回合理的结果
分区容忍性
部分节点宕机或者无法与其它节点通信时,各分区间还可保持分布式系统的功能
一般而言都要求保证分区容忍性。所以在CAP理论下,更多的是需要在可用性和一致性之间做权衡。
常用数据复制及一致性方案
Master-Slave
RDBMS的读写分离即为典型的Master-Slave方案
同步复制可保证强一致性但会影响可用性
异步复制可提供高可用性但会降低一致性
再看看kafka如何实现的C和A的平衡:
Kafka的数据复制方案接近于上文所讲的Master-Slave方案。不同的是,Kafka既不是完全的同步复制,也不是完全的异步复制,而是基于ISR的动态复制方案。
每个Partition的Leader都会维护这样一个列表,该列表中,包含了所有与之同步的Replica(包含Leader自己)。每次数据写入时,只有ISR中的所有Replica都复制完,Leader才会将其置为Commit,它才能被Consumer所消费。
这种方案,与同步复制非常接近。但不同的是,这个ISR是由Leader动态维护的。如果Follower不能紧“跟上”Leader,它将被Leader从ISR中移除,待它又重新“跟上”Leader后,会被Leader再次加加ISR中。每次改变ISR后,Leader都会将最新的ISR持久化到Zookeeper中。
由于Leader可移除不能及时与之同步的Follower,故与同步复制相比可避免最慢的Follower拖慢整体速度,也即ISR提高了系统可用性。
ISR中的所有Follower都包含了所有Commit过的消息,而只有Commit过的消息才会被Consumer消费,故从Consumer的角度而言,ISR中的所有Replica都始终处于同步状态,从而与异步复制方案相比提高了数据一致性。
ISR可动态调整,极限情况下,可以只包含Leader,极大提高了可容忍的宕机的Follower的数量。与Majority Quorum方案相比,容忍相同个数的节点失败,所要求的总节点数少了近一半