MongoDB的WiredTigerLAS.wt大小异常分析

2020-08-25 11:35:12 浏览数 (1)

背景

最近在运维MongoDB时遇到一个磁盘空间增长异常的问题,主要是WiredTigerLAS.wt这个文件占用了70GB以上的空间。经排查,有不少用户都遇到过这个问题,其背后的根本原因和MongoDB的一个bug有关。本篇文章会详细分析这个问题背后的原因以及涉及到的相关技术原理,并给出解决方法。

WiredTigerLAS.wt的来龙去脉

首先我们看下这个WiredTigerLAS.wt文件是干什么用的。从后缀名上可以知道这是WiredTiger的一个表文件,并且是一个系统表文件。通过搜索相关JIRA可以得知这个文件和WiredTiger的Cache evict相关,因此这里先简单介绍一下WiredTiger的Cache evict相关原理。

WiredTiger的Cache evict原理

WiredTiger的B-tree、Page、Extent基本介绍

我们知道,WiredTiger使用B-Tree来组织数据,并且在内存和磁盘上使用了不同的page格式,引用友东大神之前画的两张图做个介绍,让大家先有个大致的概念(可以先不关注图中细节):

图1 WiredTiger Btree、Page、Extent

图2. WiredTiger Page Internals其中,在磁盘上的page被称为extent。当内存压力小时,extent会在内存中也缓存一份,以减小用户的访问延迟。它由page header、block header和kv列表三部分组成,其中page header存储了extent经过解压和解密后在内存中的大小。block header存储了extent在磁盘中的大小和checksum值。kv列表中的key和value都是由cell和data两部分组成,cell存储了data的数据格式和长度,data就是具体的k/v值。

WiredTiger内存中的一个leaf page能保存的数据大小是有上限的,这由一个leaf_value_max的参数决定,长度超过leaf_value_max的数据被称为overflow data。WiredTiger会将该overflow data保存为一个单独的overflow extent存在表文件中,并用overflow extent address(包含overflow extent的offset和size)替换该value值。这里可以看到,访问overflow extent时需要一次额外的磁盘IO,性能肯定不如普通的extent,因此,在MongoDB中,配置的leaf_value_max的值是64MB,由于mongodb的文档大小不会超过16MB,所以不会出现产生overflow extent的情况。

WiredTiger的事务可见性基本原理

接下来我们还需要简单了解一下WiredTiger中事务可见性的一些基本原理。WiredTiger的事务隔离是基于snapshot技术来实现,支持read uncommited、read commited和snapshot三种隔离级别。

在WiredTiger的内存page中,对这个page的修改会通过Insert和Update列表组织在一起,如上图2所示(不少WiredTiger相关的原理文章中都有介绍,此处不再赘述)。总的来说,一个内存page由『已持久化』(缓存在内存中的extent)和『未持久化』这两种文档组成,而未持久化的文档又分为『未提交』 和『已提交』的文档。未提交的文档都是『部分事务可见』的文档(修改文档的事务可见,其他read commited或snapshot事务不可见),而已提交的文档分为『所有事务可见』、『 部分事务可见』和『所有事务不可见』三类。对于同个key,按照更新顺序从新到老,已提交文档的顺序是:部分事务可见 < 所有事务可见 < 所有事务不可见。若一个key在某个时刻的值是所有事务可见的,则早于该时刻的旧值肯定就是所有事务都不可见的,所以对于同一个key,只会有一个文档是所有事务可见的。

那怎么判断哪些已提交的文档是所有事务可见的,哪些是部分事务可见的,哪些又是所有事务都不可见的呢?这里是通过事务id来进行判断的(MongoDB 4.0开始增加了时间戳的判断,由于和本文关系不大,此处进行简化说明),基本规则就是遍历上面的key的Update列表,根据Update产生的事务id和当前事务的snapshot列表进行比较来判断。

参照下图,我们举例说明,假设所有事务都是使用snapshot隔离级别,现在事务t1、t2、t3、t5已经提交,只有事务t10还在运行中,而在事务t10开始时,事务t1和t2已经提交,但事务t3和t5还未提交。那么事务t10无法看见事务t3和事务t5所更新的文档,即使他们在t10运行的过程中变成提交状态。对应到图中也就是{Key1, Value1-3}和{Key3,Value3-1}对于事务t10是不可见的,只有早于事务t3提交的文档{Key1, Value1-2}、{Key3, Value3}才是所有事务可见的文档,而更早的{Key1, Value1-1}则是所有事务不可见的文档。

