第七章:缓存

与任何其他文件系统一样,ZFS使用内存缓存来提高性能。不过,与大多数其他文件系统不同,系统管理员可以调整这些缓存以调整系统行为。

ZFS在zpool.cache文件中缓存系统池列表。它可以使用缓存设备进行读写。

不过,最明显的缓存是ZFS的自适应替换缓存(Adaptive Replacement Cache)。

自适应替换缓存

从内存调用数据比从磁盘访问文件快得多。

类Unix操作系统通常在内存缓冲区缓存中保留最近访问的文件的副本。ZFS使用一种更复杂、更有效的缓存类型,自适应替换缓存(Adaptive Replacement Cache——ARC)。

了解ARC始于了解缓冲区缓存(buffer cache)。

传统缓冲区缓存

缓冲区缓存根据最近最少使用(Least Recently Used——LRU)算法选择要缓存的数据。

LRU是一个列表,按上次访问数据块的时间存储。每当使用对象时,它都会移动到列表的顶部。当缓存填满时,系统会从列表底部删除项目,直到有足够的空间在列表顶部插入新项目。

缓冲区缓存工作得足够好,可以提供性能提升,但在某些情况下LRU方法会导致不良行为。比如,每晚进行的备份,备份程序会扫描整个磁盘,查找自上次备份以来修改过的文件。运行扫描会将系统上的每个文件添加到列表的顶部,让刚刚扫描的文件从底部掉下来。在备份结束时,缓冲区缓存中充满了没有人关心的数据。于此同时,关键任务数据库已经被完全推入磁盘。这被称为缓存抖动(cache thrashing)。

ARC避免了这些问题。

ARC 设计

ARC还缓存最近从磁盘读取的文件。ARC有两对列表,而不是一个列表。一个是最近使用的(Most Recently Used——MRU)列表,它像缓冲区缓存一样跟踪访问的文件系统块。另一个是最常用(Most Frequently Used——MFU)列表,跟踪经常使用的文件系统块。

添加MFU列表可以减少缓存抖动过程的影响。扫描系统上的每个文件会清除MRU列表,但不会影响MFU列表。出于备份目的扫描一次块并不“频繁”。最常用的文件仍缓存在内存中。运行备份仍然会影响磁盘I/O,降低写入性能,但系统会从内存中缓存的副本中提供最受欢迎的文件。

每个列表都与一个重影(ghost)列表配对,其中包含有关已从列表中删除的块的信息。

当MRU或MFU列表填满时,列表底部的块会脱落。通过跟踪从缓存中掉落的这些块,ARC可以防止块不断循环进入缓存。ARC还可以决定一个区块现在是否被频繁使用,以保证进入MFU列表。

几乎所有情况下,ARC都是自调整的,系统管理员的手动调整只会损害系统。

假如有特定应用程序需要特殊处理,在开始摆弄ARC之前,应先了解它的行为。

ARC 内存使用

如果系统有空闲内存,并且ARC认为它可能会从中受益,ARC会要求获得内存。每次系统从磁盘读取内容时,ARC都会将文件缓存在内存中,直到系统使用完所有内存。

FreeBSD为内核和应用程序保留了1GB的RAM,其余的系统内存对ARC来说是公平的。在一个长时间运行、存储量大而内存不多的系统上,ARC会消耗大部分内存。

然而,ARC对内存请求的优先级却非常低。如果应用程序请求内存,但系统没有可用内存,内核会缩小ARC,为应用程序提供其请求的内存。将内存从ARC作为自由成员返回到系统的过程不是瞬时的,这可能需要几秒钟的时间。

所以,如果内存是空闲的,ARC会使用它。如果有程序需要内存,ARC会释放它。

Free RAM is wasted RAM。内存空闲就是浪费。

检查ARC大小最简单的方法就是使用top命令。以下是一个具有32GB内存和20TB磁盘的文件服务器的top输出片段:

Mem行出现在几乎所有类Unix系统的top输出中,并提供了系统用于各种任务的内存量的详细信息。虽然ARC是存储器的一个子集,但它是单独出现的,因此可以提供更多细节。

