天天看点

MongoDB WiredTiger 存储引擎cache_pool设计 (上) -- 原理篇

mongodb v.3.0之前的版本,默认使用<code>mmap(mmap引擎)</code>方式对内存中的数据进行写盘存储,遭受了很多诟病。比如<code>并发受限的表锁、不支持压缩、不可控的io</code>操作等,mmap甚至不能称作一个完整的存储引擎(笔者的个人观点),对数据(btree的数据页、索引页)的操作甚至要依赖os的mmap(in_page_cache)刷盘,并且os的page 4k为io单元对数据库本身就是不友好的,再加上其实数据库自身应该比os更懂数据的layout,比如哪些是热数据,哪些是索引(较数据页更需要留在内存里)来组织lru。

wt引擎是基于btree索引实现的,和innodb引擎有很多设计和名词上相似的地方,大家可以类比。wt对外的api接口是以table(类比mysql的表和mongodb的collection) + kv存储形式提供的,key是int64_t类型(类似mongodb的_id),value是byte[]类型(这里通过bytearray来实现mongodb上层schema free的bson结构)。上层通过cursor方式来实现数据的search/insert/update/remove操作。内部通过journal_log、checkpoint、cache(btree/cache_evict)、block_manager子模块协调访问io存储(这里主要列出本文相关模块,sweep、transaction、bloom/lsm等未列出)

MongoDB WiredTiger 存储引擎cache_pool设计 (上) -- 原理篇

<code>session 模块</code>,负责和wt引擎上层交互的句柄,每个session会关联多个cursor,cursor属于一个session

<code>cache 模块</code>,主要有内存中的btree page(数据页,索引页,溢出页)构成

<code>evict 模块</code>,如果cache内存紧张,触发cache淘汰,便利btree,并根据lru排序淘汰

<code>journal 模块</code>,wal log,类似innodb的redolog,保证数据持久化,通过定时和定量阈值来flush

<code>checkpoint 模块</code>,类似innodb checkpoint机制,异步执行btree刷盘,checkpoint之后通知log模块更新log_ckpt_lsn(lsn概念和innodb一致)

<code>block manager模块</code>,负责磁盘io的读写,cache、evict、checkpoint模块均通过该模块访问磁盘

MongoDB WiredTiger 存储引擎cache_pool设计 (上) -- 原理篇

(图片来自wiredtiger github官方wiki)

基本page类型:<code>root_page</code>(btree根节点), <code>internal_page</code>(索引页), <code>leaf_page</code>(数据页, 叶子节点)

结构:每个page被包含在一个ref里面(可以看做是page的容器), root_page/internal_page 包含一个refarray数组,包含下层节点的指针,home(红色线)指向父节点

wiredtiger将磁盘上的page读上来之后,会在内存中构建成另外一种复杂结构,这个结构较binary结构好处就是可以重新组织或嵌入具有更高并发性的结构,比如wt使用的hazardpointer,对page刷盘时,其实就是对该页的hazardpointer的'写获取'操作,并且在刷盘时保持 原有磁盘上的page不变,直接找一个新的page空间,把内存里page的修改(保存在page的modify_list中)变成磁盘page的结构写入 ,这个page刷盘的过程称为<code>reconcile</code>(wt里很多生僻的名词)。这样的好处是对不修改原有page,就能更好的并发,并且不像innodb一样,需要一个doublewritebuffer保证非disk block 512b写时对原有页可能发生conrrupt。(这里有个小问题,如果是更新都写入新page,如果每次都只是更新page中很小的数据,数据的空间占用会比较大,待验证?!)

wiredtiger也提供了类似innodb的checkpoint机制:每个客户端的写请求会先通过<code>journal进行持久化,这里类似redolog都是顺序io</code>,并且提供了类似innodb_flush_log_at_trx_commit的{j: true}参数。那么,在wt里面产生的cache脏页,就用在后台'慢慢'的刷。当写入到一定程度或者时间后,或作一次checkpoint把cache中的数据刷入磁盘,并且做fsync, 然后通知journal更新checkpoint offset,即可丢弃之前的journal。这里在每次checkpoint后,都会产生一个新的root page(也就是一个新的btree,一个bree对应一个物理文件),同时会在journal写入这个checkpoint事件。