下图是上面这个例子对应到内存中leaf page的结构,它包含了已持久化的文档(Leaf Extent中的文档)和未持久化的文档(Modify中的文档),其中的紫色文档是部分事务可见的未提交的文档,红色文档是部分事务可见的已提交的文档,绿色文档是所有事务可见的已提交的文档,灰色文档是所有事务不可见的已提交的文档。

WiredTiger page逐出的方式

WiredTiger在以下4种情况下会进行page逐出:1)cursor在访问btree的page时,当发现page的内存占用量超过了memory_page_max(MongoDB的配置值是10MB),就会对它做逐出操作,以减小page的内存占用量。2)后台eviction线程根据lru queue排序逐出page3)内存压力大时,用户线程会根据lru queue排序逐出page4)checkpoint会清理它读进cache的page

Page逐出在WiredTiger代码中是在wt_evict中实现的。wt_evict会依据当前cache使用率情况,分为内存使用低逐出和内存使用高逐出两种。若内存使用率bytes_inmem/cache_size < (eviction_target eviction_trigger)/200 且 脏页使用率bytes_dirty_leaf/cache_size < (eviction_dirty_target eviction_dirty_trigger)/200,则被认为是内存使用低,反之是内存使用高。

在WiredTiger中,eviction_target默认值是80,eviction_trigger默认值是95,eviction_dirty_target默认值是5,eviction_dirty_trigger默认值是20,所以内存使用低逐出的判断规则也就是:内存使用率<87.5%且脏页使用率<17.5%。

内存使用低逐出

它主要有3个目标:1) 从page中未持久化的文档(Modify中的文档)里,删除『所有事务不可见的已提交文档』,减少内存的占用。2) 将page中最新的『已提交文档持久化到磁盘,以减少下一次checkpoint所需的时间。3) 在内存中依然保留该page,避免下次操作读到该page时需要访问磁盘,增大访问延迟。

内存使用低逐出会先将最新的『已提交文档』以extent格式逐出到表文件tablename.wt中,同个key的文档只会持久化一个value值。如果page的modify中有新写的key或者对已有key的更新,那么取出最新的已提交value值,若没有,则从leaf extent中取出其原始的value值。当内存使用率低时,最新的已提交文档在内存中会组成新的extent并替代老的extent关联到page上,并释放内存中老的extent。最后从modify中删除『所有事务不可见的已提交文档』。

如下图所示,最新的已提交文档是『部分事务可见的已提交的文档』(红色文档),也就是说红色文档{Key1,Value1-3}和{Key3,Value3-1}会被更新到新的leaf extent中,删除所有事务不可见的文档Value1-1,而其他的文档仍然要保留在modify中。

那最新的已提交的文档Value1-3和Value3-1都已持久化了,为什么还要在modify中保留它们呢?因为Value1-3和Value3-1是部分事务可见的,对于其它不可见的事务来说,它们还需要看到其之前的文档Value1-2和Value3,所以这些文档依然都要保留。

内存使用高逐出

它主要有2个目标:1) 尽可能删除内存中page的modify和extent,大幅减少内存的占用。如果因含有未提交的文档,而无法删除modify和extent。那么也要降级为内存使用低逐出,适当减少内存的占用。2) 将page中最新的『已提交文档』持久化到磁盘,并减少下一次checkpoint的时间。

内存使用高逐出根据page的modify包含的文档种类不同,对应有不同的处理方式。具体有以下三种情况:1)page的modify不包含『部分事务可见的文档』2)page的modify不包含『部分事务可见的未提交的文档』3)page的modify包含『部分事务可见的未提交的文档』

我们先介绍page的modify不包含『部分事务可见的文档』的情况,也就是说page的modify中只包含所有事务可见的已提交文档(绿色文档),和所有事务不可见的已提交文档(灰色文档),如下图所示。由于所有事务不可见的已提交文档(灰色文档)是需要被删除的,这样modify中所剩下的『所有事务可见的已提交文档』(绿色文档)就是最新的『已提交的文档』,而内存使用高逐出会将这些绿色文档组成新的extent逐出到表文件tablename.wt中,所以modify中的数据都已被逐出,不再需要保留。最后清理内存中page的extent和modify,并将新的extent在文件中的offset和size关联到page上,便于下次访问时从磁盘中读出extent。