第一条,Total,显示ARC一共使用了多少内存。此例中使用了23GB。

ZFS访问的文件通常出现在MFU空间中,此例中显示了ARC中15GB的MFU数据。

MRU条目显示7398MB用于存储最近访问的文件。

Anon条目显示从一个队列移动到另一个队列的数据,或等待刷新到磁盘的异步写入。

Header条目显示用于有关ARC本身的元数据。此例中23GB的ARC需要412MB的元数据。

Other包括仅运行时元数据,用于帮助ARC在其缓存中查找内容。严格来说,它不是缓存本身的一部分,而是支持基础设施。

虽然ARC对内存很贪婪,但请注意,这个长时间运行的系统仍然有几GB的可用内存。

磁盘已满,但实际用户所需的数据量相对较小,任何管理过文件服务器的人都会认识到这种模式——每个企业的会计部门都有一个主电子表格,加上来自不同日期的15个略有不同的电子表格副本,所有这些都是至关重要的,必须为子孙后代永远保留。

如果ARC正使用系统的大部分内存,那是因为一个进程访问了一个文件。ARC不会滥用RAM。

Zfs-stats

FreeBSD通过vfs.zfs和kstat.zfs的各种sysctls公开ZFS性能、设置和指标。这些值本身通常意义不大,但相互比较时具有启发性。强烈建议使用zfs-stats包检查ARC,而不是直接解析这些值。

使用zfs-stats -A获取ARC的基本信息,比如当前大小和ARC中每个队列的大小。参考以下示例:

Memort Throttle Count(内存节流计数)告诉我们ARC收缩了多少次,以便将内存退回给内核供其他进程使用。如果内存节流计数很高,可以考虑降低ARC大小的限制,以确保有足够的可用内存用于其他进程。

不过,ARC内存限制并不意味着系统必须有更多的内存,而只是意味着它会使用额外的内存。

这个特殊的ARC是其最大大小的36.22%,即10.89GB。它被配置为最大大小为30.07GB、最小值为3.76GB。

ARC为MRU和MFU缓存平均分配了内存。

以下是zfs stats -E提供的ARC效率报告中的片段:

ZFS将大量内存专用于文件系统缓存。这份报告的顶部展示了一些有用的信息。

Cache Hit Ratio(缓存命中率)表示从ARC而不是通过磁盘处理的磁盘读取请求的百分比。此例中97.76%的读取请求显示内存是不足的。百分比后的数字是原始请求数,此例中该主机处理了来自ARC的76.65million(百万)个磁盘请求。

下面的CACHE HITS BY CACHE LIST显示缓存文件来自哪个缓存。

MRU缓存类似传统的缓冲区,为ARC提供的所有文件的3.35%提供服务;所有文件的96.65%来自MFU缓存。这些队列之间肯定有一些重叠——在没有MFU缓存的情况下,一些频繁访问的文件会出现MRU缓存中。但这很好地说明了为什么ARC使用MFU缓存。

Ghost保存了最近缓存的数据列表,但由于内存压力或其他限制而被丢弃。增加更多内存和增加ARC大小会提高命中率吗?Ghost列表上的命中率分别是0.04%和0.08%,因此项目不会被驱逐。这些微小的百分比可能是数万个请求,但与它所服务的数百万个请求相比,这几乎微不足道。额外的内存可能会改善其他进程,但不会改善ARC。

接下来,我们看看从ARC中提取的数据类型:

Data是文件的内容,Metadata是关于文件的一切。但Prefetch为0%,表示它并为发挥作用。

下面我们可以看到哪些类型的数据没有被缓存:

ARC应该处理多少系统请求,这取决于工作量。反复提供相同数据的web服务器可能会有很高的缓存命中率。在客户端访问许多不同文件的服务器上,缓存命中率可能会比较低。

如果池中有数百万个文件中的TB级数据,但客户端永远不会两次访问同一个数据文件,那么在内存中缓存文件就不会提高性能。

