MONGODB 的存储引擎更快,更高,更强的秘诀 --译_python

在过去的20年里,存储硬件的性能提高了两个数量级,首先固态存储系统 SSD 的引入,同时SATA导向了PCIE 的接口方式,最终非易失性的技术以及制造工艺的创新。2019年4月,Intel 发布了首款商用存储SCM,它基于傲腾基础上的持久性内存,基于3D XPOINT 技术,建立在内存总线上降低了I/O方面的延迟。

之前设备访问经常在I/O上习惯遇到延迟,软件系统运行性能经常卡在存储系统的历史正在变得成为过去。这样的结果在学术界的研究和对于操作系统以及文件系统都在经历着改变。尽管这样,主流的操作系统和软件还是跟不算硬件的发展和革命,即使在目前的SCM技术行操作系统最大的消耗还在I/O设备。

对于这样的问题的和挑战,学术界提出一个新的用户级别的文件系统,SplitFS,通过这个文件系统可以大大减少在IO方面的损耗,可惜的是在使用这样的一个文件系统上,对于商用系统来说,除了考虑稳定性和操作的正确性以外,还有这个系统的可移植性,目前这个系统仅仅支持LINUX和 ext4-dax之上进行部署。

可喜的是,这里基于软件的存储引擎可以切入I/O系统的性能优化问题,MONGODB 的存储引擎 WiredTiger, 我们可以基于目前的状态,在不失去可移植性的基础上,去提供使用传统文件系统上来提供更高的性能。我们主要的挑战来自于使用内存映射文件进行I/O和批处理去操作文件系统。在主流ssd上的65个基准测试中,我们的这些改变在19个测试中给出了性能提高了63%结果。

简化IO操作的WiredTiger 

我们的改变来自于WiredTiger中使用了一项基于UCSD 的研究,根据这项研究的人员指出,通过内存映射文件的方式,同时在文件中使用一些额外的空间进行预分配,这样的方式下可以视为没有文件系统的牵绊,大大的提高了性能。

内存映射文件

内存映射文件的工作的原理:应用系统调用MMAP 系统,通过调用请求操作系统“映射”它的虚拟地址空间的一个块到它选择的文件中一个相同大小的块(图1中的步骤1), 当他第一次访问虚拟地址中映射的内存空间 会进行如下的操作

MONGODB 的存储引擎更快,更高,更强的秘诀 --译_linux_02

1   在系统还未访问虚拟地址前,操作系统会掌握硬件的控制权

2   操作系统将控制有效的虚拟地址,并且通过虚拟地址访问文件系统,将页面读入到 buffer cache 中

3   此时通过虚拟页面和物理页面的对应的表来访问相关信息,

4   最终虚拟和物理地址的对应关系会加载到 TLB ,应用程序通过这个对应的列表来进行数据访问。

对于后续访问虚拟页面是否访问操作系统需要看如下的条件

1  如果物理页面的数据仍然在BUFFER CACHE 中并且在TLB对应表中包含相关的地址,则不需要访问操作系统,数据将通过常规的方式进行加载和访问。

2  如果页面包含的数据依然在BUFFER CACHE 中,但是TLB 中的对应的地址已经不存在了,则硬件将转换到内核模式,通过遍历页表查找条目,将其导入到TLB中,然后让软件使用常规的加载或存储指令访问数据。

3  如果包含文件数据的页不在缓冲区缓存,硬件将进入操作系统,操作系统将要求文件系统获取页面,设置页表条目,然后重复上面的 2 号的操作

反过来,系统每次调用文件的时候,都需要通过用户与系统之间的通道来进行调用,通过内存映射IO的方式对比通过系统堆栈的方式调用,明显通过后者的调用要更有效率。内存映射IO的调度和数据的返回操作在CPU 方面是没有消耗的。因此如果数据从内存文件映射区域到其他的应用BUFFER ,典型的是通过基于AVX 的MEMCP 方式加载的。当数据从核心区域加载到用户的空间,内核工作的效率可能不高,主要的原因是此时不能使用AVX 的方式。

预分配文件空间

内存和文件的映射允许我们大大的减少固定大小文件的在操作系统和文件系统之间的交互,但如果文件是增长的,我们需要引入文件系统,文件系统将更新文件的metadata,并且确保这些更新如果在系统崩溃时仍然不会丢失。

这里确保崩溃一致是一种非常的大的消耗,这里每个日志记录的持久化存储到存储中,去确保发生崩溃时文件不会丢失。但如果我们将文件碎片化,就会放大开销。这里就可以解释为什么SplitFS和UCSD研究中实现在应用程序扩展文件时都预先分配了一大块文件。根本上这个策略在批量处理文件系统的操作,减少它们的cost。

