WiredTiger 的数据缓存机制

大多数的存储引擎都有自带的缓存机制, 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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值