kafka技术内幕读书笔记(六):存储层

前言:

Kafka是一个分布式的( distributed )、分区的( partitioned )、复制的( replicated )提交日志( commitlog )服务。“分布式”是所有分布式系统的特性;“分区”指消息会按照分区分布在集群的所有节点上;“复制”指每个分区都会有多个剧本存储在不同的节点上;“提交日志”指新的消息总是以追加的方式进行存储。

日志的读写:
分区的每个副本存储在不同的消息代理节点上,每个副本都对应一个日志。在将分区存储到底层文件系统上时,每个分区对应一个目录,分区目录下有多个日志分段( LogSegment ) 。同一个目录下,所有的日志分段都属于同一个分区。

每个日志分段在物理上由一个数据文件和一个索引文件组成。数据文件存储的是消息的真正内容,索引文件存储的是数据文件的索引信息。为数据文件建立索引文件的目的是更快地访问数据文件。

写入日志:

服务端将生产者产生的消息集存储到日志文件,要考虑对消息集进行分段存储。如图6-3 所示,服务端将消息追加到日志变件,并不是直接写入底层的文件,具体步骤如下。
(1) 每个分区对应的日志对象管理了分区的所有日志分段。
(2)将消息集迫加到当前活动的日志分段,任何时刻,都只会有一个活动的日志分段。
(3)每个日志分段对应一个数据文件和索引文件,消息内容会追加到数据文件中。
(4)操作底层数据的接口是文件通道,消息集提供一个WriteFullyTo () 方法,参数是文件通道。
(5)消息集( ByteBufferMessageSet )的WriteFullyTo() 方法,调用文件通道的Write ()方法, 将底
层包含消息内容的字节缓冲区( ByteBuffer)写到文件通道中。
(6) 字节缓冲区写到文件通道中,消息就持久化到日志分段对应的数据文件中了。

1. 消息集

如图6-6 (左)所示,消息集的WriteMessage () 方法将每条消息( Message )填充到字节缓冲区中,缓冲区会暂存每个分区的一批消息。这个方法实际上是在客户端调用的,填充消息时,会为这批消息设置从0 开始递增的偏移量。如图6-6 (右)所示,在服务端调用文件通道的写方法时,才会将消息集字节缓冲区的内容刷写到文件中。

如图6-9所示,以nextOffsetMetadata为例,它的读写操作发生在服务端处理生产请求和拉取请求时,具体步骤如下。

(1) 生产者发送消息集给服务端, 服务端会将这一批消息追加到日志中。
(2) 每条消息需要指定绝对偏移量,服务端会用nextOffsetMetadata 的值作为起始偏移量。
(3) 服务端将每条带有偏移量的消息写入到日志分段中。
(4) 服务端会获取这一批消息中最后一条消息的偏移量,加上一后更新nextOffsetMetadata 。
(5) 消费线程(消费者或备份副本)会根据这个变量的最新值拉取消息。一旦变量值发生变化,消费线程就能拉取到新写入的消息。

日志分段:

服务端处理每批追加到日志分段中的消息集,都是以nextOffsetMetadata作为起始的绝对偏移量。因为这个起始偏移量总是递增的,所以每一批消息的偏移量也一直保持递增。我们可以得出的结论是:同一个分区的所有日志分段中,所有消息的偏移量都是递增的。如图6 -11 所示,上面的箭头从全局的日志分段角度看,下面每个日志分段中的箭头则只从当前的日志分段看,有下面的两个特点:

1.新创建日志分段的基准偏移量,都比之前分段的基准偏移量要大;
2.同一个日志分段中,新消息的偏移量也比之前消息的偏移量要大。

如图6 -12所示,客户端传递的每一批消息,它们的偏移量都是从0开始的相对偏移量。消息集追加到日志分段后,相对偏移量会被更新为绝对偏移量。每个日志分段都有一个基准偏移量( segmentBaseOffset ),日志分段中每条消息的偏移量都是以这个基准偏移量为基础的。

滚动创建日志分段:

为消息集分配偏移量后,日志会将消息追加到最新的日志分段。如果当前的日志分段放不下新追加的消息集,日志会采用“滚动”方式创建一个新的日志分段, 并将消息集追加到新创建的日志分段中。如图6- 16所示,滚动创建日志分段分为下面3个步骤。