WiredTiger 怎么实现优化

那么MONGODB的数据库引擎 WiredTiger是怎么优化这方面的操作,开发人员将idea分成两个部分入手。第一,这里将映射文件区域做了固化的处理的设计。同时,在验证这个简单的设计有效并可以产生性能的提升后,针对文件变化的特性中,添加了在文件增长或收缩时重新映射文件的特性。该特性需要高效的线程间的协调,也是整个设计中困难的部分——后面我们将重点解读这方面的设计。

这里我们的变化已经在WiredTiger开发分支的测试中。截至撰写本文时,更改仅针对POSIX系统,时间点是2020年1月,针对WINDOWS 端口我们有这方面的计划。

假设一个规定大小的文件映射的区域

这一部分实现的代码的工作量较低,WiredTiger对与文件相关的操作提供了封装,这里仅仅改变封装的方式,在打开文件后,我们开始通过MMAP 来产生映射虚拟地址的空间,随后调用封装器来读取或写入文件,将其部分的数据copy 到映射的buffer 中。

这里WiredTiger允许三种方式来增加和收缩文件的大小,文件的扩展可以通过fallocate系统函数来调用,如果写入的文件大小已经超过原先的设定,则可以通过隐式的方式增加,当然也可以通过truncate 系统来将其收缩,在我们最初的设计中,我们是不允许文件显示的进行扩大和缩小的,如果担心数据引擎对文件操作的正确性,实际上是没有必要的,如果操作的文件写入映射去的文件已经超过原先的映射,那么封装会现将可以调入的文件先调入,后面的超出的部分,可以持续的在后面进行调用。

当然这个设计在最初作为原型还是可以接受的,但后续发现对于生产系统来说,限制太多。

重新设置映射文件区域

摆在这里有一个有难度的问题,同步。我们可以想象一下,引入两个线程,一个读取文件,另一个要截断文件。第一个读的线程首先检查mapped buffer确认读取的mapped buffer 的边界。假设此时他要将数据从mapped buffer中拷贝数据,然后第二个线程操作被干扰,原因是他的操作要截断文件,导致新的文件的尺寸小于第一个线程检测到的边界。如果此时第一个线程拷贝数据,那么结果就是导致crash,这是因为在截断之后,映射的缓冲区比文件大,如果试图从缓冲区延伸到文件末尾以外的部分复制数据,将会产生分割错误。

很明显如果要解决这个问题,需要在每次获取mapped buffer时添加一个锁,无论是在这个文件被访问还是被修改的时候,而这样的操作将会导致串行I/O 这将引起性能的问题,相反,我们使用了一个受RCU (read-copy-update)启发的无锁同步协议,来把所有可能改变文件大小的线程称为写入器。这里写入对文件的改变无论是扩展还是截断,都会经过 fallocate system 的调用,任何的读线程都会读这个写入器写入的文件。

我们的解决方案如下,写入器首先执行改变文件大小的操作,然后将文件重新映射到虚拟地址空间,在这个期间我们实际是不希望有人访问mapped buffer,包含读写操作,但我们这里并不需要阻止所有的I/O在这段期间发生操作。在这个期间写入器正在操作映射的缓冲区可以简单的将IO的操作路由到系统的调用,而系统的调用在内核中与其他的文件操作是同步的。

为了达到没有锁的目标,我们依赖两个变量

mmap_resizing:  当一个写入操作想去执行排他的mapped buffer 的操作,我们会自动设置这个标志位。

mmap_use_count: 一个读操作在使用映射缓冲区之前应该将这个计数器加1,在使用映射缓冲区之后在将其减1。这个计数器告诉我们是否有人正在使用缓冲区。写入器等待直到计数器变为0才能继续。

在调整文件和映射缓冲区的大小之前,写入者执行函数prepare_remap_ resize_ file,本质上。写入操作有效地等待,直到没有其他人在调整缓冲区的大小,然后设置调整大小标志位以声明对该操作的独占权限。然后,它等待直到所有的读取器都使用缓冲区完成。源代码如下:

prepare_remap_resize_file:
    
wait:
    /* wait until no one else is resizing the file */
    while (mmap_resizing != 0)
        spin_backoff(...);
    
    /* Atomically set the resizing flag, if this fails retry. */
    result = cas(mmap_resizing, 1, …);
    if (result) goto wait;
    
    /* Now that we set the resizing flag, wait for all readers to finish using the buffer */
    while (mmap_use_count > 0)
        spin_backoff(...);