写请求写入journal后就可以保证durability,后续wt引擎遇到如下情况时,会触发cache的刷盘动作:

<code>checkpoint</code>

checkpoint时会遍历所有btree,把btree的所有leaf_page做reconcile操作,然后对重新分配root_page,触发的阈值包括:

journal容量达到阈值,默认2g

每隔60s执行一次

<code>cache evict</code>

evict线程会根据现在cache的使用量,分段扫描一些page(可能是drity page或clean page),进行淘汰,释放cache空间,主要受如下4个参数影响:

eviction_trigger:cache总使用量达到该百分比时,触发evict操作

eviction_target:触发上述参数evict后,需要将cache总使用量降低到该百分比水位,才停止evict

eviction_dirty_trigger:cache脏页使用量到该百分比时,触发evict操作

eviction_dirty_target:触发上述参数evict后,需要将cache脏页使用量降低到该百分比水位,才停止evict

(wiredtiger v2.8.1 一般情况使用情况eviction_trigger &gt; eviction_target &gt; eviction_dirty_trigger &gt; eviction_dirty_target,wiredtiger v2.9之后版本对着四个参数的意义进行了调整!)

<code>application evict</code>

如果写入量一直很大,那么用户请求处理线程就会阻塞并参与evict的执行,这也是一种保护措施,当io量很大时,做到同步阻塞上面的请求

cache evict在wt v2.8.1早期版本时,采用的是server-worker模型,1个server线程负责扫描btree找到一些page,然后进行lru排序,放入一个evict_queue中,再由worker线程消费,进行page evict动作。后面v2.8.1在mongodb v3.2.9这个版本中,对线程模型进行了升级, 将server线程和worker线程合并(worker通过抢一把evict_pass_lock锁来成为server),相当于n个worker线程,同一时刻,有一个worker会成为server,负责执行evict_pass(扫描btree,并填充evict_queue)的工作,较少了切换的代价 。并且把原来单一的evict_queue变成了两个,降低了server、worker之间操作一个queue的概率减少冲突,增加了并发。

虽然线程模型做了调整,但从功能上还是server-worker的模式,见下图:

MongoDB WiredTiger 存储引擎cache_pool设计 (上) -- 原理篇
MongoDB WiredTiger 存储引擎cache_pool设计 (上) -- 原理篇

每次evict_server执行cache pass是对所有btree的一次扫描,会根据当前cache情况最多扫描100个page出来。__btree的扫描是迭代的,结束时会记录当前扫描道德btree的位置,下次继续从这个地方开始walk,每个btree最多扫描出10个page,如果这个btree被exclusive独占,如checkpoint正在访问这个btree,则跳过__ ,这是为了让btree所有page都有可能被扫描到。如果扫描了足够多的page,则会停止本次cache pass,进入page的evict阶段。

扫描完成后的evict_queue,需要对每个里面page做<code>评分排序</code>,每个page的评分由<code>page-&gt;read_gen</code>(可理解成page的存活周期,如果经历过cache淘汰越多,但没被淘汰,就越高), 以及page类型共同决定,见代码:

按评分排序后,得到一个分数<code>从小到大的有序队列</code>,这个时候需要计算<code>candidate</code>(evict_page是即将被淘汰的页,candidate page是本次决定淘汰的页,evict &gt; candicate),继续上代码:

上述分析了cache相关的设计,其实可以看出evict还是有些精巧的地方,单还不够完善,尤其是在cache evict和checkpoint同时发生,或者evict_server不能找到可以evict的page时的退让策略,触发了一些线上问题,本文的下半节《mongodb wiredtiger 存储引擎cache_pool设计 (下) -- 实践篇》将对次进行深入讨论