「Linux」- 磁盘 I/O(学习笔记)

  CREATED BY JENKINSBOT

按 存储介质 分类:机械磁盘 和 固态磁盘

磁盘是可以持久化存储的设备,根据存储介质的不同,常见磁盘可以分为两类:机械磁盘和固态磁盘:

机械磁盘,硬盘驱动器(Hard Disk Driver),HDD

在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。

如果 I/O 请求刚好连续,那就不需要磁道寻址,自然可以获得最佳性能。这其实就是我们熟悉的,连续 I/O 的工作原理

随机 I/O,它需要不停地移动磁头,来定位数据位置,所以读写速度就会比较慢。

固态磁盘(Solid State Disk),SSD

固态磁盘不需要磁道寻址,所以,不管是连续 I/O,还是随机 I/O 的性能,都比机械磁盘要好得多。

按 接口类型 分类:IDE、SCSI、SAS、SATA、FC

按照接口来分类,比如可以把硬盘分为
1)IDE(Integrated Drive Electronics),在系统中磁盘前缀 hd
2)SCSI(Small Computer System Interface),在系统中磁盘前缀 sd
3)SAS(Serial Attached SCSI)
4)SATA(Serial ATA)
5)FC(Fibre Channel)

按 使用方式 分类:RAID

RAID(Redundant Array of Independent Disks)

把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列,从而可以提高数据访问的性能,并且增强数据存储的可靠性。

RAID 一般可以划分为多个级别,如 RAID0、RAID1、RAID5、RAID10 等:
1)RAID0 有最优的读写性能,但不提供数据冗余的功能
2)其他级别的 RAID,在提供数据冗余的基础上,对读写性能也有一定程度的优化

按 存储架构 分类:NFS、SMB、iSCSI

这些磁盘组合成一个网络存储集群,再通过 NFS、SMB、iSCSI 等网络存储协议,暴露给服务器使用。

最小读写单位

机械磁盘的最小读写单位,扇区,一般大小为 512 字节。

固态磁盘的最小读写单位,页,通常大小是 4KB、8KB 等。

如果每次都读写 512 字节这么小的单位的话,效率很低。所以文件系统会把连续的扇区或页组成逻辑块,然后以逻辑块作为最小单元来管理数据。常见的逻辑块的大小是 4KB,也就是说,连续 8 个扇区,或者单独的一个页,都可以组成一个逻辑块。

连续 I/O 与 随机 I/O

无论机械磁盘,还是固态磁盘,相同磁盘的随机 I/O 都要比连续 I/O 慢很多,原因也很明显:

1)对机械磁盘来说,我们刚刚提到过的,由于随机 I/O 需要更多的磁头寻道和盘片旋转,它的性能自然要比连续 I/O 慢

2)对固态磁盘来说,虽然随机性能优于 HDD,但同样存在“先擦除再写入”的限制。随机读写会导致大量的垃圾回收,所以相对应的随机 I/O 的性能弱于连续 I/O

3)此外连续 I/O 还可以通过预读的方式,来减少 I/O 请求的次数,这也是其性能优异的一个原因。很多性能优化的方案,也都会从这个角度出发,来优化 I/O 性能

主设备号 与 次设备号

在 Linux 中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。

每个块设备都会被赋予两个设备号,分别是主、次设备号:
1)主设备号,用在驱动程序中,用来区分设备类型;
2)次设备号,用来给多个同类设备编号;

通用块层

为了减小不同块设备的差异带来的影响,Linux 通过统一的通用块层来管理各种不同的块设备

通用块层,其实是处在文件系统磁盘驱动中间的一个块设备抽象层:
1)向上,为文件系统和应用程序,提供访问块设备的标准接口;向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序;
2)通用块层还会给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率;

四种 I/O 调度算法

对 I/O 请求排序的过程,也就是我们熟悉的 I/O 调度,Linux 内核支持四种 I/O 调度算法:

NONE,并不能算 I/O 调度算法。因为它完全不使用任何 I/O 调度器,对文件系统和应用程序的 I/O 其实不做任何处理,常用在虚拟机中(此时磁盘 I/O 调度完全由物理机负责)。