修改 ARC

ZFS ARC在绝大多数情况下都能自我管理。在大多数情况下,ARC无法自动调整自身,性能问题最好通过添加硬件来解决。

不过,有时调整ZFS的内存或性能可以为管理员争取时间,直到安装上新硬件。

在极少数情况下,调整ARC是特定应用的正确响应。

通过设置ARC可以使用多少内存的上下限来调整ARC,以及控制ARC缓存数据的内容、时间和原因。

限制 ARC 大小

默认的FreeBSD安装会为内核和操作程序留出1GB的内存。可以设置为ARC保留最小内存量和/或设置ARC可占用的内存量的硬限制。

ARC设置均以字节为单位,对于GB字节需要乘以1024的三次方。

ARC释放内存不会立即发生。在具有非常大的ARC主机上,从缓存中转储GB字节的对象可能需要1秒的时间。

在/boot/loader.conf中设置vfs.zfs.arc_max的值来限制ARC可使用的内存量(20G):

这里设置的最大值不是个硬限制,而是一个高水位线(high-water mark)。当ARC达到这个值时,ZFS开始匆忙减小缓存大小。最不重要的项目被添加到ghost列表中并被丢弃。如果此时正在监视ARC大小,会看到内存使用率在这个值附近波动。

ARC的默认最小值时最大值的八分之一。严格来说(strictly speaking),最小ARC尺寸是ARC中可用于元数据的最大内存量的一半,即最大ARC大小的四分之一。

在/boot/loader.conf中设置vfs.zfs.arc_min的值来设置ARC的最小值(4GB):

FreeBSD10.2及更高版本可以使用sysctl vfs.zfs.arc_free_target指定ARC为其他进程保留多少空闲内存。该值使用页面(page)为单位,一个page是4096字节的内存。2GB内存的值表示为524288。当可用内存量降至此值以下时,内核收割机(memory reaper)将运行。

收割机执行两个功能:调整ARC大小以确保有足够的可用内存;对KMEM Arena进行碎片整理。

与以前的可调参数不同,vfs.zfs.arc_free_target可以在运行中的系统上进行调整,并立即生效。

元数据和 ARC

文件系统元数据包括有关文件的所有内容,除了文件本身,还包括目录、权限、所有权、大小、属性等。ARC缓存所有这些信息,就像缓存文件内容一样。毕竟,访问文件的内容需要访问文件的元数据。

默认情况下,ARC最多使用其最大大小的四分之一来缓存此元数据。对于有很多小文件的文件系统,可以提高这个限制。

在/boot/loader.conf中设置vfs.zfs.arc_meta_limit的值(8GB):

如果zfs-stats -E显示从ARC中提取的数据比元数据多得多,可能需要考虑增加元数据限制。

记住,默认情况下,ARC的最小大小是可用于元数据的量的一半。此外,缓存的元数据不能使用比整个ARC更多的空间。

数据集和 ARC

一些数据具有规则的访问模式,这使得ARC变得无关紧要,告诉ARC不要缓存这些文件,可以释放内存来缓存可能受益的文件。

ZFS的primarycache属性定义了数据集信息的哪些部分应该进入ARC。该属性的默认值为all,意味着同时缓存文件数据和元数据。

将primarycache设置为metadata告诉ARC仅缓存每个文件的元数据,不缓存内容。这对包含大量文件的目录很有用。

如果没有元数据缓存,在大型目录上运行ls命令可能需要几分钟时间,因为ZFS会读取磁盘并组装信息。仅缓存元数据会禁用ZFS预取(ZFS prefetching),这可能会比仅缓存元数据更有损性能。

将primarycache设置为none告诉ARC不缓存任何来自数据集的内容。没有缓存就无法预取,但如果数据集没有缓存,性能显然不是问题。

对primarycache属性的更改,需要卸载并重新装载数据集:

卸载数据集会从ARC和L2ARC中删除有关该数据集的所有缓存信息。

Level 2 ARC(二级ARC)

