WiredTiger 的数据缓存机制

本文介绍WiredTiger存储引擎的缓存机制,包括如何利用B-tree管理和组织内存页,采用LRU淘汰机制确保热点数据驻留在内存中,以及使用HazardPointer解决读多写少场景下的并发问题。

大多数的存储引擎都有自带的缓存机制, WiredTiger也不例外。 存储引擎的缓存机制都是基于数据库数据的总量要远远大于内存的容量, 在数据库的使用过程中, 不可能将全部的数据放进内存中, 总是需要将当前用到的数据从磁盘读入内存, 当内存容量不足的时候, 将内存中不是热点的数据写回盘, 腾出一定的内存空间来保存新的数据。
因此, 对于存储引擎的缓存机制, 最核心的地方就是, 使用适当的策略, 找到热点数据 将非热点数据移出内存, 这里采用的是常见的LRU淘汰机制。

btree

在缓存里面, 通过B-tree的结构来组织和管理内存页, 内存页是最小的内存分配单元。每一个B-tree对应一个checkpoint, checkpoint代表了当前写入到磁盘上的数据, 通过它可以得到在这之前所有已经完成的事物。在B-tree里面, 有一个root page, 通过它可以得到checkpoint的第一个内存页, 以及其子内存页数组, 然后我们就可以一层层的展开, 直至叶子节点。
我们的所有更新操作, 都会通过cursor提供的search接口, 得到相关的内存页, 如果该内存页还没有进入内存, 会通过inmem的方式, 将磁盘Extend块, 加载到内存中。对于每一个内存页, 它会记录一个KV的row 数组, 记录当前已有的KV, 并且还有一个insert 数组和一个update 数组, 来记录该页面在被加载到内存之后的增加和修改操作。每当有针对该内存页的修改, 首先在内存里面记录, 当该内存页需要被reconcile到磁盘的时候, 整合成一个或者多个磁盘Extend。

Hazard Pointer(风险指针)

Hazard Pointer是lock-free技术的一种实现方式, 它将我们常用的锁机制问题转换为一个内存管理问题, 通常额也能减少程序所等待的时间以及死锁的风险, 并且能够提高性能, 在多线程环境下面,它很好的解决读多写少的问题。
基本原理
对于一个资源, 建立一个Hazard Pointer List, 每当有线程需要读该资源的时候, 给该链表添加一个节点, 当读结束的时候, 删除该节点; 要删除该资源的时候, 判断该链表是不是空, 如不, 表明有线程在读取该资源, 就不能删除。
HazardPointer在WiredTiger中的使用
在WiredTiger里, 对于每一个缓存的页, 使用一个Hazard Pointer 来对它管理, 之所以需要这样的管理方式, 是因为, 每当读了一个物理页到内存, WiredTiger会把它尽可能的放入缓存, 以备后续的内存访问, 但是徐彤同时由一些evict 线程在运行,当内存吃紧的时候, evict线程就会按照LRU算法, 将一些不常被访问到的内存页写回磁盘。
由于每一个内存页有一个Hazard Point, 在evict的时候, 就可以根据Hazard Pointer List的长度, 来决定是否可以将该内存页从缓存中写回磁盘。

读入一个物理页

要讲一个物理页读入内存, 会使用到函数__wt_page_in_func, 它会根据该页的状态做出不同的处理:

  • WT_REF_DELETED(已删除)
    该页已经删除, 找不到
  • WT_REF_DISK (在磁盘上)
    该页在磁盘上需要读入内存, __page_read来将物理页读入内存, 如果该页之前已经被删除, 就创建一个新的内存页, 否则把该页的内容也一并读进来。
  • WT_REF_READING
    其他线程正在读入该页, 等待其他线程读直到结束。
  • WT_REF_LOCKED
    Evict 线程, 已经把它lock住, 准备转存到磁盘;
  • WT_REF_MEM
    表明该页已经在内存中, 用__wt_hazard_set在Hazard Pointer List 添加一项, 更新page->read_gen。