NOOP,最简单的一种 I/O 调度算法。它实际上是一个先入先出的队列,只做一些最基本的请求合并,常用于 SSD 磁盘。

CFQ,(Completely Fair Scheduler),也被称为完全公平调度器,是现在很多发行版的默认 I/O 调度器,它为每个进程维护了一个 I/O 调度队列,并按照时间片来均匀分布每个进程的 I/O 请求。类似于进程 CPU 调度,CFQ 还支持进程 I/O 的优先级调度,所以它适用于运行大量进程的系统,像是桌面环境、多媒体应用等。

DeadLine,分别为读、写请求创建了不同的 I/O 队列,可以提高机械磁盘的吞吐量,并确保达到最终期限(deadline)的请求被优先处理。DeadLine 调度算法,多用在 I/O 压力比较重的场景,比如数据库等。

Linux Storage Stack

文件系统层、通用块层、设备层:https://www.thomas-krenn.com/en/wiki/Linux_Storage_Stack_Diagram

文件系统层

包括虚拟文件系统和其他各种文件系统的具体实现。对上,为应用程序提供标准的文件访问接口;对下,会通过通用块层,来存储和管理磁盘数据。

通用块层

包括块设备 I/O 队列和 I/O 调度器。它会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层。

设备层

包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作。

Linux 如何优化 I/O 效率

存储系统的 I/O ,通常是整个系统中最慢的一环。所以, Linux 通过多种缓存机制来优化 I/O 效率:
1)比方说,为了优化文件访问的性能,会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制,以减少对下层块设备的直接调用。
2)为了优化块设备的访问效率,会使用缓冲区,来缓存块设备的数据

磁盘性能指标(常见指标)

使用率

指磁盘处理 I/O 的时间百分比。过高的使用率(比如超过 80%),通常意味着磁盘 I/O 存在性能瓶颈。

这里要注意的是,使用率只考虑有没有 I/O,而不考虑 I/O 的大小。换句话说,当使用率是 100% 的时候,磁盘依然有可能接受新的 I/O 请求。

饱和度

指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求。

IOPS

指每秒的 I/O 请求数量

吞吐量

指每秒的 I/O 请求大小

响应时间

指 I/O 请求从发出到收到响应的间隔时间

# 注意事项

不要孤立地去比较某一指标,而要结合读写比例、I/O 类型(随机还是连续)以及 I/O 的大小综合来分析。

在数据库、大量小文件等这类随机读写比较多的场景中,IOPS 更能反映系统的整体性能;而在多媒体等顺序读写较多的场景中,吞吐量才更能反映系统的整体性能。

磁盘 I/O 性能(基准测试)

推荐用性能测试工具 fio ,来测试磁盘的 IOPS、吞吐量以及响应时间等核心指标。但还是那句话,因地制宜,灵活选取。在基准测试时,一定要注意根据应用程序 I/O 的特点,来具体评估指标。

并且需要我们测试出,不同 I/O 大小(一般是 512B 至 1MB 中间的若干值)分别在随机读、顺序读、随机写、顺序写等各种场景下的性能情况。

用性能工具得到的这些指标,可以作为后续分析应用程序性能的依据。一旦发生性能问题,你就可以把它们作为磁盘性能的极限值,进而评估磁盘 I/O 的使用情况。

磁盘 I/O 观测(使用情况)

iostat,来自 /proc/diskstats 文件:

# iostat -d -x 1 // -d -x表示显示所有磁盘I/O的指标
Linux 4.19.0-10-amd64 (laptop)  08/10/2020      _x86_64_        (4 CPU)

Device            r/s     w/s     rkB/s     wkB/s   rrqm/s   wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  svctm  %util
mmcblk0          0.00    0.00      0.07      0.73     0.00     0.00   0.00   3.42    2.03   46.85   0.00    18.60   472.42   5.88   0.00
sda              4.52   11.59    125.31    169.50     0.69     8.18  13.17  41.39    0.65    1.86   0.01    27.73    14.63   0.19   0.31
sdb              0.00    0.00      0.11      0.00     0.00     0.00   0.00   0.00    4.48    3.73   0.00    27.04     0.00   4.23   0.00
dm-0             0.87    1.95     25.49     13.11     0.00     0.00   0.00   0.00    0.64    1.12   0.00    29.37     6.71   0.56   0.16
loop0            0.00    0.00      0.01      0.00     0.00     0.00   0.00   0.00    0.54    0.00   0.00    35.09     0.00   0.32   0.00
loop1            0.00    0.00      0.01      0.00     0.00     0.00   0.00   0.00    0.62    0.00   0.00    21.96     0.00   0.21   0.00

