Linux下系统 I/O 性能分析的套路

如何快速分析定位 I/O 性能问题

1. 文件系统 I/O性能指标

首先,想到是存储空间的使用情况,包括容量、使用量、以及剩余空间等。我们通常也称这些为磁盘空间的用量,但是这只是文件系统向外展示的空间使用,而非在磁盘空间的真实用量,因为文件系统的元数据也会占用磁盘空间。而且,如果你配置了RAID,从文件系统看到的使用量跟实际磁盘的占用空间,也会因为RAID级别不同而不一样。

除了数据本身的存储空间,还有一个容易忽略的是索引节点的使用情况,包括容量、使用量、剩余量。如果文件系统小文件数过多,可能会碰到索引节点容量已满的问题。

其次,缓存使用情况,包括页缓存、索引节点缓存、目录项缓存以及各个具体文件系统的缓存。通过使用内存,来临时缓存文件数据或者文件系统元数据,从而减少磁盘访问次数。·

最后,文件 I/O的性能指标,包括IOPS(r/s、w/s)、响应时间(延迟)、吞吐量(B/s)等。考察这类指标时,还要结合实际文件读写情况,文件大小、数量、I/O类型等,综合分析文件 I/O 的性能。

2. 磁盘 I/O性能指标

磁盘 I/O的性能指标,主要由四个核心指标:使用率、IOPS、响应时间、吞吐量,还有一个前面提到过,缓冲区。

考察这些指标时,一定要注意综合 I/O的具体场景来分析,比如读写类型(顺序读写还是随机读写)、读写比例、读写大小、存储类型(有无RAID、RAID级别、本地存储还是网络存储)等。

不过考察这些指标时,有一个大忌,就是把不同场景的 I/O指标拿过来作对比。

3. 性能工具

一类:df、top、iostat、pidstat;

二类:/proc/meminfo、/proc/slabinfo、slabtop;

三类:strace、lsof、filetop、opensnoop

4. 性能指标及性能工具之间的关系

      

5. 如何迅速分析 I/O性能瓶颈

简单来说,就是找关联。多种性能指标间,都是存在一定的关联性。想弄清楚指标之间的关联性,就要知晓各种指标的工作原理。出现性能问题,基本的分析思路是:

先用 iostat发现磁盘 I/O的性能瓶颈;

再借助 pidstat,定位导致性能瓶颈的进程;

随后分析进程 I/O的行为;

最后,结合应用程序的原理,分析这些 I/O的来源。

图中列出最常用的几个文件系统和磁盘 I/O的性能分析工具,及相应的分析流程。

 

磁盘 I/O性能优化的几个思路

1. I/O基准测试

在优化之前,我们要清楚 I/O性能优化的目标是什么?也就是说,我们观察的这些 I/O指标(IOPS、吞吐量、响应时间等),要达到多少才合适?为了更客观合理地评估优化效果,首先应该对磁盘和文件系统进行基准测试,得到它们的极限性能。

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

fio的选项非常多,这里介绍几个常用的:

# 随机读
fio -name=randread -direct=1 -iodepth=64 -rw=randread -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
# 随机写
fio -name=randwrite -direct=1 -iodepth=64 -rw=randwrite -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
# 顺序读
fio -name=read -direct=1 -iodepth=64 -rw=read -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
# 顺序写
fio -name=write -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb 

重点解释几个参数:

  • direct,是否跳过系统缓存,iodepth1 是跳过。
  • iodepth,使用异步 I/O(AIO)时,同时发出的请求上限。
  • rw,I/O模式,顺序读 / 写、随机读 / 写。
  • ioengine,I/O引擎,支持同步(sync)、异步(libaio)、内存映射(mmap)、网络等各种 I/O引擎。
  • bs,I/O大小。 4k,默认值。
  • filename,文件路径,可以是磁盘路径,也可以是文件路径。不过要注意,用磁盘路径测试写,会破坏这个磁盘的文件系统,所以测试前,要注意备份。