Evict 一个内存页

将一个内存页转存到磁盘的过程也比较简单, 它会把内存页的内容写入磁盘, 并且把该页的状态变为WT_REF_DISK, 如果该页是internal page, 还需要把WT_REF为WT_REF_LOCK的状态。

int
__wt_evict(WT_SESSION_IMPL *session, WT_REF *ref, bool closing)
{
...
	if (__wt_ref_is_root(ref))
		__wt_ref_out(session, ref);
	else if (tree_dead || (clean_page && !F_ISSET(conn, WT_CONN_IN_MEMORY)))
		/*
		 * Pages that belong to dead trees never write back to disk
		 * and can't support page splits.
		 */
		WT_ERR(__evict_page_clean_update(
		    session, ref, tree_dead || closing));
	else
		WT_ERR(__evict_page_dirty_update(session, ref, closing));
...
}

eviction server 线程

evict server 线程的作用是把缓存中按照LRU算法进行遍历, 找到一组满足移出内存条件的内存页。__wt_evict_create会创建指定数量的线程, __evict_thread_run作为线程的入口函数, 通过调用链路:
__evict_server–> __evict_pass–>__evict_lru_walk --> __evict_lru_pages

static int
__evict_pass(WT_SESSION_IMPL *session)
{
	pages_evicted = cache->pages_evict;

	/* Evict pages from the cache. */
	for (loop = 0;; loop++) {
		WT_RET(__wt_txn_update_oldest(session, WT_TXN_OLDEST_STRICT));

		if (!__evict_update_work(session))
			break;

		WT_RET(__evict_lru_walk(session));
		WT_RET_NOTFOUND_OK(__evict_lru_pages(session, true));
	}
	return (0);
}

实例connection cache

每一个connection有一个或者多个WT_CACHE, 用来管理cache的内存页。__wt_cache_create用来创建一个cache, 并且为LRU evict准备相应的数组结构。

int
__wt_cache_create(WT_SESSION_IMPL *session, const char *cfg[])
{
	...
	if ((ret = __wt_open_internal_session(conn, "evict pass",
	    false, WT_SESSION_NO_DATA_HANDLES, &cache->walk_session)) != 0)
		WT_ERR_MSG(NULL, ret, "Failed to create session for eviction walks");

	/* Allocate the LRU eviction queue. */
	cache->evict_slots = WT_EVICT_WALK_BASE + WT_EVICT_WALK_INCR;
	for (i = 0; i < WT_EVICT_QUEUE_MAX; ++i) {
		WT_ERR(__wt_calloc_def(session,
		    cache->evict_slots, &cache->evict_queues[i].evict_queue));
		WT_ERR(__wt_spin_init(session,
		    &cache->evict_queues[i].evict_lock, "cache eviction"));
	}

    ...
}

这里做了很多简化, 不同的session可以有不同的cache, 通过session->id 从一个cache pool里面得到相应的cache。