在执行prepare_remap_resize_file之后,写入操作执行文件的大小调整操作,通过取消映射缓冲区,用新的大小重新映射,同时重新设置调整标志的大小。下面为这段的代码:

read_mmap:

    /* Atomically increment the reference counter, 
     * so no one unmaps the buffer while we use it. */
    atomic_add(mmap_use_count, 1);

    /* If the buffer is being resized, use the system call instead of the mapped buffer. */
    if (mmap_resizing)
        atomic_decr(mmap_use_count, 1);
        read_syscall(...);
    else
        memcpy(dst_buffer, mapped_buffer, …);
        atomic_decr(mmap_use_count, 1);

这里标记以下,在写文件的线程工作时必须同时执行读操作的同步(就像在read_mmap中的操作),通过查看它们是否可以使用内存映射缓冲区进行I/O操作和在写文件超过文件末尾的情况下来执行写入操作的同步。请参考WiredTiger开发分支获得完整的源代码。以下为相关的链接

https://github.com/wiredtiger/wiredtiger/tree/develop

批量文件操作

就像我们早期提到的,UCSD 的研究对我们的启发,其中的需要通过在大块中预先分配文件空间来批处理耗时耗力的文件操作。这里对WiredTiger数据库引擎的实验中证明,我们使用这种策略后的效果,这里做了比较两种配置的实验:(1)在默认配置中,WiredTiger使用fallocate系统调用来增长文件。(2)WiredTiger不允许使用fallocate,系统无法对隐式增长的文件在写入超过其末端的时候进行特殊的处理。我们统计两种情况下文件系统调用的数量,发现默认配置下的文件系统调用数量至少比没有提供这个功能的配置小一个数量级。通过这个实验我们验证了WiredTiger已经在文件的处理中在这个方面有了很大的提高。未来我们正在筹划是否可以对批处理操作进行优化来获得更大的性能提升。

性能提升

为了验证我们工作的有效性,我们通过WiredTiger基准套件WTPERF上比较了mmap分支和传统方式调用的性能差异。WTPERF是一个可配置的基准测试工具,它可以模拟各种数据布局、模式和访问模式,同时支持各种数据库配置。在65个工作负载中,我们提高了19个的性能指标。其余工作负载的性能要么保持不变或变化不大。两种工作负载(UPDATE  log-structured 的工作负载)的性能差异增加了几个百分点,其他的部分没有不同。

下面的图展示了mmap分支相对于19个基准测试的性能改进(以百分比为单位)和差异,实验运行在一个带有Intel Xeon处理器E5-2620 v4(8核)、64GB RAM和Intel Pro 6000p系列512GB SSD磁盘的系统上。我们对所有基准测试都我们使用默认设置,每个基准测试至少运行三次,以确保结果在统计上是显著的。

MONGODB 的存储引擎更快,更高,更强的秘诀 --译_数据库_03

总的来说,这些工作负载有显著的性能改进。于此有稍许不同的是对于500m-btree-50r50u和update-btree, mmap的一些操作(例如更新或插入)要慢一些,但其他操作(通常是读取)要快得多,这里我们会持续的研究这些没有获得“好处” 的操作如何能获得更大的性能提升。

其中mmap提高性能的主要原因之一是I/O速率的提高,例如,对于500m-btree-50r50u工作负载(该工作负载模拟典型的MongoDB负载),mmap的读I/O效率比系统调用高出30%左右。当然统计数据并不能完整的解释所有的结果:因为在使用mmap时候,工作负载的读吞吐量比使用系统调用时高63%。这里提出一个假设,其余的差异是由于内存映射I/O的效率的改变引起的差异,实际上,当我们使用mmap的时候对于CPU的利用率都比较高。

结论:

基于存储技术的根本性颠覆,更高的吞吐的和更低延迟的存储系统相对CPU的处理速度来来说对于系统的性能的提高更有效,更快的存储系统让软件设计的问题显露无疑,我们根据这个问题,将关注的焦点转移到系统调用与文件系统访问消耗上的开销的问题,并且发现了如何来提高系统访问的效率。

这里我们对wiredTiger 存储引擎的改变提高了63%读操作的性能,关于更多的优化,您可以查看我们关于WiredTiger 开发分支中的os_posix目录下的文件os_fs.c和os_fallocate.c 的代码。

原文

https://www.mongodb.com/blog/post/getting-storage-engines-ready-fast-storage-devices

MONGODB 的存储引擎更快,更高,更强的秘诀 --译_java_04

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: go