内核纵览 - 2.6 和 2.7 版本的存储改进
在过去十年中,存储发生了快速变化。在此之前,服务器级磁盘在所有意义上都是专有的。它们使用专有协议,通常由服务器供应商销售,并且给定的服务器通常拥有自己的磁盘,共享磁盘系统非常少见。
当 SCSI 在 1990 年代中期从 PC 升级到中端服务器时,情况有所改观。SCSI 标准允许多个发起者(服务器)共享目标(磁盘)。如果您仔细选择兼容的 SCSI 组件并进行了大量的压力测试,则可以构建共享 SCSI 磁盘集群。许多这样的集群在 1990 年代的 数据中心生产中使用,并且有些至今仍然存在。
人们还必须注意不要超过 25 米的 SCSI 总线长度限制,尤其是在构建三节点和四节点集群时。当然,超过长度的惩罚不是确定性的错误,而是不稳定的磁盘 I/O。这种限制要求磁盘分散在服务器之间。
光纤通道 (FC) 在 1990 年代中后期出现,极大地改善了这种情况。虽然兼容性曾经是并且在某种程度上仍然是一个问题,但数公里的 FC 长度大大简化了数据中心的布局。此外,大多数 FC 连接的 RAID 阵列都导出逻辑单元 (LUN),例如,这些逻辑单元可以在底层物理磁盘上进行条带化或镜像,从而简化存储管理。此外,FC RAID 阵列提供 LUN 掩码,FC 交换机提供区域划分,这两者都允许受控的磁盘共享。图 1 说明了一个示例,其中服务器 A 被允许访问磁盘 1 和 2,服务器 B 被允许访问磁盘 2 和 3。磁盘 1 和 3 是私有的,而磁盘 2 是共享的,区域由灰色矩形指示。

