Linux 磁盘IO是怎么工作的?

linux磁盘是怎么工作的呢,又有哪些指标可以衡量它的性能呢?

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

image.png

机械磁盘和固态硬盘对比图

image.png

簇也叫逻辑块

现代硬盘寻道都是采用CHS(Cylinder Head Sector)的方式,硬盘读取数据时,读写磁头沿径向移动,移到要读取的扇区所在磁道的上方,这段时间称为寻道时间(seek time)。因读写磁头的起始位置与目标位置之间的距离不同,寻道时间也不同。目前硬盘一般为2到30毫秒,平均约为9毫秒。磁头到达指定磁道后,然后通过盘片的旋转,使得要读取的扇区转到读写磁头的下方,这段时间称为旋转延迟时间(rotational latencytime)。

第一类,机械磁盘,也称为硬盘驱动器(Hard Disk Driver),通常缩写为 HDD。机械磁盘主要由盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。

显然,如果 I/O 请求刚好连续,那就不需要磁道寻址,自然可以获得最佳性能。这其实就是我们熟悉的,连续 I/O 的工作原理。与之相对应的,当然就是随机 I/O,它需要不停地移动磁头,来定位数据位置,所以读写速度就会比较慢。

第二类,固态磁盘(Solid State Disk),通常缩写为 SSD,由固态电子元器件组成。固态磁盘不受马达转速瓶颈影响,也不需要磁道寻址,所以,不管是连续 I/O,还是随机 I/O 的性能,都比机械磁盘要好得多。

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

而对固态磁盘来说,虽然它的随机性能比机械硬盘好很多,但同样存在“先擦除再写入”的限制。随机读写会导致大量的垃圾回收,所以相对应的,随机 I/O 的性能比起连续 I/O 来,也还是差了很多。

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

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

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

最简单的,就是直接作为独立磁盘设备来使用。这些磁盘,往往还会根据需要,划分为不同的逻辑分区,每个分区再用数字编号。比如我们前面多次用到的 /dev/sda ,还可以分成两个分区 /dev/sda1 和 /dev/sda2。

另一个比较常用的架构,是把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列,也就是 RAID(Redundant Array of Independent Disks),从而可以提高数据访问的性能,并且增强数据存储的可靠性。常见的有 RAID0 

RAID5 RAID10

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

其实在 Linux 中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。

通用块层,其实是处在文件系统和磁盘驱动中间的一个块设备抽象层。它主要有两个功能 。

第一个功能跟虚拟文件系统的功能类似。

向上,为文件系统和应用程序,提供访问块设备的标准接口;

向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。

第二个功能,通用块层还会给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。

其中,对 I/O 请求排序的过程,也就是我们熟悉的 I/O 调度。事实上,Linux 内核支持四种 I/O 调度算法,分别是 NONE、NOOP、CFQ 以及 DeadLine。这里我也分别介绍一下。

第一种 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 的优先级调度,所以它适用于运行大量进程的系统,像是桌面环境、多媒体应用等。

二 磁盘性能指标

磁盘性能的衡量标准,必须要提到五个常见指标,也就是我们经常用到的

使用率,饱和度,IOPS,吞吐量,响应时间。

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

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

IOPS(Input/Output Per Second),是指每秒的 I/O 请求数。

吞吐量,是指每秒的 I/O 请求大小。

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

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

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

1 磁盘io的观测

常用的命令: iostat -d -x 1

image.png

%util  ,就是我们前面提到的磁盘 I/O 使用率;

r/s+  w/s  ,就是 IOPS;

rkB/s+wkB/s ,就是吞吐量;

r_await+w_await ,就是响应时间。

iostat 不能直接得到磁盘饱和度

2 进程的I/O观测

pidstat 和 iotop

pidstat -d 1 

13:39:51      UID       PID   kB_rd/s   kB_wr/s  kB_ccwr/s  iodelay  Command 

13:39:52      102       916      0.00      4.00       0.00             0  rsyslogd

UID:用户的UID PID 进程号