ARC不断修剪自己,以保持在允许的大小范围内。长时间未被引用的文件将从MRU列表中删除。通常,从ARC中删除的项目会蒸发——当它们在幽灵列表中被提及时,如果它们再次出现,MFU队列可以识别它们,系统依赖于文件的磁盘副本。

2级ARC或L2ARC是一个辅助读缓存。L2ARC捕获从ARC脱落的项目。通过使用小型、快速、高耐久性的磁盘缓存ARC数据,您可以同时减少系统主存储器的读取负载并提高读取性能。zpool(8)命令将L2ARC称为cache device。

ZFS不信任缓存设备上的信息。

多用户、虚拟机或应用程序访问单个数据集时,使用L2ARC是有意义的。

如果工作集大于能负担得起的RAM量,可以选择基于SSD或NVMe设备的L2ARC。

对于大多数应用程序,如典型的NAS,L2ARC不会提高性能。L2ARC甚至会通过内存消耗损害性能。

L2ARC 内存使用

虽然L2ARC包含一大堆缓存数据和元数据,但这些数据的索引驻留在ARC中。一般来说,每GB的L2ARC需要大约25MB的ARC。这取决于磁盘的扇区大小、记录大小属性和其他数据集特征,这使得实际大小难以计算。一个合理的预期是,1TB的L2ARC将消耗大约25GB的ARC。

大多数L2ARC不到1TB。具有足够高耐久性以使其适合读取缓存的SSD仍然很贵。那些计划使用超过40个驱动器的大型存储阵列的人应该记住这一点。

L2ARC 缓存

L2ARC只能缓存从ARC脱落的数据。从未在ARC中出现的数据也不会出现在L2ARC中。

假设你通过primarycache属性设置为none,完全禁用了特定池上所有数据集的ARC缓存,向此池中添加L2ARC不会提高ZFS性能,因为没有缓存数据会降到L2ARC中。

如果一个数据集有意义,其中ARC包含元数据,而L2ARC缓存实际的文件数据:

问题是ARC只缓存元数据,因此它只能向L2ARC推送元数据。L2ARC只能包含ARC中的内容或其子集。

默认情况下,ARC会缓存系统访问的所有内容,因此L2ARC也会这样做。

可以使用secondarycache属性控制每个数据集如何使用L2ARC。类似primarycache属性,secondarycache可以设置为all、metadata、none。默认为all,意味着对于主ARC来说几乎足够重要的数据会被推送到L2ARC上。

流媒体文件

读取文件所需的大部分时间都花在将磁头移动到盘片上方的位置上。一旦定位,磁头会很快读取数据。这被称为流媒体。

虽然在内存外提供大文件比从磁盘读取快,但在大多数系统上,主池的多个磁盘比L2ARC的一两个磁盘快。在这种情况下,使用L2ARC缓存流文件没有意义,因此默认情况下是禁用的。

如果你有一个比主池快的L2ARC,你可能想启用大文件缓存。在/boot/loader.conf中设置vsf.zfs.l2arc_noprefetch的值为0以启用缓存(默认值为1,禁用缓存流文件):

此可调项仅在导入池时生效。FreeBSD在查看/etc/sysctl.conf之前导入池,因此必须在引导加载程序中设置。

L2ARC 写入速度

SSD没有机械硬盘坚固。ZFS在L2ARC上实现了几个写节流,以延长磁盘寿命。

与主ARC一样,L2ARC在其配置中使用字节为单位。大多数L2ARC设置以兆字节为单位,所以要将数字乘以1024的平方。

在正常操作期间,ZFS每秒只向每个L2ARC设备写入8MB。这避免了SSD设备的磨损,也有助于避免缓存抖动。(缓存抖动是指将大量数据写入缓存,但在使用之前,这些数据最终会被更新的数据覆盖。)如果你需要系统的L2ARC来处理更多数据,你可以使用sysctl vfs.zfs.l2arc_write_max来增强这一点。不要把它调得太高,以免读取速度变慢。