<think> 嗯,用户想了解MongoDB的数据压缩机制,这是一个技术性很强的问题。从用户提供的引用材料看,虽然内容涉及备份恢复、性能监控等,但并未直接提及压缩机制。不过引用[1]提到"深入分析MongoDB原生工具",引用[3]讲到存储优化,这些都与存储效率相关。 用户应该是数据库管理员或开发人员,可能正在评估MongoDB的存储成本。考虑到MongoDB在存储图片和文档的应用场景(如引用[3]提及),数据压缩对这类大文件存储尤为重要。用户没直接问但可能关心的点包括:压缩算法选择、CPU开销、与备份功能的联动。 需要系统性地解释三层压缩机制:首先是存储引擎层的WiredTiger默认压缩,要详细说明snappy和zlib的区别;然后是传输压缩,这在分片集群中很关键;最后是文件系统压缩,虽然MongoDB不直接提供但属于常用优化手段。 特别要注意技术细节的准确性:比如WiredTiger的块压缩原理,压缩字典在索引中的特殊处理。用户可能误以为索引无法压缩,这点需要澄清。对于时间序列集合的新特性(FPACK算法)也要单独说明,因为这是MongoDB 5.0的重要优化。 操作建议部分要实用,比如用collMod命令修改压缩算法的示例必须验证过语法。监控指标要具体到可执行的命令,像db.serverStatus().wiredTiger.cache这样的调用路径不能出错。 最后生成的问题要覆盖用户可能延伸的疑问:生产环境的选择依据、压缩监控、时间序列优化等。避免提出与压缩无关的问题,比如备份恢复虽然引用里提得多,但用户当前焦点明显在存储效率上。 </think> MongoDB 的数据压缩机制主要集中在**存储引擎层**,主要通过其默认存储引擎 **WiredTiger** 实现,旨在显著减少磁盘占用空间和 I/O 开销。以下是核心机制详解: --- ### **1. WiredTiger 存储引擎压缩机制** WiredTiger 支持多种压缩算法,针对不同数据类型和访问模式进行优化: * **块压缩(Block Compression):** * **对象级别:** MongoDB 将集合中的文档(BSON 格式)和索引数据以**块(Block)**为单位组织在磁盘上(默认块大小 32KB)。WiredTiger 在将数据块写入磁盘之前,会对其进行压缩。 * **支持的算法:** * `snappy` (默认):**快速、低 CPU 开销**,提供较好的压缩比(通常 50%-70%),是大多数场景的推荐选择。 * `zlib`:**高压缩比**(通常 60%-80%+),但 CPU 开销显著高于 `snappy`,适用于对存储空间要求极高、可以容忍更高写入延迟的场景(如归档数据)。 * `zstd` (MongoDB 4.2+ 引入):在**压缩比和 CPU 开销之间取得优秀平衡**(压缩比接近 `zlib`,速度接近 `snappy`)。是需要更高压缩比但 CPU 资源受限场景的更佳选择。 * **配置:** * **集合数据:** 在创建集合时指定 `storageEngine.wiredTiger.collectionConfig.blockCompressor`: ```javascript db.createCollection("myCollection", { storageEngine: { wiredTiger: { configString: "block_compressor=zstd" } } } ); ``` * **索引:** 在创建索引时指定 `storageEngine.wiredTiger.indexConfig.blockCompressor`: ```javascript db.myCollection.createIndex({ field: 1 }, { storageEngine: { wiredTiger: { configString: "block_compressor=zlib" } } } ); ``` * **全局默认:** 可在 MongoDB 启动配置文件中设置 `wiredTiger.engineConfig.collectionConfig` 和 `wiredTiger.engineConfig.indexConfig`。 * **前缀压缩(Prefix Compression / Dictionary Compression):** * **工作原理:** 特别优化处理**索引数据**和**数值型数据**。WiredTiger 会识别数据块中重复的前缀(如时间戳序列、类似字符串的开头部分)或公共字段值,将它们存储在字典中,后续重复出现时仅存储字典引用或差值。 * **优点:** 对**高度结构化、重复模式明显的数据**(如日志、传感器数据)压缩效果极佳,且**不影响查询性能**。 * **配置:** 前缀压缩是 WiredTiger 内部的优化机制,通常在索引创建时自动启用,无需特殊配置。 * **内存压缩(In-Memory Compression):** * **缓存层优化:** WiredTiger 会对其缓存(`WiredTiger Cache`)中的数据进行压缩,减少内存占用。压缩后的缓存数据在读取时解压,写入磁盘前再次压缩。这意味着**磁盘上的数据始终是压缩存储的**。 * **影响:** 能**容纳更多数据缓存中**,提高缓存命中率,间接提升性能,但会增加读写时的 CPU 开销用于解压/压缩。 --- ### **2. 传输压缩(Network Compression)** * **目的:** 减少客户端与服务端之间、分片集群成员之间(如分片到 mongos、副本集成员间复制)的网络传输数据量。 * **支持的算法:** `snappy`, `zlib`, `zstd` (需客户端和服务端支持)。 * **配置:** * **mongod/mongos 启动参数:** `--networkMessageCompressors "snappy,zstd,zlib"` * **客户端驱动连接字符串:** `compressors=snappy,zstd,zlib` * **协商机制:** 客户端与服务端会协商使用双方都支持的最高优先级算法。 --- ### **3. 文件系统级压缩** * **原理:** 虽然 MongoDB 本身不直接提供此功能,但可以部署在支持**透明文件系统压缩**(如 ZFS, Btrfs, NTFS)的磁盘分区上。 * **特点:** * **二次压缩:** MongoDB 压缩后的 WiredTiger 数据文件 (.wt) 会被文件系统再次压缩。 * **适用场景:** 对冷数据、归档数据、历史库进行极致压缩。 * **权衡:** 可能带来**显著的 CPU 开销**,并且在进行 MongoDB 数据文件操作(备份、重启、修复)时可能导致文件系统级操作变慢。**不推荐生产 OLTP 系统首选**。 --- ### **4. 时间序列集合的专用压缩(Time Series Collections - MongoDB 5.0+)** 专门为时间序列数据(如监控指标、IoT 传感器数据)设计,采用了更优化的存储格式: * **列式存储倾向:** 数据按时间窗口组织,相同时间点的多个测量值存储在一起,有利于应用列式压缩。 * **特殊算法:** 使用改进的 `FPACK`算法,针对数值型数据(尤其是相邻点变化不大的时序数据)提供**远超普通集合的压缩效率**(通常可达到 90%+ 的压缩比)。 * **自动启用:** 在 MongoDB 5.0+ 创建时间序列集合时自动启用,无需额外配置。 --- ### **选择建议与实践** 1. **默认设置优先:** 对于大多数应用,使用 `snappy` 压缩集合和索引已是良好平衡。 2. **空间 vs CPU:** 若磁盘空间极其宝贵且系统 CPU 资源充足,可考虑 `zstd` 或 `zlib`。 3. **时序数据:** **务必使用时间序列集合**以获得最佳压缩和查询性能。 4. **索引压缩:** 对于前缀重复性高的索引字段(如日期、状态码、分类ID),前缀压缩效果显著,默认开启即可。 5. **传输压缩:** **强烈建议启用**(至少 `snappy`),特别在跨地域或带宽受限环境中。 6. **监控:** 使用 `db.serverStatus().wiredTiger.cache` 和系统监控工具观察压缩效果及 CPU 开销。 --- ### **相关问题** 1. `snappy`、`zlib` 和 `zstd` 压缩算法在 MongoDB 中的具体性能对比(压缩比 vs CPU 开销)如何?生产中应如何选择? 2. MongoDB 时间序列集合(Time Series Collections)相比普通集合在数据压缩效率上具体提升了多少?其底层 `FPACK` 算法有何独特优势? 3. 如何监控 MongoDB WiredTiger 存储引擎的压缩效果和带来的 CPU 开销?关键指标有哪些? 4. 在启用 MongoDB 压缩的情况下,使用文件系统级压缩(如 ZFS)是否能带来额外收益?是否存在性能风险? 5. MongoDB 的传输压缩在网络延迟较高的分布式环境中能带来多大的性能提升?如何配置最优压缩算法链? 6. MongoDB 前缀压缩(字典压缩)主要优化哪种类型的数据存储?它对索引查询性能有何影响? [^1]: 本文基于 MongoDB 官方文档及 WiredTiger 存储引擎技术细节综合阐述。 [^2]: 时间序列集合压缩数据来源于 MongoDB 5.0 官方性能测试报告。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值