对于page的modify不包含『部分事务可见的未提交的文档』,也就是说page中只包含部分事务可见的已提交的文档(红色文档),所有事务可见的已提交文档(绿色文档),和所有事务不可见的已提交文档(灰色文档),如下图所示。内存使用高逐出不仅会将最新的『已提交文档』逐出到表文件tablename.wt中,而且还会判断是否满足使用las逐出的条件。我们将modify中除『所有事务不可见的已提交文档』(灰色文档)之外的其它文档逐出到表WiredTigerLAS中的过程叫做LAS逐出。文档被LAS逐出到表WiredTigerLAS之后,很快会被持久化到表文件WiredTigerLAS.wt中,以减少内存的使用。

在下图中,最新的『已提交文档』是部分事务可见的已提交的文档(红色文档),所以红色文档{Key1,Value1-3}和{Key3,Value3-1}会被更新到新的leaf extent中。除『所有事务不可见的已提交文档』(灰色文档)之外的其它文档就是部分事务可见的已提交的文档(红色文档)、所有事务可见的已提交文档(绿色文档),包括{Key1,Value1-3}、{Key1,Value1-2}、{Key3,Value3-1}、{Key3,Value3},它们会被写到表WiredTigerLAS中。

如果page的modify包含『部分事务可见的未提交的文档』,或者page的modify不包含『部分事务可见的未提交的文档』但不满足las逐出的条件,那么modify中的数据就不能被逐出,这就导致内存使用高逐出会降级为内存使用率低逐出。而内存使用率低在删除『所有事务不可见的已提交文档』后,还需要在内存中保留modify和extent,使得该page就只能释放少量的内存,但一个表中有很多page,可能某些page满足第一种情况page的modify不包含『部分事务可见的文档』,释放这种page后再次从磁盘中读取的代价较低,优先被逐出。

读取逐出的page

根据内存使用率低逐出和内存使用率高逐出的结果,逐出后的page有以下三种形式:1) page的modify和extent仍然在内存中2) page的extent在磁盘文件tablename.wt中3) page的extent在磁盘文件tablename.wt中,modify在表WiredTigerLAS中

第1种情况最简单,操作直接访问内存中page的文档即可。第2种情况需要先从磁盘上的tablename.wt文件中读出extent并关联到page上,然后才能访问。第3种情况在第2种情况的基础上,还要从表WiredTigerLAS中读取出该page的所有相关文档,并重建出page的modify。

LAS逐出

LAS逐出既然可以确保清理内存中的page,为什么内存高逐出方法不都采用LAS逐出呢?一方面是因为WiredTiger只有redo日志,要求文档只有提交后才能被持久化,而las逐出的文档在写入表WiredTigerLAS后就可以被持久化,所以包含『部分事务可见的未提交的文档』的page不可以执行las逐出。另一方面LAS逐出的代价较高,需要将『包含部分事务可见的已提交的文档』和『所有事务可见的已提交文档』一个一个写入表WiredTigerLAS中,后续若有操作访问该page时,还需要一个一个文档从表WiredTigerLAS中读取出来,所以基于读写性能考虑,进行las逐出的条件很苛刻。

LAS逐出不仅要page符合LAS逐出的条件,而且要整个cache的使用也符合LAS逐出的条件。先看下整个cache使用所需符合的LAS逐出条件:内存卡主超过2s(内存卡主是指 内存使用率bytes_inmem/cache_size > eviction_trigger/100 或 脏页使用率bytes_dirty_leaf/cache_size > eviction_dirty_trigger/100) 或 近期逐出的page更适合做LAS逐出(page中『部分事务可见的文档』/『未持久化的文档』>80%)。而page需符合LAS逐出的条件是page逐出不需要分页,且page的modify中所有文档都是『已提交的文档』。

LAS逐出过程通过cursor,在一个事务中将一个page的modify中『包含部分事务可见的已提交的文档』和『所有事务可见的已提交文档』写入表WiredTigerLAS。为了便于之后读取或者清理表WiredTigerLAS中的数据,写入表WiredTigerLAS的文档格式除了包含原始的key和value外,还需要保存更多的数据,如下图所示。page在LAS逐出时会有一个唯一的LAS逐出自增id,便于读取page时查找。page在LAS逐出时是先按照key从小到大遍历,对于每个key又按照update从新到旧遍历,且每个key最新的update类型会特殊标记为BIRTHMARK(例如文档{Key1,Value1-3}和{Key3,Value3-1}的类型),为了让LAS清理便于识别不同key的分界点。