(1)“当前活动的日志分段”指向旧的日志分段。
(2)旧的日志分段空间不足,会创建新的日志分段。
(3) “当前活动的日志分段”指向新的日志分段。

判断是否需要创建新的日志分段,有下面3个条件。
1.当前日志分段的大小加上消息大小超过日志分段的|调值( log. segment. bytes配置项) 。
2.离上次创建日志分段的时间到达一定需要滚动的时间( log. roll.hours配置项) 。
3.索引文件满了。日志分段由数据文件和索引文件组成,第一个条件是数据文件满了, 会创建新的日志分段;这里的第三个条件是索引文件满了,也会创建新的日志分段。

 

索引文件:

写人数据文件的每一个消息集,它的每条消息都带有消息的绝对偏移量、大小、内容。为数据文件建立索引文件的基本思路是:建立“消息绝对偏移量”到“消息在数据文件中的物理位置”的映射关系。索引文件的存储结构有下面3种形式。
1.为每条消息都存储这样的对应关系: “消息的绝对偏移量”到“消息在数据文件中的物理位置” 。
2.以稀疏的方式存储部分消息的绝对偏移2到物理位置的对应关系,减少内存占用。
3.将绝对偏移盐改用相对偏移量,进一步减少内存的占用。

读取日志:

Kafka 中分区的主副本负责消息集的读写操作,消费者或者备份副本都会向主副本同步数据。客户端读取主副本的过程又叫作“拉取”,拉取主副本的消息集, 一定会指定拉取偏移量。比如, 前面章节中消费者会根据提交偏移量,或者客户端设置的重置策略从指定的拉取位置( startOffset )开始拉取消息。服务端处理客户端的拉取请求,就会返回从这个位置开始读取的消息集。另外,客户端还会指定拉取的数据量( fetchSize ),这个值默认是Maxpartitonfetchbytes 配置项,大小为1MB 。

如图6-20所示,因为日志分段是逻辑概念,它管理了物理概念的一个数据文件和索引文件。读取日志分段时,耍先读取索引文件再读取数据文件,不应该直接读取数据文件,具体步骤如下。
(1)根据起始偏移量( startOffset )读取索引文件中对应的物理位置。
(2)查找索引文件最后返回:起始偏移盘对应的最近物理位置( startPosition )。
(3)根据起始位置直接定位到数据文件,然后开始读取数据文件的消息。
(4)最多只能读取到数据文件的结束位置( maxPosition )。

读取日志分段:

下面的代码片段是针对“备份副本拉取请求”简化后的读取方法:

日志分段的log 引用指的是文件消息集( FileMessageSet ),而不是日志对象( Log )。文件消息集的读取方法会根据传人的开始位置和读取长度,构造一个新的文件消息集对象。读取文件的一般做法是:定位到指定的位置,读取出指定长度的数据,并且借助字节缓冲区来保存读取出来的数据。而这里读取出来后还要用对象来表示,所以直接创建新的文件消息集,类似于一个视图对象。相关代码如下:

查找索引文件:

如图6 -25 所示, 二分查找的low 、mid、high表示索引编号, found 值是索引条目:编号为mid 的偏移量值。如果found 值比目标偏移量( relOffset )小,在右边查找;如果found值比目标偏移盘大,在左边查找。如果最后还是没有查到,则返回low ,因为要找的是小于或等于目标偏移量的值。

查询索引文件返回的偏移量和物理位置不一定表示起始偏移量和起始位置,还需要搜索数据文件才能确定起始偏移量对应的起始位置。只有确定起始位置,日志分段才会调用数据文件的读取方法读取消息。

搜索数据文件:

如图6 -26所示,假设要查询目标偏移量为13 的消息,数据文件的起始定位位置是0 。查询第一条消息,它的偏移量为10 ,小于目标偏移量,于是读取第一条消息的大小,然后跳过这个大小定位到第二条消息的起始位置。查询第二条消息,它的偏移量为11,也小于目标偏移量,剩下的操作和第一条消息的处理方式类似。当查询到第四条消息时,消息的偏移量等于目标偏移量,就可以结束并返回结果了。