下面展示, fio测试顺序读的示例:

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)][100.0%][r=16.7MiB/s,w=0KiB/s][r=4280,w=0 IOPS][eta 00m:00s]
read: (groupid=0, jobs=1): err= 0: pid=17966: Sun Dec 30 08:31:48 2018
   read: IOPS=4257, BW=16.6MiB/s (17.4MB/s)(1024MiB/61568msec)
    slat (usec): min=2, max=2566, avg= 4.29, stdev=21.76
    clat (usec): min=228, max=407360, avg=15024.30, stdev=20524.39
     lat (usec): min=243, max=407363, avg=15029.12, stdev=20524.26
    clat percentiles (usec):
     |  1.00th=[   498],  5.00th=[  1020], 10.00th=[  1319], 20.00th=[  1713],
     | 30.00th=[  1991], 40.00th=[  2212], 50.00th=[  2540], 60.00th=[  2933],
     | 70.00th=[  5407], 80.00th=[ 44303], 90.00th=[ 45351], 95.00th=[ 45876],
     | 99.00th=[ 46924], 99.50th=[ 46924], 99.90th=[ 48497], 99.95th=[ 49021],
     | 99.99th=[404751]
   bw (  KiB/s): min= 8208, max=18832, per=99.85%, avg=17005.35, stdev=998.94, samples=123
   iops        : min= 2052, max= 4708, avg=4251.30, stdev=249.74, samples=123
  lat (usec)   : 250=0.01%, 500=1.03%, 750=1.69%, 1000=2.07%
  lat (msec)   : 2=25.64%, 4=37.58%, 10=2.08%, 20=0.02%, 50=29.86%
  lat (msec)   : 100=0.01%, 500=0.02%
  cpu          : usr=1.02%, sys=2.97%, ctx=33312, 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=16.6MiB/s (17.4MB/s), 16.6MiB/s-16.6MiB/s (17.4MB/s-17.4MB/s), io=1024MiB (1074MB), run=61568-61568msec

Disk stats (read/write):
  sdb: ios=261897/0, merge=0/0, ticks=3912108/0, in_queue=3474336, util=90.09% 

这个示例中,重点关注几行,slat、clat、lat,以及 bw和 iops。前三者,都是指 I/O延迟,但是有不同之处:

slat,是指从 I/O提交到实际执行 I/O的时长;

clat,是指从 I/O提交到 I/O完成的时长;

lat,是指从 fio创建 I/O 到 I/O完成的时长。

这里需要注意的是,对同步 I/O来说,提交和完成是一个动作,slat就是 I/O完成的时间,clat是0;使用异步 I/O时,lat 约等于 slat + clat。

再来看bw,他表示吞吐量,上面的输出中,平均吞吐量是16MB(17005/1024)。

最后的IOPS,其实是每秒 I/O的次数,上面输出的平均 IOPS是 4250.

通常情况下,应用程序的IO 读写是并行的,每次的 I/O大小也不相同。所以上面的几个场景并不能精确模拟应用程序的 I/O模式。幸运的是,fio支持 I/O 的重放,需要先用 blktrace,记录磁盘设备的 I/O访问情况,再使用 fio,重放 blktrace的记录。

# 使用blktrace跟踪磁盘I/O,注意指定应用程序正在操作的磁盘
$ blktrace /dev/sdb
# 查看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 

2. 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优先级,他支持三个优先级类:Idle、Best-effort 和 Realtime。其中,后两者还支持 0-7的级别,数值越小,优先级越高。

  • 文件系统优化

应用程序在访问普通文件时,是通过文件系统间接负责,文件在磁盘中的读写。所以跟文件系统相关的也有很多优化方式。

第一,可以根据实际负载场景的不同,选择合适的文件系统。比如,Ubuntu默认使用ext4,Centos默认使用 xfs。相比于ext4,xfs支持更大的磁盘分区和更大的文件数量。xfs支持大于 16TB的磁盘,但它的缺点在于无法收缩,而ext4可以。

第二,在选好文件系统后,可以优化文件系统得配置选项。包括文件系统的特性(如 ext_attr、dir_index)、日志模式(如 journal、ordered、writeback等)、挂载选项(如 noatime)等等。比如在使用 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调度算法。

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

第五,在顺序读比较多的场景中,可以增大磁盘的预读数据,可以通过两种方法,调整 /dev/sdb的预读大小。一种,调整内核选项,/sys/block/sdb/queue/read_ahead_kb,默认大小128KB。另一种,blockdev工具,比如,blockdev  --setra  8192  /dev/sdb ,注意这里的单位是 512B,所以它的数值总是 read_ahead_kb的两倍。

第六,优化内核块设备 I/O的选项。比如,调整磁盘队列的长度,/sys/block/sdb/queue/nr_requests,适当增大队列长度,可以增大磁盘的吞吐量,当然也会增大 I/O延迟。

最后,磁盘本身的硬件错误,也会导致 I/O性能急剧下降。比如,查看 dmesg中是否有硬件 I/O故障的日志,还可以使用badblocks、smartctl等工具,检测磁盘的硬件问题,或用 e2fsck等来检测文件系统错误。如果发现问题,可使用fsck 等工具修复。

