术语
文件系统
计算机的文件系统是一种存储和组织计算机数据的方法,它使得对其访问和查找变得容易,文件系统使用文件和树形目录的抽象逻辑概念代替了硬盘和光盘等物理设备使用数据块的概念,用户使用文件系统来保存数据不必关心数据实际保存在硬盘(或者光盘)的地址为多少的数据块上,只需要记住这个文件的所属目录和文件名。在写入新数据之前,用户不必关心硬盘上的那个块地址没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中。
I/O
I/O(英语:Input/Output),即输入/输出,通常指数据在存储器(内部和外部)或其他周边设备之间的输入和输出,是信息处理系统(例如计算机)与外部世界(可能是人类或另一信息处理系统)之间的通信。输入是系统接收的信号或数据,输出则是从其发送的信号或数据。该术语也可以用作行动的一部分;到“运行I/O”是运行输入或输出的操作。
文件缓存
内存上的一块的一块区域,用来缓存文件系统的内容。索引节点缓存,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。
随机I/O与顺序I/O
顺序IO是指读写操作的访问地址连续。在顺序IO访问中,HDD所需的磁道搜索时间显着减少,因为读/写磁头可以以最小的移动访问下一个块。数据备份和日志记录等业务是顺序IO业务。随机IO是指读写操作时间连续,但访问地址不连续,随机分布在磁盘的地址空间中。产生随机IO的业务有OLTP服务,SQL,即时消息服务等。
预读
Linux文件预读算法磁盘I/O性能的发展远远滞后于CPU和内存,因而成为现代计算机系统的一个主要瓶颈。预读可以有效的减少磁盘的寻道次数和应用程序的I/O等待时间,是改进磁盘读I/O性能的重要优化手段之一。本文作者是中国科学技术大学自动化系的博士生,他在1998年开始学习Linux,为了优化服务器的性能,他开始尝试改进Linux kernel,并最终重写了内核的文件预读部分,这些改进被收录到Linux Kernel 2.6.23及其后续版本中。预取算法的涵义和应用非常广泛。它存在于CPU、硬盘、内核、应用程序以及网络的各个层次。预取有两种方案:启发性的(heuristic prefetching)和知情的(informed prefetching)。前者自动自发的进行预读决策,对上层应用是透明的,但是对算法的要求较高,存在命中率的问题;后者则简单的提供API接口,而由上层程序给予明确的预读指示。在磁盘这个层次,Linux为我们提供了三个API接口:posix_fadvise(2), readahead(2), madvise(2)。不过真正使用上述预读API的应用程序并不多见:因为一般情况下,内核中的启发式算法工作的很好。预读(readahead)算法预测即将访问的页面,并提前把它们批量的读入缓存。它的主要功能和任务可以用三个关键词来概括:1、批量,也就是把小I/O聚集为大I/O,以改善磁盘的利用率,提升系统的吞吐量。2、提前,也就是对应用程序隐藏磁盘的I/O延迟,以加快程序运行。3、预测,这是预读算法的核心任务。前两个功能的达成都有赖于准确的预测能力。当前包括Linux、FreeBSD和Solaris等主流操作系统都遵循了一个简单有效的原则:把读模式分为随机读和顺序读两大类,并只对顺序读进行预读。这一原则相对保守,但是可以保证很高的预读命中率,同时有效率/覆盖率也很好。因为顺序读是最简单而普遍的,而随机读在内核来说也确实是难以预测的。
写回缓存
回写缓存存在有一个与生俱来的潜在问题,即应用程序在接到写完成信号之后可能会进行一些其他操作,而在数据被真正写入非易失性介质之前系统失效。此时就会导致介质上的数据与后续操作不一致性。由于这个问题,良好的回写缓存实现时要有在系统失效期间(包括电源失效)保护缓存内容,当系统重启时再写入介质的机制。
吞吐量
磁盘的吞吐量,也就是每秒磁盘 I/O 的流量,即磁盘写入加上读出的数据的大小。
IOPS
磁盘的 IOPS,也就是在一秒内,磁盘进行多少次 I/O 读写。
写时复制
写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。没有COW之前 第一代Unix系统实现了一种傻瓜式的进程创建:当执行fork系统调用时,内核复制父进程的整个用户空间并把复制得到的那一份分配给子进程。这种行为是非常耗时的,因为它需要完成以下几项任务:
- 为子进程的页表分配页面
- 为子进程的页分配页面
- 初始化子进程的页表
- 把父进程的页复制到子进程对应的页中
有COW之后
在Linux中,系统调用fork()创建子进程时,并不会立即为子进程创建新的物理内存空间(逻辑空间当然还是保持独立,只是说两份逻辑空间一开始映射到同一份物理空间),而是公用父进程的物理空间。只有在需要写入的时候,数据才会被复制,从而使父进程、子进程拥有各自的副本。也就是说,资源的复制只有在需要写入的时候才进行,在此之前以只读方式共享。
零拷贝
技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。实现零复制的软件通常依靠基于直接存储器访问(DMA)的复制,以及通过内存管理单元(MMU)的内存映射。这些功能需要特定硬件的支持,并通常涉及到特定存储器的对齐。一种较新的方式为使用异构系统架构(HSA),便于CPU和GPU以及其他处理器传递指针。这需要CPU和GPU使用统一地址空间。Linux内核通过各个系统调用支持零复制,例如sys/socket.h的sendfile、sendfile64以及splice。它们部分在POSIX中指定,因此也存在于BSD内核或IBM AIX中,部分则是Linux内核API中独有。
使用率
是指磁盘处理 I/O 的时间百分比。过高的使用率(比如超过 80%),通常意味着磁盘 I/O 存在性能瓶颈。
饱和度
饱和度是指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求。
响应时间
响应时间是指 I/O 请求从发出到收到响应的间隔时间。
优化方案
当然, 想要优化 I/O 性能,肯定离不开 Linux 系统的 I/O 栈图的思路辅助。
应用程序优化
首先,我们来看一下,从应用程序的角度有哪些优化 I/O 的思路。应用程序处于整个 I/O 栈的最上端,它可以通过系统调用,来调整 I/O 模式(如顺序还是随机、同步还是异步), 同时,它也是 I/O 数据的最终来源。在我看来,可以有这么几种方式来优化应用程序的 I/O 性能。
第一,可以用追加写代替随机写,减少寻址开销,加快 I/O 写的速度。
第二,可以借助缓存 I/O ,充分利用系统缓存,降低实际 I/O 的次数。
第三,可以在应用程序内部构建自己的缓存,或者用 Redis 这类外部缓存系统。这样,一方面,能在应用程序内部,控制缓存的数据和生命周期;另一方面,也能降低其他应用程序使用缓存对自身的影响。C 标准库提供的 fopen、fread 等库函数,都会利用标准库的缓存,减少磁盘的操作。而你直接使用 open、read 等系统调用时,就只能利用操作系统提供的页缓存和缓冲区等,而没有库函数的缓存可用。
第四,在需要频繁读写同一块磁盘空间时,可以用 mmap 代替 read/write,减少内存的拷贝次数。
第五,在需要同步写的场景中,尽量将写请求合并,而不是让每个请求都同步写入磁盘,即可以用 fsync() 取代 O_SYNC。
第六,在多个应用程序共享相同磁盘时,为了保证 I/O 不被某个应用完全占用,推荐你使用 cgroups 的 I/O 子系统,来限制进程 / 进程组的 IOPS 以及吞吐量。最后,在使用 CFQ 调度器时,可以用 ionice 来调整进程的 I/O 调度优先级,特别是提高核心应用的 I/O 优先级。ionice 支持三个优先级类:Idle、Best-effort 和 Realtime。其中, Best-effort 和 Realtime 还分别支持 0-7 的级别,数值越小,则表示优先级别越高。
文件系统优化
应用程序访问普通文件时,实际是由文件系统间接负责,文件在磁盘中的读写。所以,跟文件系统中相关的也有很多优化 I/O 性能的方式。
第一,你可以根据实际负载场景的不同,选择最适合的文件系统。比如 Ubuntu 默认使用 ext4 文件系统,而 CentOS 7 默认使用 xfs 文件系统。相比于 ext4 ,xfs 支持更大的磁盘分区和更大的文件数量,如 xfs 支持大于 16TB 的磁盘。但是 xfs 文件系统的缺点在于无法收缩,而 ext4 则可以。其他对比:
- 初始化模式下,ext4性能并没有比xfs来得高
- 随机读写模式下,ext4性能比xfs将近高一倍
- 其他测试模式中,ext4和xfs性能相当
在一些对随机IO性能要求较高的环境下,可以尝试使用ext4,比如数据库,大型图片后台存储等
第二,在选好文件系统后,还可以进一步优化文件系统的配置选项,包括文件系统的特性(如 ext_attr、dir_index)、日志模式(如 journal、ordered、writeback)、挂载选项(如 noatime)等等。比如, 使用 tune2fs 这个工具,可以调整文件系统的特性(tune2fs 也常用来查看文件系统超级块的内容)。而通过 /etc/fstab ,或者 mount 命令行参数,我们可以调整文件系统的日志模式和挂载选项等。
第三,可以优化文件系统的缓存。
- 比如,你可以优化 pdflush 脏页的刷新频率(比如设置 dirty_expire_centisecs 和 dirty_writeback_centisecs)以及脏页的限额(比如调整 dirty_background_ratio 和 dirty_ratio 等)。
- 再如,你还可以优化内核回收目录项缓存和索引节点缓存的倾向,即调整 vfs_cache_pressure(/proc/sys/vm/vfs_cache_pressure,默认值 100),数值越大,就表示越容易回收。最后,在不需要持久化时,你还可以用内存文件系统 tmpfs,以获得更好的 I/O 性能 。tmpfs 把数据直接保存在内存中,而不是磁盘中。比如 /dev/shm/ ,就是大多数 Linux 默认配置的一个内存文件系统,它的大小默认为总内存的一半。
磁盘优化
数据的持久化存储,最终还是要落到具体的物理磁盘中,同时,磁盘也是整个 I/O 栈的最底层。从磁盘角度出发,自然也有很多有效的性能优化方法。
第一,最简单有效的优化方法,就是换用性能更好的磁盘,比如用 SSD 替代 HDD。
第二,我们可以使用 RAID ,把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列。这样做既可以提高数据的可靠性,又可以提升数据的访问性能。
第三,针对磁盘和应用程序 I/O 模式的特征,我们可以选择最适合的 I/O 调度算法。比方说,SSD 和虚拟机中的磁盘,通常用的是 noop 调度算法。而数据库应用,我更推荐使用 deadline 算法。
第四,我们可以对应用程序的数据,进行磁盘级别的隔离。比如,我们可以为日志、数据库等 I/O 压力比较重的应用,配置单独的磁盘。
第五,在顺序读比较多的场景中,我们可以增大磁盘的预读数据,比如,你可以通过下面两种方法,调整 /dev/sdb 的预读大小。
- 调整内核选项 /sys/block/sdb/queue/read_ahead_kb,默认大小是 128 KB,单位为 KB。
- 使用 blockdev 工具设置,比如 blockdev --setra 8192 /dev/sdb,注意这里的单位是 512B(0.5KB),所以它的数值总是 read_ahead_kb 的两倍。
第六,我们可以优化内核块设备 I/O 的选项。比如,可以调整磁盘队列的长度 /sys/block/sdb/queue/nr_requests,适当增大队列长度,可以提升磁盘的吞吐量(当然也会导致 I/O 延迟增大)。
最后,要注意,磁盘本身出现硬件错误,也会导致 I/O 性能急剧下降,所以发现磁盘性能急剧下降时,你还需要确认,磁盘本身是不是出现了硬件错误。比如,你可以查看 dmesg 中是否有硬件 I/O 故障的日志。还可以使用 badblocks、smartctl 等工具,检测磁盘的硬件问题,或用 e2fsck 等来检测文件系统的错误。如果发现问题,你可以使用 fsck 等工具来修复。