【MongoDB】MongoDB的存储引擎及Wiredtiger的读/写缓存、数据结构设计、Page生命周期等实现原理(超详细)
文章目录
- 存储引擎的种类
- Wiredtiger的实现原理(B 树结构)
- 读缓存
- 写缓存
- 存储引擎与数据结构设计
- 1. 存储引擎的基本任务
- 2. B-Tree 和 LSM Tree
- 3. WiredTiger 存储引擎
- 4. 其他内存数据结构
- Page
- 1.Page的生命周期
- 2.Page的各种状态
- 3.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-Tree 和 LSM 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_ROW 和 WT_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的频率与效果。 |