【MongoDB】MongoDB的存储引擎及Wiredtiger的读/写缓存、数据结构设计、Page生命周期等实现原理(超详细)

文章目录

更多相关内容可查看

存储引擎的种类

MongoDB 支持多个存储引擎

存储引擎 描述 适用场景
MMAPv1 MongoDB 最早的存储引擎,基于内存映射文件,适合高并发和低延迟的场景。但存在性能瓶颈,如锁粒度粗、没有事务支持等,逐渐被 WiredTiger 替代。 高并发、低延迟的场景,但不适合高复杂度和高事务性的需求。
WiredTiger 从 MongoDB 3.0 版本开始成为默认存储引擎,提供更细粒度的锁、更强大的事务支持、数据压缩和更好的多核 CPU 利用。 大规模、高并发的数据访问场景,适合需要事务支持的应用。
In-Memory 专为内存数据库设计,数据仅保存在 RAM 中,不做持久化,适用于需要极高数据存取速度的场景。 对数据存取速度要求极高、容忍数据丢失的应用。

Wiredtiger的实现原理(B 树结构)

读缓存

理想情况下,MongoDB可提供近似内存式的读写性能。WiredTiger引擎实现数据的二级缓存,第一层是操作系统层级的页面缓存,第二层则是引擎提供的内部缓存:

读取数据时的流程:

  • 数据库发起Buffer I/O读操作,由操作系统将磁盘数据页加载到文件系统的页缓存区
  • 引擎层读取页缓存区的数据,进行解压后存放到内部缓存区
  • 在内存中完成匹配查询,将结果返回给应用。

如果数据已经被存储在内部缓存中,MongoDB则可以发挥最佳的读性能。稍差的情况是内部缓存中找不到,但数据仍然被存储在操作系统的页缓存中,此时需要花费一些数据解压缩的开销。直接从磁盘加载数据时,性能是最差的。因此MongoDB为了尽可能保证业务查询的热点数据能快速被访问,其内部缓存的默认大小达到内存的一半,该值由wiredTigerCacheSize参数指定,其默认计算公式: w i r e d T i g e r C a c h e S i z e = M a t h . m a x ( ( R A M − 1 G B ) , 256 M B ) wiredTigerCacheSize=Math.max((RAM-1GB),256MB) wiredTigerCacheSize=Math.max((RAM−1GB),256MB)


写缓存

当数据发生写入时,MongoDB并不会立即持久化到磁盘上,而是先在内存中记录这些变更,随后通过CheckPoint机制将变化的数据写入磁盘。即,非实时持久化。

采用延迟持久化方案,则避不开可靠性问题。

MongoDB单机下保证数据可靠性的机制包括以下两个部分:

  • CheckPoint机制:快照(snapshot)描述某一时刻(point-in-time)数据在内存中的一致性视图,而这种数据的一致性是WiredTiger通过MVCC(多版本并发控制)实现的。当建立CheckPoint时,WiredTiger会在内存中建立所有数据的一致性快照,并将该快照覆盖的所有数据变化一并进行持久化(fsync)。成功之后,内存中数据的修改才得以真正保存。MongoDB默认每60s建立一次CheckPoint,在检查点写入过程中,上一个检查点仍然是可用的。这样可以保证一旦出错,仍能恢复到上一个检查点。
  • Journal日志:一种预写式日志(write aheadlog)机制,主要用来弥补CheckPoint机制的不足。如果开启Journal日志,那么WiredTiger会将每个写操作的redo日志写入Journal缓冲区,该缓冲区会频繁地将日志持久化到磁盘上。默认情况下,Journal缓冲区每100ms执行一次持久化。此外,Journal日志达到100MB,或是应用程序指定journal:true,写操作都会触发日志的持久化。一旦MongoDB发生宕机,重启程序时会先恢复到上一个检查点,然后根据Journal日志恢复增量的变化。由于Journal日志持久化的间隔非常短,数据能得到更高的保障,如果按照当前版本的默认配置,则其在断电情况下最多会丢失100ms的写入数据。

journal日志 按我的理解是 mongodb每六十秒进行一次Checkpoint的数据持久化到磁盘 如果在这30秒的时候 发生宕机 而Journal日志会记录每100ms的日志,重启就会读取这三十秒的Journal日志进行数据恢复

结合CheckPoint和Journal日志,数据写入的内部流程图:

步骤:

  • 应用向MongoDB写入数据(插入、修改或删除)
  • 数据库从内部缓存中获取当前记录所在的页块,如果不存在则会从磁盘中加载(Buffer I/O)
  • WiredTiger开始执行写事务,修改的数据写入页块的一个更新记录表,此时原来的记录仍然保持不变
  • 如果开启Journal日志,在写数据同时会写入一条Journal日志(Redo Log)。该日志在最长不超过100ms之后写入磁盘
  • 数据库每隔60s执行一次CheckPoint操作,此时内存中的修改会真正刷入磁盘。