图 1. 光纤通道允许 LUN 掩码和区域划分。服务器 A 可以访问磁盘 1 和 2,服务器 B 可以访问磁盘 2 和 3。
这种受控共享使块结构化集中存储更具吸引力。这反过来又允许分布式文件系统提供与本地文件系统相同的语义,同时仍然提供合理的性能。
现代廉价的磁盘和服务器大大降低了大型服务器场的成本。然而,正确备份每台服务器可能非常耗时,并且跟上磁盘故障可能是一个挑战。备份的需求促使数据集中化,这样就不需要备份每台服务器上物理位置的磁盘。然后可以在中央位置执行备份。
集中式数据可以存储在 NFS 服务器上。这是一种合理的方法,在许多情况下都很有用,尤其是在 NFS v4 成为主流的情况下。但是,服务器有时需要对其数据进行直接的块级访问
给定的服务器可能需要特定文件系统的功能,例如 ACL、扩展属性或日志记录。
特定的应用程序可能需要比 NFS 等协议能够提供的更好的性能或鲁棒性。
某些应用程序可能需要本地文件系统语义。
在某些情况下,从本地磁盘迁移到 RAID 阵列可能更容易。
然而,2.4 Linux 内核在处理大型 RAID 阵列时存在一些挑战,包括存储重新配置、多路径 I/O、对大型 LUN 的支持以及对大量 LUN 的支持。2.6 内核有望在许多这些领域提供帮助,尽管 2.7 开发工作还有一些改进领域。
由于大多数 RAID 阵列允许动态创建、删除和调整 LUN 大小,因此 Linux 内核能够对这些操作做出反应非常重要,最好无需重启。Linux 2.6 内核通过 /sys 文件系统实现了这一点,该文件系统取代了早期的 /proc 接口。例如,以下命令使内核忘记 busid 3、通道 0、目标 7 和 LUN 1 上的 LUN
echo "1" > \ /sys/class/scsi_host/host3/device/3:0:7:1/delete
busid 3 与 host3 中的 3 是冗余的。但是,这种格式也用于需要 busid 的上下文中,例如在 /sys/bus/scsi/devices 中。
要稍后仅恢复该特定 LUN,请执行
echo "0 7 1" > /sys/class/scsi_host/host3/scan
要调整同一 LUN 的大小,请使用
echo 1 > /sys/bus/scsi/devices/3:0:7:1/rescan
要扫描所有通道、目标和 LUN,请尝试
echo "- - -" > /sys/class/scsi_host/host3/scan
要仅扫描一个特定目标,请输入
echo "0 7 -" > /sys/class/scsi_host/host3/scan
虽然这种设计不是特别用户友好,但它对于自动化工具来说效果很好,自动化工具可以利用 libsys 库和 systool 实用程序。
FC 的优势之一是它允许服务器和 RAID 阵列之间存在冗余路径,这可以允许移除和更换发生故障的 FC 设备,而服务器应用程序甚至不会注意到发生了任何事情。但是,只有当服务器具有健壮的多路径 I/O 实现时,这才有可能。
当然,人们不会抱怨 Linux 2.4 内核缺少多路径 I/O 实现。现实情况恰恰相反,因为在 SCSI 中间层、设备驱动程序、md 驱动程序和 LVM 层中都有实现。
事实上,2.6 中过多的 I/O 实现可能会使将不同类型的 RAID 阵列连接到同一服务器变得困难甚至不可能。Linux 内核需要一个单一的多路径 I/O 实现,以适应所有支持多路径的设备。理想情况下,这样的实现会持续监控所有可能的路径,并自动确定何时修复了发生故障的 FC 设备。希望目前正在进行的设备映射器 (dm) 多路径目标的工作将解决这些问题。
一些 RAID 阵列允许从多个磁盘的连接中创建非常大的 LUN。Linux 2.6 内核包含一个 CONFIG_LBD 参数,该参数可容纳 TB 级的 LUN。
为了在 Linux 上运行大型数据库和相关应用程序,需要大量的 LUN。理论上,可以使用较少数量的大型 LUN,但是这种方法存在许多问题
许多存储设备对 LUN 大小有限制。
磁盘故障恢复在较大的 LUN 上花费的时间更长,这使得在恢复完成之前第二个磁盘发生故障的可能性更高。这种二次故障将意味着不可恢复的数据丢失。
如果大多数 LUN 都是固定大小且可互换的,则存储管理要容易得多。过大的 LUN 会浪费存储空间。
大型 LUN 可能需要更长的备份窗口,而增加的停机时间可能超过关键任务应用程序的用户愿意承受的范围。
内核的 dev_t 大小从 16 位增加到 32 位,这使得 i386 构建的 2.6 内核能够支持 4,096 个 LUN,尽管在撰写本文时,还有一个额外的补丁仍在等待从 James Bottomley 的树移动到主树中。一旦集成此补丁,64 位 CPU 将能够支持多达 32,768 个 LUN,使用 4G/4G 拆分和/或 Maneesh Soni 的 sysfs/dcache 补丁构建的 i386 内核也应该能够支持。当然,64 位 x86 处理器,例如 AMD64 和 Intel 的 64 位 ia32e,应该有助于将 32 位限制从困境中解脱出来。
通过高速存储区域网络 (SAN) 从多台服务器轻松访问大型 RAID 阵列使分布式文件系统变得更加有趣和有用。也许并非巧合,许多开源分布式文件系统正在开发中,包括 Lustre、OpenGFS 和 IBM SAN 文件系统的客户端部分。此外,还有许多专有的分布式文件系统可用,包括 SGI 的 CXFS 和 IBM 的 GPFS。所有这些分布式文件系统都提供本地文件系统语义。
相比之下,较旧的分布式文件系统(例如 NFS、AFS 和 DFS)提供受限制的语义,以便节省网络带宽。例如,如果一对 AFS 客户端同时写入同一个文件,则最后一个关闭文件的客户端获胜——另一个客户端的更改将丢失。以下事件序列说明了这种差异
客户端 A 打开一个文件。
客户端 B 打开同一个文件。
客户端 A 将所有 A 写入文件的第一个块。
客户端 B 将所有 B 写入文件的第一个块。
客户端 B 将所有 B 写入文件的第二个块。
客户端 A 将所有 A 写入文件的第二个块。
客户端 A 关闭文件。
客户端 B 关闭文件。
使用本地文件系统语义,文件的第一个块包含所有 B,第二个块包含所有 A。使用 last-close 语义,两个块都包含所有 B。
语义上的这种差异可能会让设计为在本地文件系统上运行的应用程序感到惊讶,但这大大减少了两个客户端之间所需的通信量。使用 AFS last-close 语义,两个客户端仅需要在打开和关闭时进行通信。但是,使用严格的本地语义,它们可能需要在每次写入时进行通信。
事实证明,令人惊讶的是,很大一部分现有应用程序可以容忍语义上的差异。但是,随着本地网络变得更快更便宜,偏离本地文件系统语义的理由越来越少。毕竟,提供与本地文件系统完全相同语义的分布式文件系统可以运行在本地文件系统上运行的任何应用程序。另一方面,偏离本地文件系统语义的分布式文件系统可能会也可能不会这样做。因此,除非您要跨广域网分发文件系统,否则额外的带宽似乎是为完全兼容性付出的很小的代价。
Linux 2.4 内核对分布式文件系统并不完全友好。除其他外,它缺少用于使 mmap() 文件的页面无效的 API,以及一种有效保护进程免受 oom_kill() 影响的方法。它还缺少对向导出同一分布式文件系统的两个不同服务器发出的 NFS 锁定请求的正确处理。
假设同一系统上的两个进程 mmap() 同一个文件。每个进程都实时看到另一个进程的内存写入的连贯视图。如果分布式文件系统要忠实地提供本地语义,则需要连贯地组合从不同节点 mmap() 文件的进程的内存写入。这些进程不能同时对文件页面具有写访问权限,因为那样将没有合理的方法来组合更改。
解决此问题的常用方法是让节点的 MMU 使用所谓的分布式共享内存来完成脏活。其思想是任何给定时间只有一个节点允许写入。当然,这目前意味着一次只有一个节点可以访问给定文件的给定页面,因为页面可以在不通知底层文件系统的情况下从只读升级为可写。
当某些其他节点的进程发生页面错误时,例如,相对于文件开头偏移量为 0x1234 时,它必须向当前拥有可写副本的节点发送消息。该节点必须从任何 mmap() 了该页面的用户进程中删除该页面。在 2.4 内核中,分布式文件系统必须深入到 VM 系统的内部才能完成此操作,但 2.6 内核提供了一个 API,第二个节点可以按如下方式使用该 API
invalidate_mmap_range(inode->mapping, 0x1234, 0x4);
然后可以将页面的内容发送到第一个节点,第一个节点可以将其映射到发生错误的进程的地址空间中。熟悉 CPU 体系结构的读者应该认识到此步骤与缓存一致性协议的相似之处。但是,此过程非常缓慢,因为数据必须以页面大小的块通过某种网络传输。它也可能需要在途中写入磁盘。
2.6 内核中仍然存在的挑战包括允许多个节点上的进程有效地将给定文件的给定页面映射为只读,这要求文件系统被告知对只读映射的写入尝试。此外,2.6 内核还必须允许文件系统有效地确定 VM 系统已逐出哪些页面。这允许分布式文件系统更好地确定要从内存中逐出哪些页面,因为逐出不再被任何用户进程映射的页面是一种合理的启发式方法——如果您可以有效地找出哪些页面是这些页面。
当前 NFS lockd 的实现使用每个服务器的锁定状态数据库。当导出本地文件系统时,这工作得很好,因为锁定状态保存在 RAM 中。但是,如果使用 NFS 从两个不同的节点导出相同的分布式文件系统,我们最终会得到图 2 中所示的情况。运行独立 lockd 副本的两个节点都可以将相同的锁交给两个不同的 NFS 客户端。不用说,这种事情可能会降低您的应用程序的正常运行时间。
解决此问题的一种直接方法是让 lockd 获取针对底层文件系统的锁,允许分布式文件系统正确仲裁并发的 NFS 锁定请求。但是,lockd 是单线程的,因此如果分布式文件系统在评估来自 lockd 的请求时阻塞,则 NFS 锁定将被暂停。分布式文件系统在从节点故障中恢复、由于消息丢失而重新传输等期间,可能会长时间阻塞。
处理此问题的一种方法是使用多线程 lockd。但是,这样做会增加复杂性,因为 lockd 的不同线程必须协调,以避免将相同的锁交给两个不同的客户端。此外,还存在应该提供多少个线程的问题。
尽管如此,针对这两种方法的补丁都存在,并且它们已经得到了一些应用。其他可能的方法包括使用 2.6 内核的通用工作队列而不是线程,或者要求底层文件系统立即响应,但允许它说“我不知道,但一旦我找到答案就会告诉你”。后一种方法将允许文件系统有时间整理其锁,同时避免暂停 lockd。
一些分布式文件系统使用特殊线程,其工作是释放不再使用的包含缓存文件状态的内存,类似于 bdflush 写出脏块的方式。显然,杀死这样的线程在某种程度上会适得其反,因此这样的线程应该免于内存不足杀手 oom_kill()。
2.6 内核中的技巧是通过使用以下命令设置 CAP_SYS_RAWIO 和 CAP_SYS_ADMIN 功能
cap_raise(current->cap_effective, CAP_SYS_ADMIN|CAP_SYS_RAWIO);
这里,current 指示当前正在运行的线程。这会导致 oom_kill() 避免此线程,如果它确实选择它,则使用 SIGTERM 而不是 SIGKILL。线程可以捕获或忽略 SIGTERM,在这种情况下,oom_kill() 会标记该线程,以便避免再次杀死它。
存储系统似乎将继续变化。LAN 设备比 SAN 设备便宜得多这一事实预示着 iSCSI 的良好前景,iSCSI 在 TCP 上运行 SCSI 协议。但是,iSCSI 的广泛使用引发了一些安全问题,因为未能禁用 IP 转发可能会让某人入侵您的存储系统。有些人认为,串行 ATA (SATA) 注定要取代 SCSI,就像 SCSI 本身取代专有磁盘接口协议一样。其他人则认为 RAID 阵列将被对象存储或对象存储目标所取代,事实上,此类设备的新兴标准正在出现。无论哪种方式,与存储系统的接口都将继续具有挑战性和令人兴奋。
我感谢 Linux 社区,但特别感谢 Daniel Phillips 和 Hugh Dickins 进行了非常棒的讨论,并感谢 Mike Anderson 和 Badari Pulavarty 对近期 2.6 内核功能的解释以及他们对本文的审阅。我还感谢 Bruce Allan 和 Trond Myklebust 就解决 NFS lockd 问题提出的想法。
Paul E. McKenney 是 IBM 的杰出工程师,在 SMP 和 NUMA 算法方面工作的时间比他愿意承认的要长。在此之前,他从事分组无线电和互联网协议方面的工作,但在互联网普及之前很久。他的爱好包括跑步和通常的家庭主妇和孩子的生活习惯。