// Device,磁盘设备的名字

// ### IOPS:
// r/s,每秒发送给磁盘的读请求数,合并后的请求数
// w/s,每秒发送给磁盘的写请求数,合并后的请求数

// ### 吞吐量:
// rkB/s,每秒从磁盘读取的数据量
// wkB/s,每秒向磁盘写入的数据量

// rrqm/s,每秒合并的读请求数,
// wrqm/s,每秒合并的写请求数,

// %rrqm,%orrqm表示合并读请求的百分比
// %wrqm,%wrqm表示合并写请求的百分比

// ### 响应时间:r_await + w_await
// r_await,读请求处理完成等待时间,包括队列中的等待时间和设备实际处理的时间,单位为毫秒
// w_await,写请求处理完成等待时间,包括队列中的等待时间和设备实际处理的时间,单位为毫秒

// aqu-sz,平均请求队列长度,旧版中为avgqu-sz

// rareq-sz,平均读请求大小,单位为kB
// wareq-sz,平均写请求大小,单位为kB

// svctm,处理I/O请求所需的平均时间(不包括等待时间),单位为毫秒。注意这是推断的数据,并不保证完全准确

// ### 使用率:
// %util,磁盘处理I/O的时间百分比,即使用率,由于可能存在并行I/O,100%并不一定表明磁盘 I/O 饱和

// 在观测指标时,也别忘了结合请求的大小( rareq-sz 和 wareq-sz)一起分析。

进程 I/O 观测

使用 pidstat 和 iotop 这两个工具:

# pidstat -d 1
Linux 4.19.0-10-amd64 (laptop)  08/10/2020      _x86_64_        (4 CPU)

06:21:39 PM   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
06:21:40 PM     0       608      0.00      3.92      0.00       1  jbd2/dm-0-8
06:21:40 PM 64055      2158      0.00      3.92      0.00       0  qemu-system-x86
06:21:40 PM  1001      2986      0.00      3.92      0.00       0  thunderbird-bin

06:21:40 PM   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
06:21:41 PM  1001      2986      0.00      4.00      0.00       0  thunderbird-bin

// 用户 ID(UID)和进程 ID(PID)
// 每秒读取的数据大小(kB_rd/s) ,单位是 KB
// 每秒发出的写请求数据大小(kB_wr/s) ,单位是 KB
// 每秒取消的写请求数据大小(kB_ccwr/s) ,单位是 KB
// 块 I/O 延迟(iodelay),包括等待同步块 I/O 和换入块 I/O 结束的时间,单位是时钟周期。

类似于 top 的工具,你可以按照 I/O 大小对进程排序:

# iotop
Total DISK READ:         0.00 B/s | Total DISK WRITE:        31.97 K/s
Current DISK READ:       0.00 B/s | Current DISK WRITE:       8.72 K/s
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
  943 be/3 root        0.00 B/s    0.00 B/s  0.00 %  0.30 % [jbd2/dm-3-8]
18608 be/4 libvirt-    0.00 B/s   11.63 K/s  0.00 %  0.00 % qemu-system-x86_64
18458 be/4 libvirt-    0.00 B/s    2.91 K/s  0.00 %  0.00 % qemu-system

// 前两行分别表示,进程的磁盘读写大小总数和磁盘真实的读写大小总数。因为缓存、缓冲区、I/O 合并等因素的影响,它们可能并不相等
// 剩下的部分是从各个角度来分别表示进程的 I/O 情况,包括线程 ID、I/O 优先级、每秒读磁盘的大小、每秒写磁盘的大小、换入和等待 I/O 的时钟百分比等

参考文献

24 | 基础篇:Linux 磁盘I/O是怎么工作的(上)