Journal日志采用的是顺序I/O写操作,频繁地写入对磁盘的影响并不是很大。在MongoDB 3.4及以下版本中,当Journal日志达到2GB时同样会触发CheckPoint行为。如果应用存在大量随机写入,则CheckPoint可能会造成磁盘I/O的抖动。在磁盘性能不足的情况下,问题会更加显著,此时适当缩短CheckPoint周期可以让写入平滑一些。


存储引擎与数据结构设计

这里通俗点来做简单的描述,一个是减少阅读量,一个是便于理解

1. 存储引擎的基本任务

存储引擎的核心任务就是管理数据库如何高效地存储和读取数据。它主要做两件事:

  • 从磁盘读取数据到内存,然后返回给应用。
  • 将修改的数据从内存写回磁盘

为了解决如何快速存取大量数据,存储引擎通常使用两种重要的数据结构:B-TreeLSM Tree

2. B-Tree 和 LSM Tree
  • B-Tree 是一种为磁盘存取优化的数据结构。它的目标是减少数据查找时的磁盘读写次数。

  • B-Tree 树的结构分为三个层级:

    • 根节点 (Root):树的顶端,指引查找。
    • 内部节点 (Internal Nodes):用来继续引导查找,指向下一层的节点。
    • 叶子节点 (Leaf Nodes):存储实际的数据,或者是数据的位置(偏移量)。
  • 每个节点(Page)存储一定量的数据,为了减少磁盘 I/O 操作,B-Tree 会根据节点大小和树的层级组织数据。例如,假设一个节点有 100 个分支,叶子节点就能存储 100 万个数据条目。

  • B-Tree 的操作开销:随着数据的插入和删除,B-Tree 需要做一些额外的操作(比如节点的分裂、合并),这些操作虽然能保持数据的有序性,但也会带来性能开销。

LSM Tree

  • LSM Tree 是另一种数据结构,特别适用于 写入密集型应用。与 B-Tree 不同,LSM Tree 将数据的更新操作分为两个阶段:首先在内存中进行(内存中的数据结构叫 MemTable),然后周期性地将内存中的数据合并写入磁盘。
  • LSM Tree 的优势是能够高效地处理大量写操作,但读取时可能需要从多个地方(内存和磁盘)查找数据,因此查找效率可能不如 B-Tree。
3. WiredTiger 存储引擎

WiredTiger 是一个在 MongoDB 中使用的存储引擎,它支持 B-Tree 和 LSM Tree 这两种数据结构。

磁盘上的数据结构

WiredTiger 存储引擎在磁盘上的数据结构是 B+ Tree(B-Tree 的一种变种)。它与 B-Tree 的主要区别是,B+ Tree 的叶子节点不仅存储数据的键(key),还存储实际的数据(值,value)。这样,数据可以在 B+ Tree 的叶子节点上直接找到。

  • 页面(Page)结构:每个 B+ Tree 节点就是一个页面,每个页面的大小是固定的。页面内保存了节点的元数据和实际的数据。
  • 块管理 :WiredTiger 会为每个页面分配一个"块",通过块的地址来定位数据。每个块会包含一些校验和信息,确保数据的完整性。

内存中的数据结构

在内存中,WiredTiger 将磁盘上的数据加载到内存,并通过 B+ Tree 维护索引。此外,还会用其他数据结构来支持高效的 CRUD(增、删、改、查)操作:

  • Leaf Page (叶子页面):存储实际的数据,并且每个叶子页面有一个 WT_ROW 数组来保存数据的键和值。
  • 更新信息 :对于修改过的数据,WiredTiger 会在内存中维护一个 WT_UPDATE 结构,记录数据的历史修改。如果一条数据被多次修改,这些修改会以链表的形式被记录下来。
  • 插入操作 :对于新的插入数据,WiredTiger 会使用 WT_INSERT_HEAD 数据结构来跟踪待插入的数据。
4. 其他内存数据结构

除了 WT_ROWWT_UPDATE,WiredTiger 还使用了一些其他的数据结构来优化性能:

  • WT_PAGE_MODIFY:用于保存页面的修改信息。
  • Lookaside Table:当数据正在被修改时,WiredTiger 会把未完成的修改保存在一个额外的存储区域,以便可以在后续访问时恢复。
  • 校验和(Checksum):每个页面都有一个校验和,用来保证数据的完整性。

Page

1.Page的生命周期

数据以page为单位加载到cache、cache里面又会生成各种不同类型的page及为不同类型的page分配不同大小的内存、eviction触发机制和reconcile动作都发生在page上、page大小持续增加时会被分割成多个小page,所有这些操作都是围绕一个page来完成的。