KB_rd/s: 每秒读取数据大小,单位kB

kB_wr/s :每秒发出的写请求数据大小 单位kB

kB_ccwr/s: 每秒取消的写请求数据大小,单位kB

块I/O 延迟iodelay,包括等待同步块I/O和换入块I/O结束时间,单位是时钟周期。

iotop

Total DISK READ :       0.00 B/s | Total DISK WRITE :       7.85 K/s 

Actual DISK READ:       0.00 B/s | Actual DISK WRITE:       0.00 B/s 

  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND 

15055 be/3 root        0.00 B/s    7.85 K/s 0.00 %   0.00 %   systemd-journald 

Total DISK READ

从这个输出,你可以看到,前两行分别表示,进程的磁盘读写大小总数和磁盘真实的读写大小总数。

因为缓存、缓冲区、I/O 合并等因素的影响,它们可能并不相等。

TID:thread id 线程id,

PRIO:priority 优先级

USER:用户

DISK READ :每秒读取磁盘的大小,

DISK WRITE: 每秒写磁盘的大小

SWAPIN:换入的百分比

IO> : 等待IO的时钟百分比

三 磁盘的性能测试

(Flexible I/O Tester)正是最常用的文件系统和磁盘 I/O 性能基准测试工具。它提供了大量的可定制化选项,可以用来测试,裸盘或者文件系统在各种场景下的 I/O 性能,包括了不同块大小、不同 I/O 引擎以及是否使用缓存等场景。性能测试建议直接通过写裸盘的方式进行测试,会得到较为真实的数据。但直接测试裸盘会破坏文件系统结构,导致数据丢失,请在测试前确认磁盘中数据已备份

安装命令:

apt-get install -y fio

fio -name=randread -direct=1 -iodepth=64 -rw=randread -ioengine=libaio -bs=4k -size=1G 

-numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sda

-name 是 起个名字

-direct 表示是否跳过系统缓存。1表示跳过缓存

-iodepth 表示使用异步I/O(asynchronous I/O 简称AIO) 时,同时发出的I/O 请求上限。

rw 表示I/O 模式 read/write 分别表示顺序读/写,而 randread/randwrite 则分别表示随机读/写。

-ioengine  表示I/O 引擎,它支持同步sync,异步libaio,内存映射mmap、网络net 等各种I/O

-bs 表示单次I/O块文件的大小 

-size 测试文件大小为1G 以每次4k的io进程测试

-numjobs 本次测试线程为1

-runtime 单位是秒 1000秒,如果没有这个参数,一直将1G文件分每次4k 每次读完为止

-group_reporting 汇总每个进程的信息

-filename  表示文件路径,当然,它可以是磁盘路径(测试磁盘性能),也可以是文件路径(测试文件系统性能)。不过注意,用磁盘路径测试写,会破坏这个磁盘中的文件系统,所以在使用前,你一定要事先做好数据备份。

执行结果

read: (g=0): rw=read, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64

fio-3.1

Starting 1 process

Jobs: 1 (f=1): [R(1)][41.7%][r=93.5MiB/s,w=0KiB/s][r=23.9k,w=0 IOPS][eta 00m:07s]