文件消息集视图:

文件消息集是数据文件的实现类,这个类除了文件、文件通道外,还有两个代表文件位置的变量:开始位置( start )和结束位置( end ) 。文件消息集的读取方法根据起始位置和读取大小,创建一个新的文件消息集视图。每次调用读取方法,都会生成一个新的文件消息集对象。我们仅在服务端处理客户端的拉取请求时才会调用消息集的读取方法。

如果客户端的拉取请求读取的是同一个日志分段( 一个日志分段够客户端的拉取请求读取很多次),数据文件是同一个,说明同一个文件消息集会调用多次读取方法。虽然读取方法新创建的文件消息集视图每次都不同,但所有的文件消息集都共用同一个文件和文件通道。为了区分不同的文件消息集,我们把和日志分段相关的文件消息集叫作“原始文件消息集”,调用“原始文件消息集”读取方法创建的新文件消息集叫作“文件消息集视图” 。“原始文件消息集”的起始位置为0 ,结束位置为无限大,而且它们都不会变化。但读取方法创建的“文件消息集视图”,它们的起始和结束位置则是变化的。

如图16-27所示,服务端读取日志分段创建了新消息集,并将消息集发送给客户端,具体步骤如下。
(1)日志分段新创建数据文件,文件和文件通道会用来创建一个文件消息集对象(原始文件消息集)。
(2) 每次读取日志分段,都会调用原始消息集的读取方法。
(3)原始消息集的每也读取方法,都会创建新的“文件消息集视图” 。
(4) 文件消息集视图的WriteTo()方法, 会将文件通道的字节直接传输到客户端网络通道。

原始文件消息集和新创建的文件消息集视图使用场景不同。前者可以看作全局的文件消息集,后者是局部的文件消息集视图。如图6-28 所示,这两种文件消息集互相关联,与之相关的操作如下。
(1)生产者产生的字节缓冲区消息集会追加到日志分段对应的文件消息集。
(2)文件消息集会将字节缓冲区消息集写入到数据文件底层的文件通道中。
(3)服务端处理客户端的拉取请求,读取日志分段,会读取文件消息集。
(4)文件消息集的读取方法会生成一个局部的文件消息集视图,它和数据文件底层的文件通道相关。
(5)局部文件消息集视图发送拉取响应结果给客户端,会将文件通道的字节直接传输给网络通道。

日志管理:

Kafka消息千· ~理节点的数据目录配置项( log.dirs )可以设置多个目录。如图6-31 (左)所示,代理节点的log.dirs = /tmp/kafka_logs1,/tmp/kafka_logs2 ,表示它有两个数据目录。代理节点负责的所有分区分别分布在这两个目录巾,第一个数据目录下有[ test0-0, test0-1,test1-2 ], 第二个数据目录下有[ test0-2,test1 - 0,test1 - 1 ] 。右图中,第一个数据目录下除了所有分区的日志目录,还有一个代表所有分区的全局检查点文件。

Kafka服务端的配置文件( server. properties )中针对日志管理后台线程有不同的配置项:

 

检查点文件:

消息代理节点用多个数据目录存储所有的分区日志,每个数据目录都有一个全局的检查点文件,检查点文件会存储这个数据目录下所有日志的检查点信息。检查点表示日志已经刷新到磁盘的位置,它在分布式存储系统中,主要用于故障的恢复。

如图6-32所示,检查点文件在日志管理类和日志实例的运行过程中起了重要作用,具体步骤如下。
(1) Kafka启动时创建日志管理类,读取检查点文件,并把每个分区对应的检查点( checkPoint )作为日志的恢复点(recoveryPoint ),最后创建分区对应的日志实例。
(2)消息追加到分区对应的日志,在刷新日志时,将最新的偏移量作为日志的检查点。
(3)日志管理器会启动一个定时任务: 读取所有日志的检查点,并写入全局的检查点文件。

 

刷新日志:

日志管理器启动时全定时调度flushDirtyLogs()方法,定期将页面缓存中的数据真正刷写到磁盘的文件中。日志在未刷写之前,数据保存在操作系统的页面缓存中,这比直接将数据写到磁盘文件快得多。但这种做法同时也意味着: 如果数据还没来得及刷写到磁盘上,消息代理节点崩溃了,就会导致数据丢失(当然,如果有副本,多个节点同时崩溃的情况比较少见,降低了数据丢失的风险)。有两种策略可以将日志刷写到磁盘上:时间策略和大小策略。对于时间而言,用调度器来做最适合,所以日志管理器启动的时候会启动一个定时器,每隔log.flush.interval.ms 的时间执行一次刷写动作。

刷新日志方法的参数是日志的最新偏移量( logEndOffset ),它要和日志中现有的检查点位置( recoveryPoint )比较,只有最新偏移量比检查点位置大,才需要刷新。由于一个日志有多个日志分段,所以刷新日志时,会刷新从检查点位置到最新偏移量的所有日志分段,最后更新检查点位置。如图6 -33 (左)所示,消息集追加到一个日志分段中,通过收集的消息数量或者固定的间隔时间,周期性地更新检查点。如右图所示,新创建一个日志分段,会立即刷写旧的日志分段,井更新检查点。

清理日志:

为了控制日志中所有日志分段的总大小不超过阔值( log. retention.bytes 配置项),日志管理器会定时清理旧的日志分段。清理日志分段时,从最旧的日志分段开始清理。因为日志分段中消息的偏移量是递增的,所以清理旧的日志分段,表示清理旧的消息。日志清理有下面两种策略。

1.删除( delete ) 。超过日志的阁值,直接物理删除整个日志分段。
2.压缩( compact ) 。不直接删除日志分段,而是采用合并压缩的方式。(压缩后打上清理点)

服务端处理读写请求:

Kafka服务在启动时会先创建各种相关的组件,最后才会创建KafkaApis 。业务组件一般都有后台的线程,除了创建组件后,也要启动这些后台线程。

服务端将请求的处理交给副本管理器( ReplicaManager)。与日志存储相关的业务组件是副本管理器, 负责日志的底层类是日志管理器,副本管理器通过日志管理器间接地操作底层的日志。相关代码如下:

追加消息时,生产者客户端会发送每个分区以及对应的消息集(messagesPerPartition ); 拉取消息时,客户端会发送每个分区以及对应的拉取信息(fetchlnfo)。服务端返回给客户端的响应结果也会按照分区分别返回,生产请求的响应结果( PartitionResponse )包含追加消息集到分区后返回的起始偏移量。拉取请求的响应结果( FetchResponsePartitionData )包含每个分区的最高水位、每个分区的消息集。

分区与副本:

分区对象:

每个分区都只有一个主副本和多个备份副本,不同节点上的分区对象,它们的主副本对象都是同一个( leaderRepHcaldOpt变量) 。另外,分区对象还维护了所有的副本( assignedReplicaMap 字典,简称AR ) 、同步的副本( inSyncReplicas 集合,简称ISR ) 。

副本对象:

分区创建副本分成本地副本( localReplica )和远程副本( remoteReplica )。节点编号和副本编号相同的副本叫作本地副本,编号不同的叫作远程副本。本地副本和远程副本的区别如下。
1.本地副本有日志( Log ),远程副本没有日志。有日志就表示有日志文件。
2.创建本地副本时,会读取“检查点文件”中这个分区的初始最高水位。远程副本没有初始最高水位。

如图6-50 所示,生产者追加消息集到主副本的本地日志(步骤(2 )),备份副本同步数据也会将拉取结果写入向己的本地日志(步骤( 6 )),这两种场景都会更新本地日志的偏移量元数据。除此之外,主副本所在的服务端处理备份副本的拉取请求,也会更新备份副本的偏移量元数据(步骤(4))  具体步骤如下。

(1) 生产者客户端将消息集追加到分区的主副本,这里假设副本1 是主副本。
(2)消息集追加到主副本的本地日志,会更新日志的偏移量元数据。
(3)其他消息代理节点上的备份副本向主副本所在的消息代理节点同步数据。
(4)主副本所在的副本管理器读取本地日志,井更新对应拉取的备份副本信息。
(5)主副本所在的服务端将拉取结果返回给发起拉取请求的备份副本。
(6)备份副本接收到服务端返回的拉取结果,将消息集追加到本地日志,更新日志的偏移量元数据。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值