深入解析Linux下的磁盘缓存机制与SSD的写入放大问题

时间:2022-01-21 06:44:54

前段时间在开发一个使用SSD做缓存的系统,在高速写入数据时会出现大量的磁盘缓存。太多的磁盘缓存如果没有及时的写入磁盘中,在机器出现问题时是非常危险的,这样会导致很多的数据丢失,但是如果实时的将数据刷入磁盘中,这样写入效率有太低了。为了弄明白Linux系统的这种磁盘写入特性,最近深入的学习了一下。
    VFS(Virtual File System)的存在使得Linux可以兼容不同的文件系统,例如ext3、ext4、xfs、ntfs等等,其不仅具有为所有的文件系统实现一个通用的外接口的作用,还具有另一个与系统性能相关的重要作用——缓存。VFS中引入了高速磁盘缓存的机制,这属于一种软件机制,允许内核将原本存在磁盘上的某些信息保存在RAM中,以便对这些数据的进一步访问能快速进行,而不必慢速访问磁盘本身。高速磁盘缓存可大致分为以下三种:
目录项高速缓存——主要存放的是描述文件系统路径名的目录项对象
索引节点高速缓存——主要存放的是描述磁盘索引节点的索引节点对象
页高速缓存——主要存放的是完整的数据页对象,每个页所包含的数据一定属于某个文件,同时,所有的文件读写操作都依赖于页高速缓存。其是Linux内核所使用的主要磁盘高速缓存。
正是由于缓存的引入,所以VFS文件系统采用了文件数据延迟写的技术,因此,如果在调用系统接口写入数据时没有使用同步写模式,那么大多数据将会先保存在缓存中,待等到满足某些条件时才将数据刷入磁盘里。

内核是如何将数据刷入磁盘的呢?在看完以下两点后就能得到答案。

1. 把脏页写入磁盘
    正如我们所了解的,内核不断用包含块设备数据的页填充页高速缓存。只要进程修改了数据,相应的页就被标记为脏页,即把它的PG_dirty标志位置。
    Unix系统允许把脏缓冲区写入块设备的操作延迟执行,因为这种策略可以显著地提高系统的性能。对高速缓存中的页的几次写操作可能只需对相应的磁盘块进行一次缓慢的物理更新就可以满足。此外,写操作没有读操作那么紧迫,因为进程通常是不会因为延迟写而挂起,而大部分情况都因为延迟读而挂起。正是由于延迟写,使得任一物理块设备平均为读请求提供服务将多于写请求。
一个脏页可能直到最后一刻(即直到系统关闭时)都一直逗留在主存中。然而,从延迟写策略的局限性来看,它有两个主要的缺点:
  一、如果发生了硬件错误或者电源掉电的情况,那么就无法再获得RAM的内容,因此,从系统启动以来对文件进行的很多修改就丢失了。
  二、页高速缓存的大小(由此存放它所需的RAM的大小)就可要很大——至少要与所访问块设备的大小不同。
因此,在下列条件下把脏页刷新(写入)到磁盘:
页高速缓存变得太满,但还需要更多的页,或者脏页的数量已经太多。
自从页变成脏页以来已过去太长时间。
进程请求对块设备或者特定文件任何待定的变化都进行刷新。通过调用sync()、fsync()或者fdatasync()系统调用来实现。
缓冲区页的引入是问题更加复杂。与每个缓冲区页相关的缓冲区首部使内核能够了解每个独立块缓冲区的状态。如果至少有一个缓冲区首部的PG_Dirty标志被置位,就应该设置相应缓冲区页的PG_dirty标志。当内核选择要刷新的缓冲区时,它扫描相应的缓冲区首部,并只把脏块的内容有效的写到磁盘。一旦内核把缓冲区的所有脏页刷新到磁盘,就把页的PG_dirty标志清0。

2. pdflush内核线程
    早期版本的Linux使用bdfllush内核线程系统地扫描页高速缓存以搜索要刷新的脏页,并且使用另一个内核线程kupdate来保证所有的页不会“脏”太长时间。Linux 2.6用一组通用内核线程pdflush替代上述两个线程。
这些内核线程结构灵活,它们作用于两个参数:一个指向线程要执行的函数的指针和一个函数要用的参数。系统中pdflush内核线程的数量是要动态调整的:pdflush线程太少时就创建,太多时就杀死。因为这些内核线程所执行的函数可以阻塞,所以创建多个而不是一个pdflush内核线程可以改善系统性能。
根据下面的原则控制pdflush线程的产生和消亡:
必须有至少两个,最多八个pdflush内核线程
如果到最近的1s期间没有空闲pdflush,就应该创建新的pdflush线程
如果最近一次pdflush变为空闲的时间超过了1s,就应该删除一个pdflush线程
所有的pdflush内核线程都有pdflush_work描述符,其数据结构如下:

深入解析Linux下的磁盘缓存机制与SSD的写入放大问题

当系统没有要刷新的脏页时,pdflush线程会自动处于睡眠状态,最后由pdflush_operation()函数来唤醒。那么在这个过程中pdflush内核线程主要完成了哪些工作呢?其中一些工作与脏数据的刷新有关。尤其是pdflush通常执行下面的回调函数之一:
    1. background_writeout(): 系统地扫描页高速缓存以搜索要刷新的脏页。