read: (groupid=0, jobs=1): err= 0: pid=15736: Wed May 15 09:52:03 2019

   read: IOPS=24.6k, BW=96.2MiB/s (101MB/s)(1024MiB/10639msec)

    slat (nsec): min=1027, max=4613.5k, avg=37668.74, stdev=28576.60

    clat (usec): min=2, max=28678, avg=2557.76, stdev=632.33

     lat (usec): min=35, max=28712, avg=2595.99, stdev=637.71

    clat percentiles (usec):

     |  1.00th=[ 1729],  5.00th=[ 2212], 10.00th=[ 2245], 20.00th=[ 2245],

     | 30.00th=[ 2278], 40.00th=[ 2311], 50.00th=[ 2376], 60.00th=[ 2507],

     | 70.00th=[ 2606], 80.00th=[ 2769], 90.00th=[ 3097], 95.00th=[ 3359],

     | 99.00th=[ 4293], 99.50th=[ 4817], 99.90th=[ 9634], 99.95th=[11207],

     | 99.99th=[24773]

   bw (  KiB/s): min=83832, max=103288, per=96.12%, avg=94731.78, stdev=5825.20, samples=9

   iops        : min=20958, max=25822, avg=23682.67, stdev=1456.27, samples=9

  lat (usec)   : 4=0.01%, 50=0.01%, 100=0.01%, 250=0.01%, 500=0.01%

  lat (usec)   : 750=0.01%, 1000=0.01%

  lat (msec)   : 2=1.72%, 4=96.77%, 10=1.39%, 20=0.07%, 50=0.02%

  cpu          : usr=0.44%, sys=90.92%, ctx=230013, majf=0, minf=75

  IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%

     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%

     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%

     issued rwt: total=262144,0,0, short=0,0,0, dropped=0,0,0

     latency   : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):

   READ: bw=96.2MiB/s (101MB/s), 96.2MiB/s-96.2MiB/s (101MB/s-101MB/s), io=1024MiB (1074MB), run=10639-10639msec

Disk stats (read/write):

  sda: ios=106720/0, merge=60/0, ticks=5588/0, in_queue=5588, util=96.06%

bw: 磁盘的吞吐量 大约 在 96.2M  

iops:其实就是每秒 I/O 的次数  23682.67

通常情况下,应用程序的 I/O 都是读写并行的,而且每次的 I/O 大小也不一定相同。所以,刚刚说的这几种场景,并不能精确模拟应用程序的 I/O 模式。那怎么才能精确模拟应用程序的 I/O 模式呢?

幸运的是,fio 支持 I/O 的重放。借助前面提到过的 blktrace,再配合上 fio,就可以实现对应用程序 I/O 模式的基准测试。你需要先用 blktrace ,记录磁盘设备的 I/O 访问情况;然后使用 fio ,重放 blktrace 的记录。

# 使用 blktrace 跟踪磁盘 I/O,注意指定应用程序正在操作的磁盘

$ blktrace /dev/sdb  

程序操作完毕之后 按ctrl+c 

# 查看 blktrace 记录的结果

# ls

sdb.blktrace.0  sdb.blktrace.1

# 将结果转化为二进制文件

$ blkparse sdb -d sdb.bin

# 使用 fio 重放日志

$ fio --name=replay --filename=/dev/sdb --direct=1 --read_iolog=sdb.bin 

四 磁盘优化

数据的持久化存储,最终还是要落到具体的物理磁盘中,同时,磁盘也是整个 I/O 栈的最底层。从磁盘角度出发,自然也有很多有效的性能优化方法。

第一,最简单有效的优化方法,就是换用性能更好的磁盘,比如用 SSD 替代 HDD。

第二,我们可以使用 RAID ,把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列。这样做既可以提高数据的可靠性,又可以提升数据的访问性能。

第三,针对磁盘和应用程序 I/O 模式的特征,我们可以选择最适合的 I/O 调度算法。比方说,SSD 和虚拟机中的磁盘,通常用的是 noop 调度算法。而数据库应用,我更推荐使用 deadline 算法。

第四,我们可以对应用程序的数据,进行磁盘级别的隔离。比如,我们可以为日志、数据库等 I/O 压力比较重的应用,配置单独的磁盘。

第五,在顺序读比较多的场景中,我们可以增大磁盘的预读数据,比如,你可以通过下面两种方法,调整 /dev/sdb 的预读大小。

比如,你可以查看 dmesg 中是否有硬件 I/O 故障的日志。  还可以使用 badblocks、smartctl 等工具,检测磁盘的硬件问题,或用 e2fsck 等来检测文件系统的错误。如果发现问题,你可以使用 fsck 等工具来修复。

参考文档:

https://blog.csdn.net/xingjiarong/article/details/46312571 

http://oserror.com/backend/ssd-principle  ssd 擦除再写入的过程

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注