案例分析

1. 疯狂打日志的“内鬼”

在某个系统当中执行top后输出结果如下:

# 按1切换到每个CPU的使用情况 
$ top 
top - 14:43:43 up 1 day,  1:39,  2 users,  load average: 2.48, 1.09, 0.63 
Tasks: 130 total,   2 running,  74 sleeping,   0 stopped,   0 zombie 
%Cpu0  :  0.7 us,  6.0 sy,  0.0 ni,  0.7 id, 92.7 wa,  0.0 hi,  0.0 si,  0.0 st 
%Cpu1  :  0.0 us,  0.3 sy,  0.0 ni, 92.3 id,  7.3 wa,  0.0 hi,  0.0 si,  0.0 st 
KiB Mem :  8169308 total,   747684 free,   741336 used,  6680288 buff/cache 
KiB Swap:        0 total,        0 free,        0 used.  7113124 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND 
18940 root      20   0  656108 355740   5236 R   6.3  4.4   0:12.56 python 
1312 root      20   0  236532  24116   9648 S   0.3  0.3   9:29.80 python3 

发现 CPU0的使用率非常高,sy  6%,wa 92.7%,说明可能正在运行 I/O 密集型 进程。再看下面的进程,python进程 CPU% 达到 6.3%,其余的进程都很低,看起来可能是python这个进程有问题。再看看内存使用情况。8GB 总内存 只剩下 700MB,Buffer/Cache 占用 6G之多,说明内存被缓存占用。虽然大部分缓存可回收,还是去看看缓存的去处,确认缓存使用是否合理。到这里,可以知道,CPU的 iowait 是一个潜在瓶颈,内存的缓存占用较多,那磁盘 I/O 怎么样?

# -d表示显示I/O性能指标,-x表示显示扩展统计(即所有I/O指标) 
$ iostat -x -d 1 
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 
loop0            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 
sdb              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 
sda              0.00   64.00      0.00  32768.00     0.00     0.00   0.00   0.00    0.00 7270.44 1102.18     0.00   512.00  15.50  99.20

可以看到,磁盘 sda I/O使用率已经达到99%,很可能已经接近饱和。w/s 64、wkB/s 32M、w_await(写请求的响应时间) 7sec、aqu-sz(请求队列长度) 达到1100。超慢的响应时间和特长请求队列,进一步验证了 I/O已经达到了饱和的猜想。所以按前面说的 iowait 达到 90%的原因正是sda 的磁盘 I/O瓶颈所致。接下来该 I/O瓶颈的根源:

$ pidstat -d 1 

15:08:35      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command 
15:08:36        0     18940      0.00  45816.00      0.00      96  python 

15:08:36      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command 
15:08:37        0       354      0.00      0.00      0.00     350  jbd2/sda1-8 
15:08:37        0     18940      0.00  46000.00      0.00      96  python 
15:08:37        0     20065      0.00      0.00      0.00    1503  kworker/u4:2 

还是 python进程的写每秒超过45MB,比上面iostat还要大,显然就是这个进程导致的 I/O 瓶颈。再看 iodelay , python确实比较大 ,但是另外两个jdb2/sda1-8、kworker/u4:2 比 python还要大得多。kworker是一个内核线程,jdb2是 ext4 系统中,为了保证数据完整的内核线程。这里只要知道他们都是保证文件系统基本功能的内核线程就可以,他们延迟的根源就是大量 I/O。

其实,在前面系统文章讲过,读写文件必须通过系统调用完成,可以观察一下系统调用情况,就可以知道正在写文件的进程。常用的查看系统调用情况使用的工具这里推荐 strace。

$ strace -p 18940 
strace: Process 18940 attached 
...
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f7aee9000 
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f682e8000 
write(3, "2018-12-05 15:23:01,709 - __main"..., 314572844 
) = 314572844 
munmap(0x7f0f682e8000, 314576896)       = 0 
write(3, "\n", 1)                       = 1 
munmap(0x7f0f7aee9000, 314576896)       = 0 
close(3)                                = 0 
stat("/tmp/logtest.txt.1", {st_mode=S_IFREG|0644, st_size=943718535, ...}) = 0 

从write() 系统调用看到,在文件描述符编号为 3 的文件中,写入了300 MB的数据。在看后面的stat() 系统调用,得知文件路径 /tmp/logtest.txt.1。这种的 “点 + 数字”的格式经常在日志回滚中见到,这里就猜测是日志回滚文件。再往下:

$ lsof -p 18940 
COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF    NODE NAME 
python  18940 root  cwd    DIR   0,50      4096 1549389 / 
python  18940 root  rtd    DIR   0,50      4096 1549389 / 
… 
python  18940 root    2u   CHR  136,0       0t0       3 /dev/pts/0 
python  18940 root    3w   REG    8,1 117944320     303 /tmp/logtest.txt 

lsof 命令专门用来查看进程打开的文件列表,这里的“文件”包括目录、块设备、动态库、网络套接字等。这里先简单介绍一下 lsof的几列含义:FD 文件描述符号,TYPE 文件类型,NAME 文件路径。最后一行可以看到python进程确实打开了 /tmp/logtest.txt,并且文件描述符编号为 3,3 后面的 w,表示以写的方式打开文件,更加证实的上面的猜测。

#查看案例应用的源代码
$ cat app.py 

logger = logging.getLogger(__name__) 
logger.setLevel(level=logging.INFO) 
rHandler = RotatingFileHandler("/tmp/logtest.txt", maxBytes=1024 * 1024 * 1024, backupCount=1) 
rHandler.setLevel(logging.INFO) 

def write_log(size): 
  '''Write logs to file''' 
  message = get_message(size) 
  while True: 
    logger.info(message) 
    time.sleep(0.1) 

if __name__ == '__main__': 
  msg_size = 300 * 1024 * 1024 
  write_log(msg_size) 

那就直接查看应用源代码,发现它的日志路径是 /tmp/logtest.txt,默认记录 INFO级别以上的所有日志。每次写日志的大小 300MB,这跟上面分析结果一致。一般来说,生产系统的日志级别可动态调整,这里也可以。如果发送SIGUSR1信号,日志级别可调整为 INFO级;如果发送SIGUSR2信号,日志级别可调整为 WARNING级:

def set_logging_info(signal_num, frame): 
  '''Set loging level to INFO when receives SIGUSR1''' 
  logger.setLevel(logging.INFO) 

def set_logging_warning(signal_num, frame): 
  '''Set loging level to WARNING when receives SIGUSR2''' 
  logger.setLevel(logging.WARNING) 

signal.signal(signal.SIGUSR1, set_logging_info) 
signal.signal(signal.SIGUSR2, set_logging_warning) 

接下来在终端执行 kill命令,kill -SIGUSR2 18940 。再执行 top、iostat,你会发现,iowait 变为0,sda 磁盘的 I/O使用率也会逐渐减少到0。到这里,我们不仅成功定位狂打日志的应用,而且解决了 I/O的性能瓶颈。

2. 磁盘 I/O的延迟高

再某个系统下,执行命令时发现响应特别慢,这时候来看看系统资源使用情况:

$ top 
top - 14:27:02 up 10:30,  1 user,  load average: 1.82, 1.26, 0.76 
Tasks: 129 total,   1 running,  74 sleeping,   0 stopped,   0 zombie 
%Cpu0  :  3.5 us,  2.1 sy,  0.0 ni,  0.0 id, 94.4 wa,  0.0 hi,  0.0 si,  0.0 st 
%Cpu1  :  2.4 us,  0.7 sy,  0.0 ni, 70.4 id, 26.5 wa,  0.0 hi,  0.0 si,  0.0 st 
KiB Mem :  8169300 total,  3323248 free,   436748 used,  4409304 buff/cache 
KiB Swap:        0 total,        0 free,        0 used.  7412556 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND 
12280 root      20   0  103304  28824   7276 S  14.0  0.4   0:08.77 python 
   16 root      20   0       0      0      0 S   0.3  0.0   0:09.22 ksoftirqd/1 
1549 root      20   0  236712  24480   9864 S   0.3  0.3   3:31.38 python3 

两个CPU的 wa都非常高,特别是CPU0,达到94.4%,而剩余内存还有3GB,比较充足。再往下,进程部分的python 进程%CPU 达到14%,虽然不是瓶颈,但是有点嫌疑。查看一下系统 I/O使用情况:

-x选项 显示出扩展统计信息,即所有 I/O指标
$ iostat -d -x 1
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 
loop0            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 
sda              0.00   71.00      0.00  32912.00     0.00     0.00   0.00   0.00    0.00 18118.31 241.89     0.00   463.55  13.86  98.40 

可以看到 磁盘 sda 使用率达到98%,接近饱和,写请求响应时间 12sec,每秒写的数据 32MB,显然写磁盘遇到了瓶颈。

$ pidstat -d 1 
14:39:14      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command 
14:39:15        0     12280      0.00 335716.00      0.00       0  python 

python进程的每秒写 32MB,与上面一致。再使用strace 查看一下系统调用情况:

$ strace -p 12280 
strace: Process 12280 attached 
select(0, NULL, NULL, NULL, {tv_sec=0, tv_usec=567708}) = 0 (Timeout) 
stat("/usr/local/lib/python3.7/importlib/_bootstrap.py", {st_mode=S_IFREG|0644, st_size=39278, ...}) = 0 
stat("/usr/local/lib/python3.7/importlib/_bootstrap.py", {st_mode=S_IFREG|0644, st_size=39278, ...}) = 0 

这里并没有任何 write() 调用,很明显,这里就存在矛盾,iostat 证明有 I/O性能瓶颈,pidstat也证明是python进程导致的,但是trace却找不到任何write() 系统调用。

这里再推荐一个工具 filetop,他是bcc软件包的一部分,基于Linux内核的 eBPF(extended Berkeley Packet Filter)机制,主要跟踪内核中文件的读写情况,并输出线程 ID(TID)、读写大小、读写类型、文件名称。然后运行下面命令:

# -C 选项表示输出新内容时不清空屏幕 
$ ./filetop -C 

TID    COMM             READS  WRITES R_Kb    W_Kb    T FILE 
514    python           0      1      0       2832    R 669.txt 
514    python           0      1      0       2490    R 667.txt 
514    python           0      1      0       2685    R 671.txt 
514    python           0      1      0       2392    R 670.txt 
514    python           0      1      0       2050    R 672.txt 
...
TID    COMM             READS  WRITES R_Kb    W_Kb    T FILE 
514    python           2      0      5957    0       R 651.txt 
514    python           2      0      5371    0       R 112.txt 
514    python           2      0      4785    0       R 861.txt 
514    python           2      0      4736    0       R 213.txt 
514    python           2      0      4443    0       R 45.txt 

多观察一会儿,你会发现线程号514的python 应用会先大量的写入txt文件,再大量的读。

$ ps -efT | grep 514
root     12280  514 14626 33 14:47 pts/0    00:00:05 /usr/local/bin/python /app.py 

可以看到线程号 514属于 12280的线程,但是filetop 没给出文件路径,只有文件名称。在推荐一个命令 opensnoop,同属于bcc 软件包,可以动态追踪内核中的 open系统调用:

$ opensnoop 
12280  python              6   0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/650.txt 
12280  python              6   0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/651.txt 
12280  python              6   0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/652.txt 

可以看到打开的文件数量,按照数字编号,综合filetop 和 opensnoop,案例应用应该是在先写文件 再读入内存。

$ ls /tmp/9046db9e-fe25-11e8-b13f-0242ac110002 | wc -l 
ls: cannot access '/tmp/9046db9e-fe25-11e8-b13f-0242ac110002': No such file or directory 
0 

$ opensnoop 
12280  python              6   0 /tmp/defee970-fe25-11e8-b13f-0242ac110002/261.txt 
12280  python              6   0 /tmp/defee970-fe25-11e8-b13f-0242ac110002/840.txt 
12280  python              6   0 /tmp/defee970-fe25-11e8-b13f-0242ac110002/136.txt 

发现文件路径名在不断变化,说明这些文件是应用程序自动生成,用完就删除。正是这些文件读写引发 I/O瓶颈。查看一下源码:

@app.route("/popularity/<word>") 
def word_popularity(word): 
  dir_path = '/tmp/{}'.format(uuid.uuid1()) 
  count = 0 
  sample_size = 1000 
   
  def save_to_file(file_name, content): 
    with open(file_name, 'w') as f: 
    f.write(content) 

  try: 
    # initial directory firstly 
    os.mkdir(dir_path) 

    # save article to files 
    for i in range(sample_size): 
        file_name = '{}/{}.txt'.format(dir_path, i) 
        article = generate_article() 
        save_to_file(file_name, article) 

    # count word popularity 
    for root, dirs, files in os.walk(dir_path): 
        for file_name in files: 
            with open('{}/{}'.format(dir_path, file_name)) as f: 
                if validate(word, f.read()): 
                    count += 1 
    finally: 
        # clean files 
        shutil.rmtree(dir_path, ignore_errors=True) 

    return jsonify({'popularity': count / sample_size * 100, 'word': word}) 

源码中,确实每次都会生成一些临时文件,再读入内存,最后删掉它们。这是一种常见的利用磁盘空间处理数据的技巧,不过本例中 I/O请求过重,导致磁盘 I/O利用率过高。这里就需要做一些算法优化以避免 I/O性能问题,比如内存充足时,直接把数据放内存处理会更快。

 

 

以上是工作之余的学习总结,特此记录之。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值