为了得到需要刷新的脏页,就要彻底的搜索与在磁盘上有映像的索引节点相应的所有address_space对象(是一棵搜索树)。由于页高速缓存可能有大量的页,如果用一个单独的执行流来扫描整个高速缓存,会令CPU和磁盘长时间繁忙,因此,Linux使用一种复杂的机制把对页高速缓存的扫描划分为几个执行流。当内存不足或者用户显式的(用户态进程发出sync()系统调用等)调用请求刷新操作时会执行wakeup_bdflush()函数。wakeup_bdflush()函数会调用pdflush_operation()唤醒pdflush内核线程,并委托它执行回调函数background_writeout()。background_writeout()函数有效的从页高速缓存中获得指定数量的脏页,并把它写回磁盘。此外,执行background_writeout()函数的pdflush内核线程只有在满足以下两个条件下才能被唤醒:一是对页高速缓存中的页内容进行了修改,二是引起脏页部分增加到超过某个脏背景阈值。背景阈值通常设置为系统中所有页的10%,不过可以通过修改文件/proc/sys/vm/dirty_background_ratio来调整该值。
    2. wb_kupdate():检查页高速缓存中是否有“脏”了很久时间的页,避免当一些页很久没有被刷新时发生饥饿危险。
 内核在初始化期间会建立wb_timer动态定时器,其的定时间距为dirty_writeback_centisecs文件中所规定的几百分之一秒(通常是500分之一秒,不过可以通过修改/proc/sys/vm/dirty_writeback_centisecs文件调整该值)。定时器函数会调用pdflush_operation()函数,然后将wb_kupdate()函数的地址传入。wb_kupdate()函数遍历页高速缓存搜索陈旧的脏索引节点,把已保持脏状态时间超过30秒的页都写到磁盘,之后重置定时器。

PS:关于SSD的写入放大问题


    现在固态硬盘正在越来越多地被作为服务器磁盘使用。之前在SSD(Solid State Drive)上设计并实现缓存系统用于存储数据块时遇到了一些问题,比如在磁盘写满后,如果老化掉一些最久未使用的数据块后,继续大量写入新的数据,随着时间的推移,写入速度变得比刚开始时慢了许多。为了弄清楚为什么会出现这样的情况,于是在网上搜索了一些关于SSD的资料,原来这种情况是由于SSD硬件设计本身决定的,最终映射到应用程序上该中现象称为写入放大现象(WA: Write amplification),WA是闪存及SSD相关的一个极为重要的属性,这个术语在2008年被Intel公司和SiliconSystems公司(于2009 年被西部数据收购)第一次提出并在公开稿件里使用。下面简单讲解一下为什么会出现这种情况以及是怎样一个过程。
    SSD的设计完全不同于传统的机械磁盘,其是一个完整的电子设备,没有传统机械盘的读写磁头。因此,在读写数据的时候由于少了磁头在磁道之间的寻道过程所以SSD能提供较高的IOPS性能。也正因为其少了磁头的调度,所以SSD还能减少电量的使用,在数据中心中使用对企业来说是非常有益的。
    SSD与传统磁盘相比其有了很大的性能优势,以及较多的优点,但是事物总是有两面性的,其也自身也存在一些问题,写入在SSD中的数据是不可以直接更新的,只能通过扇区覆盖重写,在覆盖重写之前需要先擦除,而且擦除操作又是不能在扇区上做的,只能在磁盘的块上来完成,擦除块之前需要将原有的还有效的数据先读出,然后在与新来的数据一起写入,这些重复的操作不单会增加写入的数据量 ,还会减少闪存的寿命,更吃光闪存的可用带宽而间接影响随机写入性能。
深入解析Linux下的磁盘缓存机制与SSD的写入放大问题

写入放大的解决办法
    在实际操作中我们很难完全解决掉SSD写入放大的问题,只能通过某些方法来更有效的减少放大的倍数。一个很简单的办法就是将一块大的SSD硬盘只使用其的一部分容量,比如128GB你只是用64GB,那么最坏的情况就是写入放大能减少约3倍。当然这种方法有点过于浪费资源了。另外还可以在写数据时采用顺序写入,当SSD被顺序写入时,其写入放大一般为1,但某些因素会影响到该值。
    除了上面的方法外,现阶段公认的比较好的方法是TRIM。TRIM位于操作系统层。操作系统使用TRIM命令来通知SSD某个page的数据不需要了,则可以回收。 支持TRIM的操作系统和以往的主要区别是删除一个Page的操作不同。在磁盘时期,删除一个page之后在文件系统的记录信息里将该page的标志位设置为可用,但是并没有将数据删除。使用SSD且支持TRIM的操作系统,在删除一个page时,会同时通知SSD这个page的数据不需要了,SSD内部有一个空闲时刻的垃圾收集进程,在空闲时刻SSD会将一些空闲的数据集中到一起,然后一起Erase。这样每次写操作,就在已经Erase好了的Page上写入新的数据。

    虽然其存在写入放大的问题,但是这并没有让我们拒绝使用它。用其来做缓存加速已在很多项目中得到使用,尤其是在数据库缓存项目中,在这些项目中SSD高效的读性能得到了充分的利用。随着Facebook的开源项目Flash Cache的发布,以及Facebook内部的大量使用,Flash Cache已经成为了一个比较成熟的技术方案,这使得更多的公司选择了SSD来做存储或者缓存。