前言
说明
本文主要介绍几个高性能中间件的磁盘文件存储结构、存储逻辑等,从而学习它们的设计思路。
linux磁盘文件
- 硬盘的最小存储单位是扇区(Sector,512B),块(block)由多个连续的扇区组成,块的最常见的大小是4kb(8个连续的扇区组成)。
- 磁盘读取的基本单位是扇区(物理层),而操作系统(文件管理系统)读写的基本单位是块(逻辑层)。
- block:文件数据存储在块中,一个文件可能会占用多个块,但是一个块只能存放一个文件。
- inode:linux/unix文件系统的基础,每个文件都有一个inode,存储文件的元信息。虽然我们将文件存储在了块(block)中,但是我们还需要一个空间来存储文件的元信息metadata:如某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等。这种 存储文件元信息的区域就叫inode(索引节点:indexNode)。
- 可以使用 stat 命令可以查看文件的 inode 信息。每个 inode 都有一个号码,Linux/Unix 操作系统不使用文件名来区分文件,而是使用 inode 号码区分不同的文件。
Kafka
每个分区会创建一个分区目录,分区目录下面存放的是日志文件(.log),名称相同的索引文件(.index / .timeIndex)
这里仅分析文件结构,更多实现细节查看:https://blog.csdn.net/songcf_faith/article/details/124741797
1.日志文件
- 命名:日志文件名为上一个log文件的最后一个offset的值+1,作为新的日志和索引文件名(索引和日志文件同名/仅后缀不同)。(比如00000000000000000271.log文件的上一个日志文件中最后的offset一定是270)
- 大小:每个日志文件大小默认1G,可根据log.segment.bytes配置;
- 内容:日志文件存储了消息的具体内容(offset、messageSize、data);offset类似消息自增id
- 清理:日志文件和索引文件会不断被清理,依赖于topic的保留时长(log.retention.ms)和保留字节大小(log.retention.bytes)决定。
2.索引文件
- 命名:同日志文件名,仅后缀不同。
- offset索引:索引文件每条记录包含相对offset(4B)和日志position(4B)两部分,共8字节
- time索引:索引文件每条记录包含时间戳(8B)和日志position(4B)两部分,共8字节
- 内容:存放的是稀疏索引(像跳表)并非全部,可以节约空间将索引全部加载进内存,缺点就是部分消息不能一次性定位。
图片引用自:https://segmentfault.com/a/1190000021824942
RocketMQ
所有消息数据存到CommitLog中,提升文件刷盘效率,然后另外文件存消息队列位置索引(ConsumerQueueFiles)、时间/关键词索引(IndexFiles)。
这里仅分析文件结构,更多实现细节查看:https://blog.csdn.net/songcf_faith/article/details/124600106
1.CommitLog
- 命名:文件名称为当前文件开始字符在整个日志中的偏移量固定20字符(前面补0)。无法写入最后一条消息且文件没满1G,则写入空余字节数(4B)+结尾标记(4B)
- 大小:单文件默认1G,方便内存映射,fileChannel.map映射不能大小>=有符号int(2^31-1即2G),为了方便理解所以默认大小定为1G(可配置修改)
- 内容:消息完整内容,所有主题日志都追加到该文件中,比起按主题分文件写(如kafka)能够更高效的顺序写,刷盘效率高。
- 多个线程调用write把日志写到文件的pageCache中;也可以配置堆外缓冲(5个1G的DirectByteBuffer),循环写入堆外缓冲,再由线程写入pageCache
2.ConsumerQueueFiles
- 命名:文件名称为当前文件开始字符在索引日志中的偏移量固定20字符(前面补0)(0、600w、1200w)
- 内容:每个队列一个目录,30w条消息一个文件,每条消息20字节(hashCode、在commitLog中的偏移、消息字节数)
3.IndexFiles
(slots类似hash表,kafka没有这种索引)
- 命名:按照创建时间来命名,所以key搜索时都需要传时间
- 内容:文件内容分三部分(header和slots是固定的,indexes是追加的/数量不定)
- header——文件头部,共40B(开始/结束时间,最大/最小偏移,有效slot数量,索引数量)
- slots——keyHash槽slot,每个4B,固定500w个(索引编号)
- indexes——索引数据,每个20B,最多2000w个(keyHash,logOffset,timeDiff,indexNo)
- 索引数据的最后4B——是indexNo,指向相同hash的下一个索引数据,slot中hash冲突时会替换为新记录的indexNo,然后新记录的最后4B存原slot中数据,形成冲突链表
图片引用自:https://www.cnblogs.com/xijiu/p/15565997.html
PostgreSQL
- pg的存储管理单元是page,一个文件由多个page组成(相当于数组的一个bucket),和Oracle中的数据块一样,指的是数据库的块,操作系统块的整数倍个,默认是8K也就是两个操作系统块(4k的文件系统块)。
- 由于32位寻址所以表或索引大小最多2^32 * 8k = 32T,单个文件超过1G(可配置)就会分割新文件。
- 如果page设低了,相同数据量的文件需要分裂成更多的page,IO次数和索引分裂次数都会增加,性能会降低较多;如果page设高了,page内部的数据检索效率会降低,性能一样会降低不少,一般来说8K和16K对于数据库系统来说是最优解。
- 为什么需要page:便于文件管理,将文件分为大小相等的数个逻辑单元,通过page编号作为索引直接将文件当作数组访问。索引文件中一个page是B树的一个树节点,数据文件中一个page是的一个数组槽。
图片引用自pg官网
1.数据文件
包含5部分:PageHeaderData、ItemList、FreeSpace、Tuple、SpecialSpace
- PageHeaderData——页面头部信息24B,空闲空间/特殊空间位置偏移、校验码、标志位等
- ItemList/LinePoint——4B,记录了3个数据:定位tuple的偏移量15b、属性位2b、tuple的长度15b
- FreeSpace——空闲空间,Item从它前面开始向后分配,Tuple从它后面开始向前分配
- Tuple——数据项,头部信息HeapTupleHeaderData(实现MVCC的关键)、表记录的数据字段
- SpecialSpace——索引访问模式相关的数据,不同的索引访问方式存放不同的数据。比如b-tree索引用存储指向页面的左右兄妹的链接,以及其他一些和索引结构相关的数据。
2.索引文件
- pg采用的非聚集索引,所以索引和数据是分开存储的,索引page内容和数据page内容同样是5部分,一个page页相当于B树的一个节点。
- 树结构:metaPage -> rootPage -> branchPage(0-n个非叶子节点) -> leafPage(0-n个叶子节点)。树结构参考MySQL中的BTree。
- PageHeaderData
- ItemList
- FreeSpace
- Tuple——索引字段数据、完整数据物理位置<块号,块内偏移>、其它
- SpecialSpace——左右兄弟块号、层级等信息
MySQL
- mysql的存储管理单元是page(同pg),区别是mysql的默认page大小是16kb。
- 文件空间(space)包含多个区段(extent),每个区段包含连续的64个页面(page)
- 主键索引是聚集索引,叶节点存储了完整的数据记录
- 辅助索引(二级索引)是非聚集索引,叶节点存储了主键索引值
这里仅分析文件结构,更多实现细节查看:https://blog.csdn.net/songcf_faith/article/details/124599663
数据文件和索引文件
每个文件page的内容
- File Header
- Index Header
- FSEG Header
- System Records
- User Records——(类似postgresql的Tuple)数据字段,具体的一条数据(索引数据或完整的行数据),大小是不定的,各条数据已排序,并通过尾指针串联起来形成单向链表。
- Free Space——(类似postgresql的FreeSpace)空闲空间,暂未存放数据的区域。UserRecords从它的前面往后分配,PageDirectory从它的后面往前分配。
- Page Directory——(类似postgresql的Item/LinePopint)每个元素大小固定2B的一个动态数组,顺序存放具体数据的指针(方便多分查找,因为具体数据的长度是不确定的)。
- File Trailer
注意这里4-8条Record才会有一个Directory,1是为了节约空间,2是避免每次插入数据Directory都产生大量移动(因为数组是有序的)
BTree结构(类似pg,不过pg索引类似innodb的辅助索引 叶节点不存数据)
图片引用自:https://blog.jcole.us/2013/01/07/the-physical-structure-of-innodb-index-pages/
Page中的结构(类似postgresql中的page)
ES / Lucene
lucene中的文件比较多,文档数据相关的如下:
- XXX.fdx,XXX.fdt 保存了XXX此段包含的所有文档,每篇文档包含了多少字段,每个字段保存了那些信息。
- XXX.tis,XXX.tii保存了词典(Term Dictionary),也即此段包含的所有的词按字典顺序的排序。
- XXX.frq保存了倒排表,也即包含每个词的文档ID列表。
- XXX.prx保存了倒排表中每个词在包含此词的文档中的位置。
这里仅分析文件结构,更多实现细节查看:https://blog.csdn.net/songcf_faith/article/details/124599368
1.词典索引文件和词典文件
文件内容格式TODO
图片引用自:https://www.cnblogs.com/forfuture1978/archive/2009/12/14/1623599.html
2.词倒排表和词位置文件
3.文档数据索引和文档文件
总结
- 文件的结构主要用到了:数组、hash表、跳表、升级的B树、排序数组、单/双向链表等数据结构,来提升查找、访问效率;
- MQ存的是消息,消息索引,索引方式比较简单,索引通常是线性或二分查找,所以通过固定索引项20B存储;
- DB的索引复杂,涉及BTree,将文件拆分为多个块来管理,一个块节点内有多个项,而且是块节点带指向兄弟节点指针;块的使用会导致部分空间浪费,但是提升了访问效率;
- 核心目的降低磁盘IO次数,然后尽可能的少浪费空间。