步骤 描述
第一步 Pages从磁盘读到内存。
第二步 Pages在内存中被修改。
第三步 被修改的脏Pages在内存中被reconcile,完成后将discard这些Pages。
第四步 Pages被选中,加入淘汰队列,等待evict线程淘汰出内存。
第五步 Evict线程会将"干净"的Pages直接从内存丢弃,将经过reconcile处理后的磁盘映像写到磁盘,再丢弃"脏的"Pages。

pages的状态是在不断变化的,因此,对于读操作来说,它首先会检查pages的状态是否为WT_REF_MEM,然后设置一个hazard指针指向要读的pages,如果刷新后,pages的状态仍为WT_REF_MEM,读操作才能继续处理。

与此同时,evict线程想要淘汰pages时,它会先锁住pages,即将pages的状态设为WT_REF_LOCKED,然后检查pages上是否有读操作设置的hazard指针,如有,说明还有线程正在读这个page则停止evict,重新将page的状态设置为WT_REF_MEM;如果没有,则pages被淘汰出去

2.Page的各种状态
状态 描述
WT_REF_DISK 初始状态,page在磁盘上的状态,必须被读到内存后才能使用。当page被evict后,状态也会被设置为此。
WT_REF_DELETED page在磁盘上,但是已经从内存B-Tree上删除。当不再需要读某个leaf page时,可以将其删除。
WT_REF_LIMBO page的映像已经被加载到内存,但page上有额外的修改数据在lookasidetable上没有被加载到内存。
WT_REF_LOOKASIDE page在磁盘上,但在lookasidetable中也有与此page相关的修改内容,必须加载这部分内容才能读取该page。
WT_REF_LOCKED 当page被evict时,将其锁住,其他线程不可访问。
WT_REF_MEM page已经从磁盘读到内存,并且能正常访问。
WT_REF_READING page正在被某个线程从磁盘读到内存,其他线程等待它被读完,不需要重复读取。
WT_REF_SPLIT 当page变得过大时,会被split,状态设为此。原来指向的page不再被使用。
3.Page的大小参数

无论将数据从磁盘读到内存,还是从内存写到磁盘,都是以page为单位调度的,但是在磁盘上一个page到底多大?是否是最小分割单元?以及内存里面的各种page的大小对存储引擎的性能是否有影响?

参数名称 描述 默认值 影响
allocation_size MongoDB磁盘文件的最小分配单元,由WiredTiger自带的块管理模块分配。一个page可以由一个或多个这样的单元组成。 4KB 不需要修改此值,大多数场景下和操作系统的虚拟内存页大小相当。
memory_page_max WiredTiger Cache中的一个内存page允许增长的最大值,超过此值时会split并通过reconcile将数据写入磁盘。 5MB 设置不当会影响性能,过大导致锁持有时间长,过小增加split和reconcile频率。
internal_page_max 磁盘上internal page的最大值,超过此值时会split为多个pages。 4KB 影响B-Tree深度和key数量,太大影响查找速度,太小则B-Tree深度过大。
leaf_page_max 磁盘上leaf page的最大值,超过此值时会split为多个pages。 32KB 影响磁盘I/O性能,调大有助于减少I/O,但太大可能会导致读写放大。
internal_key_max internal page允许的最大key值,超过此值会额外存储,可能导致额外的磁盘I/O。 internal_page_max的1/10 影响磁盘I/O,过大时需要额外存储,增加读取时间。
leaf_key_max leaf page允许的最大key值,超过此值会额外存储,可能导致额外的磁盘I/O。 leaf_page_max的1/10 影响磁盘I/O,过大时需要额外存储,增加读取时间。
leaf_value_max leaf page允许的最大value值,超过此值会额外存储,可能导致额外的磁盘I/O。 leaf_page_max的1/2 影响磁盘I/O,过大时需要额外存储,增加读取时间。
split_pct 内存中将要被reconciled的page大小与internal_page_max或leaf_page_max的百分比,决定是否发生split。 75% 控制split发生的概率,影响reconcile和split的频率与效果。
相关推荐
Daniel 大东18 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
起飞的风筝1 小时前
【redis】—— 环境搭建教程
数据库·redis·缓存
韭菜盖饭1 小时前
LeetCode每日一题3261---统计满足 K 约束的子字符串数量 II
数据结构·算法·leetcode
♡喜欢做梦2 小时前
【数据结构】ArrayList与LinkedList详解!!!——Java
java·开发语言·数据结构·链表
好心的小明2 小时前
【深圳大学】数据结构A+攻略(计软版)
数据结构
熬夜学编程的小王2 小时前
【初阶数据结构篇】插入、希尔、选择、堆排序
数据结构·c++·插入排序·选择排序·希尔排序
三小尛2 小时前
快速排序(C语言)
数据结构·算法·排序算法
椅子哥2 小时前
数据结构--排序算法
java·数据结构·算法·排序算法
DDDiccc2 小时前
JAVA学习日记(十五) 数据结构
数据结构·学习
Nydia.J2 小时前
【学习笔记】数据结构(七)
数据结构·考研