31-套路篇:磁盘I/O性能优化的几个思路





I/O基准测试

I/O性能优化的目标是什么?
换句话说观察的这些I/O性能指标(比如IOPS、吞吐量、延迟等),要达到多少才合适呢?

事实上,I/O性能指标的具体标准,每个人估计会有不同的答案,因为每个人的应用场景、使用的文件系统和物理磁盘等,都有可能不一样
为了更客观合理地评估优化效果,首先应该对磁盘和文件系统进行基准测试
得到文件系统或者磁盘I/O的极限性能



fio测试工具

fio(Flexible I/O Tester)是最常用的文件系统和磁盘I/O性能基准测试工具
它提供了大量的可定制化选项,可以用来测试裸盘或者文件系统在各种场景下的I/O性能
包括了不同块大小、不同I/O引擎以及是否使用缓存等场景

fio的安装

# centos
[root@local_sa_192-168-1-6 ~]# yum install -y fio

fio的选项非常多, 通过几个常见场景的测试方法,介绍一些最常用的选项
这些常见场景包括随机读随机写顺序读以及顺序写

# 随机读
[root@local_sa_192-168-1-6 ~]# fio -name=randread -direct=1 -iodepth=64 -rw=randread -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sda

# 随机写
[root@local_sa_192-168-1-6 ~]# fio -name=randwrite -direct=1 -iodepth=64 -rw=randwrite -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sda

# 顺序读
[root@local_sa_192-168-1-6 ~]# fio -name=read -direct=1 -iodepth=64 -rw=read -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sda

# 顺序写
[root@local_sa_192-168-1-6 ~]# fio -name=write -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sda

### 参数解释
1. direct表示是否跳过系统缓存,1就表示跳过系统缓存
2. iodepth表示使用异步I/O(asynchronous I/O,简称AIO)时,同时发出的I/O请求上限,示例中设置的是64
3. rw表示I/O模式,read/write分别表示顺序读/写,randread/randwrite则分别表示随机读/写
4. ioengine表示I/O引擎,它支持同步(sync)、异步(libaio)、内存映射(mmap)、网络(net)等各种I/O引擎
上面示例中设置的libaio表示使用异步I/O
5. bs表示I/O的大小,示例设置成了4K(这也是默认值)
6. filename表示文件路径,可以是磁盘路径(测试磁盘性能),也可以是文件路径(测试文件系统性能)
示例中把它设置成了磁盘/dev/sda
不过注意用磁盘路径测试写,会破坏这个磁盘中的文件系统,所以在使用前一定要事先做好数据备份

使用fio测试顺序读的一个报告示例

[root@local_sa_192-168-1-6 ~]# fio -name=read -direct=1 -iodepth=64 -rw=read -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sda
read: (g=0): rw=read, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.7
Starting 1 process
Jobs: 1 (f=1): [R(1)][100.0%][r=193MiB/s,w=0KiB/s][r=49.5k,w=0 IOPS][eta 00m:00s]
read: (groupid=0, jobs=1): err= 0: pid=29635: Thu Dec 23 15:36:30 2021
   read: IOPS=36.0k, BW=145MiB/s (152MB/s)(1024MiB/7085msec)
    slat (nsec): min=0, max=766383, avg=7182.49, stdev=21669.78
    clat (usec): min=311, max=264215, avg=1712.73, stdev=5782.78
     lat (usec): min=316, max=264219, avg=1722.41, stdev=5782.67
    clat percentiles (usec):
     |  1.00th=[   996],  5.00th=[  1012], 10.00th=[  1106], 20.00th=[  1123],
     | 30.00th=[  1123], 40.00th=[  1139], 50.00th=[  1172], 60.00th=[  1303],
     | 70.00th=[  1614], 80.00th=[  1729], 90.00th=[  1860], 95.00th=[  1942],
     | 99.00th=[  9634], 99.50th=[ 19006], 99.90th=[ 57410], 99.95th=[128451],
     | 99.99th=[263193]
   bw (  KiB/s): min=48936, max=216416, per=100.00%, avg=148128.57, stdev=47396.09, samples=14
   iops        : min=12234, max=54104, avg=37032.14, stdev=11849.02, samples=14
  lat (usec)   : 500=0.01%, 750=0.01%, 1000=3.29%
  lat (msec)   : 2=92.23%, 4=3.13%, 10=0.43%, 20=0.52%, 50=0.26%
  lat (msec)   : 100=0.06%, 250=0.05%, 500=0.02%
  cpu          : usr=19.73%, sys=24.89%, ctx=21432, majf=0, minf=76
  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 rwts: total=262144,0,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
   READ: bw=145MiB/s (152MB/s), 145MiB/s-145MiB/s (152MB/s-152MB/s), io=1024MiB (1074MB), run=7085-7085msec