表WiredTigerLAS文档的key:LAS逐出自增id,btree的id,本次LAS逐出的序号,原始key表WiredTigerLAS文档的value:update的事务id,update的事务开始时间,update的事务持久化时间,update的事务状态,update类型,原始value

LAS逐出一个page的所有文档时,是放在一个事务中的,为了保证原子性。为了性能考虑,使用了read uncommited隔离级别(由于read committed需要访问全局事务表,来分析哪些事务可见)。

LAS清理

逐出到LAS表里的key如何清理呢?它的清理是由一个后台LAS清理线程来完成的。该线程每隔2s会通过cursor遍历一遍表WiredTigerLAS,当它发现标记为BIRTHMARK的update时,它会检查该update对应的文档当前是不是所有事务可见的,如果是的话,那么就删除该key对应的update列表。所以LAS清理线程的目的是保证WiredTigerLAS.wt文件大小不会持续增加。

WiredTigerLAS.wt大小异常原因

至此,我们已经摸清了WiredTigerLAS.wt的来龙去脉。并且,我们还知道,有一个LAS清理机制可以保证WiredTigerLAS.wt文件的大小得到控制。那为什么用户还是遇到了WiredTigerLAS.wt文件大小异常的情况呢?原因是LAS清理逻辑有个bug,只有在有表被删除时,才会执行LAS清理逻辑。这个bug最近才被官方修复,并且修复之后,又暴露了一个之前被掩盖的数据不一致的bug,这和LAS清理使用的read uncommited隔离级别有关。

上面说了LAS逐出一个page的所有文档时,是放在一个事务中的。同样,LAS清理也是在一个事务中进行的。由于LAS逐出和LAS清理是并发执行,使用read uncommited隔离级别的LAS清理可能只清理了某个key的update列表中部分update的情况(例如清理了{Key1,Value1-3},保留了{Key1,Value1-2})。这是因为当LAS逐出在一个事务中先写完{Key1,Value1-3}时,LAS清理就能看到刚写的{Key1,Value1-3},这时如果发现{Key1,Value1-3}已经全局可见了,LAS清理就会清理update列表,而这时只清理{Key1,Value1-3},等LAS清理完并遍历到下一个key后,LAS逐出才继续写了{Key1,Value1-2},这样就导致在表WiredTigerLAS中残留value1-1的情况。而之后用户访问该page时就会出现读不到{Key1,Value1-3},只能读到{Key1,Value1-2}的情况,造成数据不一致(虽然在extent中有{Key1,Value1-3},但查找时会优先访问modify中的文档)。

这个bug的修复也比较简单,将LAS清理改为使用read commited隔离级别就可以了,这样LAS清理就不可能看到某个key的不完整的update列表的情况,也就不可能出现只清理了某个key的update列表中部分update的情况。

总结

本文详细分析了WiredTigerLAS.wt大小增长异常的原因,并介绍了相关技术原理,下面做个简要总结:

1) 表WiredTigerLAS的作用是当内存使用率高时用于临时存放不能被逐出到用户表文件中的数据,表WiredTigerLAS中的数据会被高优先级逐出到磁盘文件WiredTigerLAS.wt中,所以能达到减少内存使用的目的。

2)当WiredTiger内存使用率高且有大量并发写入时,它会使用内存使用率高逐出 LAS逐出方式来逐出page,而大量并发写入会使得page的modify中会包含很多『部分事务可见的已提交的文档』,LAS逐出最后会将这些文档逐出到表WiredTigerLAS中,造成文件WiredTigerLAS.wt持续有数据写入。

3)当MongoDB负载稳定的时候,LAS清理机制本来可以保证文件WiredTigerLAS.wt空间达到一定大小后就不再增加,但由于LAS清理执行时机的bug,造成写入的数据无法被删除,而又有新数据写入,造成文件WiredTigerLAS.wt大小持续增大。

4)LAS清理执行时机的bug被修复后,暴露了原来被掩盖的LAS清理和LAS逐出事务并发执行时可能导致数据出现不一致的问题,将LAS清理换为使用read commited隔离级别后得到解决。

最后,以上问题在最新的MongoDB版本(包括阿里云最新的MongoDB版本)中都已经得到了修复,请大家放心使用。

0 人点赞