当系统首次启动时,L2ARC为空。空的L2ARC没有多大用处。ZFS在系统启动后执行Turbo Warmup Phase(涡轮预热阶段)。在该阶段,它向L2ARC写入超出vfs.zfs.l2arc_write_max设置的限制的额外数据。

Turbo Warmup Phase会一直持续到ARC从L2ARC中取出第一个项目。所需的时间长度完全取决于系统。默认情况下,ZFS可以在涡轮预热阶段向每个L2ARC设备额外写入8MB。

sysctl vfs.zfs.l2arc_write_boost控制分配的额外带宽。

你可以在任何时候更改sysctls。以下示例我们设置16MB:

当然,软件设置不能突破硬件限制。

SSD越来越强壮。现代数据中心级SSD的耐久性更高。由于主机可能不会一直全速写入L2ARC,因此可以将这些参数调得更高。

ZFS 意图日志——ZIL

缓存不仅仅用于读取数据。ZFS也通过ZFS意图日志(ZFS Intent Log——ZIL)使用缓存进行写入。ZFS将写入数据转储到ZIL,然后处理这些写入数据以将其正确添加到ZFS。

每个池都有自己的ZIL。在正常使用中,ZFS在每个提供者上为ZIL使用一块空间。也可以添加一个外部设备用作ZIL。

严格来说,ZIL并不是一个写缓存。

要了解池何时需要单独的日志设备,何时不需要,必须了解ZFS如何写入数据。

同步和异步事务

ZFS最终到达永久存储的数据的完整性。磁盘上的数据应始终保持一致。

系统可能会在程序和磁盘之间丢失数据,但没有任何文件系统可以保护驻留在RAM中的运行中数据。

为确保磁盘上的数据完整性,ZFS将写入请求分组到事务组(transaction groups——txgs)中。事务组是一堆数据和相关的文件系统元数据。

当要求系统写入磁盘时,ZFS会在事务组中收集这些写入。一个事务组可以包含来自许多不相关进程的写入。一旦组有足够的数据或定时器到期,该事务组就会被写入磁盘。该计时器可能长达30秒,也可能短至5秒,具体取决于运行的FreeBSD版本。

事务组是文件系统的待办事项列表。数据在完全写入磁盘之前容易发生系统故障。如果系统在事务组写入磁盘之前崩溃或死亡,则该数据将丢失。减少事务组超时可能会减少数据丢失量,但也会严重影响性能。

下面我们看一下数据写入磁盘的过程。

程序将一块数据交给内存,要求写入磁盘。在内核确认收到数据之前,程序会一直等待,直到内核表明已经获取到了数据。等待此响应的程序被称为I/O阻塞(blocking on I/O)。

重要的问题是:内核何时确认收到数据?何时将数据添加到事务组?或者何时将其写入磁盘?

在正常操作中,当数据作为挂起事务组的一部分在内存中时,内核会确认数据。数据不在磁盘上——内核只是声称对数据负责。异步操作的变体在现代文件系统中很常见,例如Linux的extfs和BSD UFS的各种版本。

文件系统也可以在同步模式下工作,在这种模式下,内核仅在位实际写入物理存储介质时才确认数据。同步挂载非常安全,但速度也非常慢。写入数据的程序将阻塞等待物理硬件响应内核的写入请求。某些程序(如数据库服务器)通过使用fsync系统调用请求特定文件的同步确认。系统管理员可能会同步挂载数据集,这样系统只会在写入完成时确认数据,或者使用fsync程序告诉系统立即将所有内容刷新到磁盘。

ZFS 意图日志

当ZFS以同步模式写入文件时,它不会立即将事务组推送到磁盘,相反,这些数据会被丢到ZIL上。它们没有整齐地排列成ZFS数据集块,相反,它们只是磁盘上的一堆块。当事务组被写入磁盘时,ZIL上的块被写入到它们的正确位置。

池导入过程检查ZIL中尚未到达其最终位置的数据。如果系统在其导入的池中发现正在运行的块,则完成这些事务。