Disk stats (read/write):
  sda: ios=20458/0, merge=235263/0, ticks=11379/0, in_queue=11379, util=98.67%
  
##
这个报告中需要重点关注的是slat、clat、lat以及bw和iops这几行
事实上slat、clat、lat都是指I/O延迟(latency),不同之处在于
1. slat是指从I/O提交到实际执行I/O的时长(Submission latency)
2. clat是指从I/O提交到I/O完成的时长(Completion latency)
3. lat是指从fio创建I/O到I/O完成的总时长
##
4. bw是指吞吐量,在示例中平均吞吐量大约是145MB(148128KiB/1024)
5. iops是指每秒I/O的次数,上面示例中的平均IOPS为36000
##
对同步I/O来说,由于I/O提交和I/O完成是一个动作,所以slat实际上就是I/O完成的时间,而clat是0
使用异步I/O(libaio)时,lat近似等于slat+clat之和


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

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

# 使用blktrace跟踪磁盘I/O,注意指定应用程序正在操作的磁盘
[root@local_sa_192-168-1-6 ~]# blktrace /dev/sda

# 查看blktrace记录的结果
[root@local_sa_192-168-1-6 ~]# ls
sda.blktrace.0 sda.blktrace.1

# 将结果转化为二进制文件
[root@local_sa_192-168-1-6 ~]# blkparse sda -d sda.bin

# 使用fio重放日志
[root@local_sa_192-168-1-6 ~]# fio --name=replay --filename=/dev/sda --direct=1 --read_iolog=sda.bin

##
这样就通过blktrace+fio的组合使用,得到了应用程序I/O模式的基准测试报告




I/O性能优化

得到I/O基准测试报告后,再用上一节总结的性能分析套路,找出I/O的性能瓶颈并优化,就是水到渠成的事情了
当然想要优化I/O性能,肯定离不开Linux系统的I/O栈图的思路辅助,可以结合下面的 I/O 栈图再回顾一下
image-20211223160326611



应用程序优化

应用程序处于整个I/O栈的最上端,它可以通过系统调用,来调整 I/O 模式(如顺序还是随机、同步还是异步)
同时,它也是I/O数据的最终来源,可以有这么几种方式来优化应用程序的I/O性能

  1. 可以用追加写代替随机写,减少寻址开销,加快I/O写的速度
  2. 可以借助缓存I/O,充分利用系统缓存,降低实际I/O的次数
  3. 可以在应用程序内部构建自己的缓存,或者用Redis这类外部缓存系统
    这样一方面,能在应用程序内部,控制缓存的数据和生命周期
    另一方面,也能降低其他应用程序使用缓存对自身的影响
  4. 在需要频繁读写同一块磁盘空间时,可以用mmap代替read/write,减少内存的拷贝次数
  5. 在需要同步写的场景中,尽量将写请求合并,而不是让每个请求都同步写入磁盘
    即可以用fsync()取代O_SYNC
  6. 在多个应用程序共享相同磁盘时,为了保证I/O不被某个应用完全占用
    推荐使用cgroups的I/O子系统,来限制进程/进程组的IOPS以及吞吐量
  7. 在使用CFQ调度器时,可以用ionice来调整进程的I/O调度优先级,特别是提高核心应用的I/O优先级
    ionice支持三个优先级类:Idle、Best-effort和Realtime
    其中Best-effort和Realtime还分别支持 0-7 的级别,数值越小,则表示优先级别越高


