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