池通常使用每个存储提供者上的一小块空间作为ZIL。这意味着每次同步写入都会被写入物理存储两次。但是该池只使用ZIL来同步写入数据。正常的异步写入存储在RAM中,并作为常规事务组的一部分提交。

有时,你可以通过将ZIL放在一个专用的快速设备上来提高性能,该设备称为单独意图日志(Separate Intent LOG——SLOG)。

单独意图日志

您可以使用单独的意图日志(或称SLOG)将ZIL从池中分离出来。通过将ZIL移动到单独的专用硬件,您可以避免将相同的数据两次写入存储提供商。如果SLOG硬件比池快,内核可以更快地确认数据,从而提高请求应用程序的性能。

尽管常用,但SLOG与ZIL并不等同。SLOG是硬件。ZIL位于SLOG或存储提供者上。你可以通过窗口启动SLOG,但你只能对着ZIL咒骂。

最快、最可靠、最昂贵的SLOG是NVRAM芯片。高耐久性SSD是SLOG最常见的选择。你甚至可以使用非常快的SAS驱动器,但它们是最不可靠的。其中每一个都需要一个专用电源,如电池或超级电容器,以便在系统电源故障时完成写入。

SLOG不需要很大。sysctl vfs.zfs.dirty_data_max提供了最大可能的运行中数据量。FreeBSD 10的ZFS默认使用大小等于系统RAM十分之一的ZIL。您可以使用单个硬件来支持多个池的SLOG提供程序,但这也会在这些池之间分割该设备的I/O。使用SLOG的一个原因是应对I/O短缺。

并非所有SSD或NVRAM都是平等的。许多标榜为“高耐久性”的设备不够强大,无法处理即使是中等规模的池的所有写入。对于数据完整性至关重要的应用程序,作者强烈建议您咨询专门从事ZFS的硬件供应商,如iX Systems(www.ixsystems.com)。一个正确选择的SLOG可以极大地加速你的程序,而一个糟糕的选择会破坏你的池。

每个数据集ZIL调优

可以控制数据集如何(或是否)使用具有sync属性的ZIL。就像挂载传统的文件系统同步或异步一样,sync属性决定了数据集是否接受fsync请求。

默认设置是standard,告诉数据集将ZIL用于同步请求。如果程序使用fsync请求内核在数据安全地存储在磁盘上之前不确认数据,则数据将被写入ZIL。其他数据作为事务组的一部分异步写入。这是默认操作。

将sync设置为always,所有写入都将发送给ZIL。没有异步写入。这是管理数据最安全的方法,但会带来严重的性能损失。可以选择在专用于关键数据的数据集上设置该属性。

将sync设置为disabled将完全禁用此数据集上的ZIL。写入都是异步的。该系统对使用sync的每个程序都撒谎。永远不要禁用数据库或NFS使用的任何数据集上的ZIL。实际上,在数据集上禁用ZIL的唯一原因是验证ZIL不会导致特定应用程序出现性能问题。如果禁用ZIL可以修复应用程序,请务必提交错误报告或安装快速SLOG设备。

在几乎所有情况下,保持sync为standard。可能有一两个数据集需要将sync设置为always。

通过堆栈进行同步写入

ZFS最终成为许多不同应用程序的数据存储后端,如网络文件系统(NFS)和iSCSI。

可以将zvol用于虚拟化系统的驱动器。所有这些不同的层都独立运行。

没有什么比fsync系统调用更明显、更危险了。

如果数据完整性很重要,应验证同步写入是否在整个应用程序堆栈中工作。

zpool.cache

/boot/zfs/zpool.cache是一种缓存,它不会影响日常系统管理。

zpool.cache文件包含系统上当前活动的池及其提供程序的描述。当您启动ZFS系统时,内核会检查根池上的zpool.cache文件,以发现它应该导入哪些系统池。

尽管有很多挥之不去的过时文档,但在现代版本的ZFS或FreeBSD上几乎没有理由更改缓存文件的位置。

现在您已经了解了缓存,我们可以谈谈性能。