文件系统优化

应用程序访问普通文件时,实际是由文件系统间接负责,文件在磁盘中的读写
所以,跟文件系统中相关的也有很多优化I/O性能的方式

  1. 可以根据实际负载场景的不同,选择最适合的文件系统
    比如Ubuntu默认使用ext4文件系统,而CentOS 7默认使用xfs文件系统
    相比于ext4,xfs支持更大的磁盘分区和更大的文件数量,如xfs支持大于16TB的磁盘
    但是xfs文件系统的缺点在于无法收缩,而ext4则可以
  2. 在选好文件系统后,还可以进一步优化文件系统的配置选项
    包括文件系统的特性(如ext_attr、dir_index)
    日志模式(如journal、ordered、writeback)
    挂载选项(如noatime)等等
    比如使用tune2fs这个工具,可以调整文件系统的特性(tune2fs 也常用来查看文件系统超级块的内容)
    而通过/etc/fstab或者mount命令行参数,我们可以调整文件系统的日志模式和挂载选项等
  3. 可以优化文件系统的缓存
    比如可以优化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栈的最底层
从磁盘角度出发,自然也有很多有效的性能优化方法

  1. 最简单有效的优化方法,就是换用性能更好的磁盘,比如用SSD替代HDD
  2. 使用RAID把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列
    这样做既可以提高数据的可靠性,又可以提升数据的访问性能
  3. 针对磁盘和应用程序I/O模式的特征,可以选择最适合的I/O调度算法
    比方说SSD和虚拟机中的磁盘,通常用的是noop调度算法
    而数据库应用,推荐使用deadline算法
  4. 可以对应用程序的数据,进行磁盘级别的隔离
    比如可以为日志、数据库等I/O压力比较重的应用,配置单独的磁盘
  5. 在顺序读比较多的场景中,可以增大磁盘的预读数据
    比如可以通过下面两种方法,调整/dev/sdb的预读大小
    1. 调整内核选项/sys/block/sdb/queue/read_ahead_kb,默认大小是128KB,单位为KB
    2. 使用blockdev工具设置,比如blockdev --setra 8192 /dev/sdb,注意这里的单位是512B(0.5KB),所以它的数值总是read_ahead_kb的两倍
  6. 可以优化内核块设备I/O的选项
    比如可以调整磁盘队列的长度/sys/block/sdb/queue/nr_requests
    适当增大队列长度,可以提升磁盘的吞吐量(当然也会导致I/O延迟增大)
  7. 要注意磁盘本身出现硬件错误,也会导致I/O性能急剧下降,所以发现磁盘性能急剧下降时
    需要确认,磁盘本身是不是出现了硬件错误
    比如可以查看dmesg中是否有硬件I/O故障的日志
    还可以使用badblocks、smartctl 等工具,检测磁盘的硬件问题
    或用e2fsck等来检测文件系统的错误
    如果发现问题可以使用fsck等工具来修复



小结

常见的文件系统和磁盘I/O的性能优化思路和方法
发现I/O性能问题后,不要急于动手优化
而要先找出最重要的、可以最大程度提升性能的问题
然后再从I/O栈的不同层入手,考虑具体的优化方法

记住,磁盘和文件系统的I/O,通常是整个系统中最慢的一个模块
所以在优化I/O问题时,除了可以优化I/O的执行流程
还可以借助更快的内存、网络、CPU等,减少I/O调用

可以充分利用系统提供的Buffer、Cache
或是应用程序内部缓存, 再或者Redis这类的外部缓存系统


posted @ 2021-12-23 16:26  李成果  阅读(1082)  评论(0)    收藏  举报