linux性能评估-磁盘io概念实战篇
1.文件系统和磁盘的 IO 问题的分析(如何找出狂打日志的“内鬼”?)
预先安装 docker、sysstat 等工具,如 apt install docker.io sysstat
安装losf:yum install lsof
安装iostat::yum install iostat
1.我们在终端中执行下面的命令,运行今天的目标应用:
$ docker run -v /tmp:/tmp --name=app -itd feisky/logapp |
2.然后,在终端中运行 ps 命令,确认案例应用正常启动。如果操作无误,你应该可以在 ps 的输出中,看到一个 app.py 的进程:
ps -ef | grep /app.py root 10805 10774 42 02 : 04 pts/ 0 00 : 07 : 04 python /app.py |
可以先用 top ,来观察 CPU 和内存的使用情况;然后再用 iostat ,来观察磁盘的 I/O 情况。
3.接下来,你可以在终端中运行 top 命令,观察 CPU 和内存的使用情况:
top - 10 : 35 : 04 up 268 days, 19 : 53 , 2 users, load average: 3.00 , 3.10 , 2.85 Tasks: 169 total, 1 running, 168 sleeping, 0 stopped, 0 zombie Cpu0 : 0.3 %us, 1.3 %sy, 0.0 %ni, 0.0 %id, 98.3 %wa, 0.0 %hi, 0.0 %si, 0.0 %st Cpu1 : 2.0 %us, 1.3 %sy, 0.0 %ni, 96.7 %id, 0.0 %wa, 0.0 %hi, 0.0 %si, 0.0 %st Cpu2 : 0.0 %us, 0.0 %sy, 0.0 %ni, 73.9 %id, 26.1 %wa, 0.0 %hi, 0.0 %si, 0.0 %st Cpu3 : 14.3 %us, 43.3 %sy, 0.0 %ni, 6.7 %id, 35.7 %wa, 0.0 %hi, 0.0 %si, 0.0 %st Mem: 8061128k total, 4679996k used, 3381132k free, 294232k buffers Swap: 0k total, 0k used, 0k free, 2683132k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 30989 root 20 0 940m 910m 3304 D 57.8 11.6 4 : 59.60 python 5918 root 20 0 2146m 59m 1984 S 1.7 0.7 1713 : 02 python3 522 root 20 0 0 0 0 D 1.3 0.0 6 : 03.57 flush- 252 : 0 1274 root 20 0 2449m 69m 2672 S 1.3 0.9 2037 : 04 java |
观察 top 的输出,你会发现,CPU 的使用率非常高,它的系统 CPU 使用率(sys%)为 1.3%,而 iowait 超过了 98%。这说明 CPU0上,可能正在运行 I/O 密集型的进程。不过,究竟是什么原因呢?这个疑问先保留着,我们先继续看完。
接着我们来看,进程部分的 CPU 使用情况。你会发现, python 进程的 CPU 使用率已经达到了 57%,而其余进程的 CPU 使用率都比较低。看起来 python 是个可疑进程。记下 python 进程的 PID 号 30989,我们稍后分析。
最后再看内存的使用情况,总内存 8G,剩余内存有4G,而 Buffer/Cache 占用内存高达 2GB 之多,这说明内存主要被缓存占用。虽然大部分缓存可回收,我们还是得了解下缓存的去处,确认缓存使用都是合理的。
到这一步,你基本可以判断出,CPU 使用率中的 iowait 是一个潜在瓶颈,而内存部分的缓存占比较大,那磁盘 I/O 又是怎么样的情况呢?
4.运行 iostat 命令,观察 I/O 的使用情况:
# -d 表示显示 I/O 性能指标,-x 表示显示扩展统计(即所有 I/O 指标) [root @CESHI_Game_YALI_3 ceshi]# iostat -x -d 1 Linux 2.6 . 32 - 642.13 . 1 .el6.x86_64 (CESHI_Game_YALI_3) 05 / 09 / 2019 _x86_64_ ( 4 CPU) Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await r_await w_await svctm %util vda 0.00 0.00 0.00 72.00 0.00 72080.00 1001.11 159.52 3429.00 0.00 3429.00 13.89 100.00 dm- 0 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 dm- 1 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 dm- 2 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 dm- 3 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 |
disk属性值说明:
- rrqm/s: 每秒进行 merge 的读操作数目。即 rmerge/s
- wrqm/s: 每秒进行 merge 的写操作数目。即 wmerge/s
- r/s: 每秒完成的读 I/O 设备次数。即 rio/s
- w/s: 每秒完成的写 I/O 设备次数。即 wio/s
- rsec/s: 每秒读扇区数。即 rsect/s
- wsec/s: 每秒写扇区数。即 wsect/s
- rkB/s: 每秒读K字节数。是 rsect/s 的一半,因为每扇区大小为512字节。
- wkB/s: 每秒写K字节数。是 wsect/s 的一半。
- avgrq-sz: 平均每次设备I/O操作的数据大小 (扇区)。
- avgqu-sz: 平均I/O队列长度。
- await: 平均每次设备I/O操作的等待时间 (毫秒)。
- svctm: 平均每次设备I/O操作的服务时间 (毫秒)。
- %util: 一秒中有百分之多少的时间用于 I/O 操作,即被io消耗的cpu百分比
备注:如果 %util 接近 100%,说明产生的I/O请求太多,I/O系统已经满负荷,该磁盘可能存在瓶颈。如果 svctm 比较接近 await,说明 I/O 几乎没有等待时间;如果 await 远大于 svctm,说明I/O 队列太长,io响应太慢,则需要进行必要优化。如果avgqu-sz比较大,也表示有当量io在等待。
观察 iostat 的最后一列,你会看到,磁盘 vda 的 I/O 使用率已经高达 100%,已经 I/O 饱和。
再看前面的各个指标,每秒写磁盘请求数是 72 ,写大小是 72 MB,写请求的响应时间为 3 秒,而请求队列长度则达到了 159。
超慢的响应时间和特长的请求队列长度,进一步验证了 I/O 已经饱和的猜想。此时,sda 磁盘已经遇到了严重的性能瓶颈。
到这里,也就可以理解,为什么前面看到的 iowait 高达 100% 了,这正是磁盘 vda 的 I/O 瓶颈导致的。接下来的重点就是分析 I/O 性能瓶颈的根源了。那要怎么知道,这些 I/O 请求相关的进程呢?
5.使用 pidstat 加上 -d 参数,就可以显示每个进程的 I/O 情况。所以,你可以在终端中运行如下命令来观察:
[root @CESHI_Game_YALI_3 ceshi]# pidstat -d 1 Linux 2.6 . 32 - 642.13 . 1 .el6.x86_64 (CESHI_Game_YALI_3) 05 / 09 / 2019 _x86_64_ ( 4 CPU) 10 : 15 : 00 AM PID kB_rd/s kB_wr/s kB_ccwr/s Command 10 : 15 : 01 AM 522 0.00 11.88 0.00 flush- 252 : 0 10 : 15 : 01 AM 30989 0.00 44609.90 0.00 python |
从 pidstat 的输出,你可以发现,只有 python 进程的写比较大,而且每秒写的数据超过 45 MB,很明显,正是 python 进程导致了 I/O 瓶颈。
再往下看 iodelay 项。虽然只有 python 在大量写数据,但你应该注意到了,有两个进程 (kworker 和 kswapd0 )的延迟,居然比 python 进程还大很多。
综合 pidstat 的输出来看,还是 python 进程的嫌疑最大。接下来,我们来分析 python 进程到底在写什么。
知道了进程的 PID 号,具体要怎么查看写的情况呢?
想起 strace 了吗,它正是我们分析系统调用时最常用的工具。
6.接下来,我们在终端中运行 strace 命令,并通过 -p 10805 指定 python 进程的 PID 号:
[root @CESHI_Game_YALI_3 ceshi]# strace -p 30989 Process 30989 attached munmap( 0x7f6aa96bb000 , 314576896 ) = 0 write( 4 , "\n" , 1 ) = 1 munmap( 0x7f6abc2bc000 , 314576896 ) = 0 clock_gettime(CLOCK_MONOTONIC, { 23225970 , 456233268 }) = 0 select( 0 , NULL, NULL, NULL, { 0 , 100000 }) = 0 (Timeout) clock_gettime(CLOCK_REALTIME, { 1557368480 , 701591410 }) = 0 getpid() = 1 mmap(NULL, 314576896 , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, - 1 , 0 ) = 0x7f6abc2bc000 mmap(NULL, 393220096 , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, - 1 , 0 ) = 0x7f6aa4bbb000 mremap( 0x7f6aa4bbb000 , 393220096 , 314576896 , MREMAP_MAYMOVE) = 0x7f6aa4bbb000 munmap( 0x7f6abc2bc000 , 314576896 ) = 0 lseek( 4 , 0 , SEEK_END) = 943718535 lseek( 4 , 0 , SEEK_CUR) = 943718535 munmap( 0x7f6aa4bbb000 , 314576896 ) = 0 close( 4 ) = 0 stat( "/tmp/logtest.txt.1" , {st_mode=S_IFREG| 0644 , st_size= 943718535 , ...}) = 0 unlink( "/tmp/logtest.txt.1" ) = 0 stat( "/tmp/logtest.txt" , {st_mode=S_IFREG| 0644 , st_size= 943718535 , ...}) = 0 rename( "/tmp/logtest.txt" , "/tmp/logtest.txt.1" ) = 0 open( "/tmp/logtest.txt" , O_WRONLY|O_CREAT|O_APPEND|O_CLOEXEC, 0666 ) = 4 fcntl( 4 , F_SETFD, FD_CLOEXEC) = 0 fstat( 4 , {st_mode=S_IFREG| 0644 , st_size= 0 , ...}) = 0 lseek( 4 , 0 , SEEK_END) = 0 ioctl( 4 , TIOCGWINSZ, 0x7fffda8916e0 ) = - 1 ENOTTY (Inappropriate ioctl for device) lseek( 4 , 0 , SEEK_CUR) = 0 ioctl( 4 , TIOCGWINSZ, 0x7fffda891600 ) = - 1 ENOTTY (Inappropriate ioctl for device) lseek( 4 , 0 , SEEK_CUR) = 0 mmap(NULL, 314576896 , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, - 1 , 0 ) = 0x7f6abc2bc000 mmap(NULL, 314576896 , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, - 1 , 0 ) = 0x7f6aa96bb000 write( 4 , "2019-05-09 02:21:20,701 - __main" ..., 314572844 ) = 314572844 |
从 write() 系统调用上,我们可以看到,进程向文件描述符编号为 3 的文件中,写入了 300MB 的数据。看来,它应该是我们要找的文件。不过,write() 调用中只能看到文件的描述符编号,文件名和路径还是未知的。
再观察后面的
stat() 调用,你可以看到,它正在获取 /tmp/logtest.txt.1 的状态。 这种“点 +
数字格式”的文件,在日志回滚中非常常见。我们可以猜测,这是第一个日志回滚文件,而正在写的日志文件路径,则是 /tmp/logtest.txt。
当然,这只是我们的猜测,自然还需要验证。这里,我再给你介绍一个新的工具 lsof。它专门用来查看进程打开文件列表,不过,这里的“文件”不只有普通文件,还包括了目录、块设备、动态库、网络套接字等。
接下来,我们在终端中运行下面的 lsof 命令,看看进程 18940 都打开了哪些文件:
[root @CESHI_Game_YALI_3 ceshi]# lsof -p 30989 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME python 30989 root cwd DIR 253 , 3 4096 393217 / python 30989 root rtd DIR 253 , 3 4096 393217 / python 30989 root txt REG 253 , 3 28016 394264 /usr/local/bin/python3. 7 python 30989 root mem REG 253 , 3 394992 /usr/local/lib/python3. 7 /lib-dynload/_queue.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 394990 /usr/local/lib/python3. 7 /lib-dynload/_pickle.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 395001 /usr/local/lib/python3. 7 /lib-dynload/_struct.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 395023 /usr/local/lib/python3. 7 /lib-dynload/select.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 394998 /usr/local/lib/python3. 7 /lib-dynload/_socket.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 394993 /usr/local/lib/python3. 7 /lib-dynload/_random.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 394960 /usr/local/lib/python3. 7 /lib-dynload/_bisect.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 394996 /usr/local/lib/python3. 7 /lib-dynload/_sha3.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 394961 /usr/local/lib/python3. 7 /lib-dynload/_blake2.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 393383 /lib/libcrypto.so. 43.0 . 1 (stat: No such file or directory) python 30989 root mem REG 253 , 3 394981 /usr/local/lib/python3. 7 /lib-dynload/_hashlib.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 395015 /usr/local/lib/python3. 7 /lib-dynload/math.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 394982 /usr/local/lib/python3. 7 /lib-dynload/_heapq.cpython-37m-x86_64-linux-gnu.so (stat: No such file or directory) python 30989 root mem REG 253 , 3 393325 /etc/localtime (path dev= 252 , 1 , inode= 1966707 ) python 30989 root mem REG 253 , 3 394372 /usr/local/lib/libpython3.7m.so. 1.0 (stat: No such file or directory) python 30989 root mem REG 253 , 3 393380 /lib/ld-musl-x86_64.so. 1 (stat: No such file or directory) python 30989 root 0u CHR 136 , 1 0t0 4 / 1 python 30989 root 1u CHR 136 , 1 0t0 4 / 1 python 30989 root 2u CHR 136 , 1 0t0 4 / 1 python 30989 root 3r CHR 1 , 9 0t0 297894194 /dev/urandom python 30989 root 4w REG 252 , 1 807931904 2101841 /tmp/logtest.txt |
这个输出界面中,有几列简单介绍一下,FD 表示文件描述符号,TYPE 表示文件类型,NAME 表示文件路径。这也是我们需要关注的重点。
再看最后一行,这说明,这个进程打开了文件 /tmp/logtest.txt,并且它的文件描述符是 4 号,而 4 后面的 w ,表示以写的方式打开。
这跟刚才 strace 完我们猜测的结果一致,看来这就是问题的根源:进程 30989 以每次 300MB 的速度,在“疯狂”写日志,而日志文件的路径是 /tmp/logtest.txt。
所以,今后,在碰到这种“狂打日志”的场景时,你可以用 iostat、strace、lsof 等工具来定位狂打日志的进程,找出相应的日志文件,再通过应用程序的接口,调整日志级别来解决问题。
7.案例结束:
docker rm -f app |
2.磁盘IO延迟很高,该怎么办?
今天的案例需要两台虚拟机,其中一台是案例分析的目标机器,运行 Flask 应用,它的 IP 地址是 172.16.109.245;而另一台作为客户端,请求单词的热度。
1.首先,我们在第一个虚拟机中执行下面的命令,运行本次案例要分析的目标应用:
$ docker run --name=app -p 10000 : 80 -itd feisky/word-pop |
2.然后,在第二个终端中运行 curl 命令,访问 http://172.16.109.245:10000/,确认案例正常启动。你应该可以在 curl 的输出界面里,看到一个 hello world 的输出:
$ curl http: //172.16.109.245:10000/ hello world |
3.接下来,在第二个终端中,访问案例应用的单词热度接口,也就是 http://172.16.109.245:1000/popularity/word
curl http: //172.16.109.245:10000/popularity/word |
稍等一会儿,你会发现,这个接口居然这么长时间都没响应,究竟是怎么回事呢?我们先回到终端一来分析一下。
我们试试在第一个终端里,随便执行一个命令,比如执行 df 命令,查看一下文件系统的使用情况。奇怪的是,这么简单的命令,居然也要等好久才有输出。
通过 df 我们知道,系统还有足够多的磁盘空间。那为什么响应会变慢呢?看来还是得观察一下,系统的资源使用情况,像是 CPU、内存和磁盘 I/O 等的具体使用情况。
这里的思路其实跟上一个案例比较类似,我们可以先用 top 来观察 CPU 和内存的使用情况,然后再用 iostat 来观察磁盘的 I/O 情况。
为了避免分析过程中 curl 请求突然结束,我们回到终端二,按 Ctrl+C 停止刚才的应用程序;然后,把 curl 命令放到一个循环里执行;这次我们还要加一个 time 命令,观察每次的执行时间:
$ while true ; do time curl http: //172.16.109.245:10000/popularity/word; sleep 1; done |
继续回到终端一来分析性能。我们在终端一中运行 top 命令,观察 CPU 和内存的使用情况:
观察 top 的输出可以发现,两个 CPU 的 iowait 都非常高。特别是 CPU0, iowait 已经高达 50 %,而剩余内存还有 3GB,看起来也是充足的。
再往下看,进程部分有一个 python 进程的 CPU 使用率稍微有点高,达到了 48%。虽然 48% 并不能成为性能瓶颈,不过有点嫌疑——可能跟 iowait 的升高有关。
那这个 PID 号为 87829 的 python 进程,到底是不是我们的案例应用呢?
我们在第一个终端中,按下 Ctrl+C,停止 top 命令;然后执行下面的 ps 命令,查找案例应用 app.py 的 PID 号:
root @ubuntu :/home/xhong# ps aux|grep app.py root 87773 0.0 0.7 95808 14804 pts/ 0 Ss+ 19 : 10 0 : 00 python /app.py root 87829 17.7 0.8 97052 17620 pts/ 0 Sl+ 19 : 10 6 : 18 /usr/local/bin/python /app.py root 87969 0.0 0.0 21536 1020 pts/ 0 S+ 19 : 46 0 : 00 grep --color=auto app.py |
从 ps 的输出,你可以看到,这个 CPU 使用率较高的进程,正是我们的案例应用。不过先别着急分析 CPU 问题,毕竟 iowait 已经高达 81%, I/O 问题才是我们首要解决的。
接下来,我们在终端一中,运行下面的 iostat 命令,其中:
- -d 选项是指显示出 I/O 的性能指标;
- -x 选项是指显示出扩展统计信息(即显示所有 I/O 指标)。
root @ubuntu :/home/xhong# iostat -d -x 1 Linux 4.18 . 0 - 16 -generic (ubuntu) 03 / 12 / 2019 _x86_64_ ( 2 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 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 loop1 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 loop2 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 loop3 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 loop4 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 loop5 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 loop6 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 loop7 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 410.00 0.00 301612.00 0.00 71.00 0.00 14.76 0.00 87.20 37.47 0.00 735.64 2.06 84.40 |
明白了指标含义,再来具体观察 iostat 的输出。你可以发现,磁盘 sda 的 I/O 使用率已经达到 84% ,接近饱和了。而且,写请求的响应时间高达 87毫秒,每秒的写数据为 32MB,显然写磁盘碰到了瓶颈。
那要怎么知道,这些 I/O 请求到底是哪些进程导致的呢?我想,你已经还记得上一节我们用到的 pidstat。
在终端一中,运行下面的 pidstat 命令,观察进程的 I/O 情况:
root @ubuntu :/home/xhong# pidstat -d 1 Linux 4.18 . 0 - 16 -generic (ubuntu) 03 / 12 / 2019 _x86_64_ ( 2 CPU) 08 : 03 : 10 PM UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 08 : 03 : 11 PM 1000 1997 8.00 0.00 0.00 0 Xorg 08 : 03 : 11 PM 1000 85313 32.00 0.00 0.00 17 gnome-terminal- 08 : 03 : 11 PM 0 87829 0.00 305732.00 0.00 0 python 08 : 03 : 11 PM 0 87991 0.00 0.00 0.00 25 kworker/u256: 0 -flush- 8 : 0 |
从 pidstat 的输出,我们再次看到了 PID 号为 12280 的结果。这说明,正是案例应用引发 I/O 的性能瓶颈。
走到这一步,你估计觉得,接下来就很简单了,上一个案例不刚刚学过吗?无非就是,先用 strace 确认它是不是在写文件,再用 lsof 找出文件描述符对应的文件即可。
到底是不是这样呢?我们不妨来试试。还是在终端一中,执行下面的 strace 命令:
root @ubuntu :/home/xhong# strace -p 87829 strace: Process 87829 attached select( 0 , NULL, NULL, NULL, {tv_sec= 0 , tv_usec= 997634 }) = 0 (Timeout) futex( 0x7ffd2c24b5c4 , FUTEX_WAIT_PRIVATE, 2 , {tv_sec= 0 , tv_nsec= 4999263 }) = 0 futex( 0x7ffd2c24b5c4 , FUTEX_WAIT_PRIVATE, 2 , {tv_sec= 0 , tv_nsec= 4999719 }) = 0 |
从 strace 中,你可以看到大量的 stat 系统调用,并且大都为 python 的文件,但是,请注意,这里并没有任何 write 系统调用。
由于 strace 的输出比较多,我们可以用 grep ,来过滤一下 write,比如:
root @ubuntu :/home/xhong# strace -p 87829 2 >& 1 | grep write |
遗憾的是,这里仍然没有任何输出。
难道此时已经没有性能问题了吗?重新执行刚才的 top 和 iostat 命令,你会不幸地发现,性能问题仍然存在。
文件写,明明应该有相应的 write 系统调用,但用现有工具却找不到痕迹,这时就该想想换工具的问题了。怎样才能知道哪里在写文件呢?
这里我给你介绍一个新工具,
filetop。它是 bcc软件包的一部分,基于 Linux 内核的 eBPF(extended Berkeley Packet
Filters)机制,主要跟踪内核中文件的读写情况,并输出线程 ID(TID)、读写大小、读写类型以及文件名称。
至于老朋友 bcc 的安装方法,可以参考它的 Github 网站https://github.com/iovisor/bcc ,。比如在 Ubuntu 16 以上的版本中,你可以运行下面的命令来安装它:
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/iovisor.list sudo apt-get update sudo apt-get install bcc-tools libbcc-examples linux-headers-$(uname -r) |
安装后,bcc 提供的所有工具,就全部安装到了 /usr/share/bcc/tools 这个目录中。接下来我们就用这个工具,观察一下文件的读写情况。
首先,在终端一中运行下面的命令:
root @ubuntu :/home/xhong# cd /usr/share/bcc/tools # -C 选项表示输出新内容时不清空屏幕 root @ubuntu :/usr/share/bcc/tools# ./filetop -C Tracing... Output every 1 secs. Hit Ctrl-C to end 20 : 26 : 45 loadavg: 1.48 1.32 1.18 1 / 639 88212 TID COMM READS WRITES R_Kb W_Kb T FILE 88208 python 2 0 5517 0 R 323 .txt 88208 python 2 0 4589 0 R 346 .txt 88208 python 2 0 4541 0 R 835 .txt 88208 python 2 0 4394 0 R 154 .txt 88208 python 2 0 4345 0 R 83 .txt 88208 python 2 0 4345 0 R 352 .txt 88208 python 2 0 4345 0 R 79 .txt 88208 python 2 0 4296 0 R 665 .txt 88208 python 2 0 4248 0 R 100 .txt 88208 python 2 0 4199 0 R 932 .txt 88208 python 2 0 4150 0 R 904 .txt 88208 python 2 0 4101 0 R 234 .txt 88208 python 2 0 4101 0 R 435 .txt |
你会看到,filetop 输出了 8 列内容,分别是线程 ID、线程命令行、读写次数、读写的大小(单位 KB)、文件类型以及读写的文件名称。
这些内容里,你可能会看到很多动态链接库,不过这不是我们的重点,暂且忽略即可。我们的重点,是一个 python 应用,所以要特别关注 python 相关的内容。
多观察一会儿,你就会发现,每隔一段时间,线程号为 88208 的 python 应用就会先写入大量的 txt 文件,再大量地读。
线程号为 88208 的线程,属于哪个进程呢?我们可以用 ps 命令查看。先在终端一中,按下 Ctrl+C ,停止 filetop ;然后,运行下面的 ps 命令。这个输出的第二列内容,就是我们想知道的进程号:
root @ubuntu :/usr/share/bcc/tools# ps -efT | grep 88234 root 87829 88234 87773 65 20 : 31 pts/ 0 00 : 00 : 15 /usr/local/bin/python /app.py root 88241 88241 86638 0 20 : 31 pts/ 0 00 : 00 : 00 grep --color=auto 88234 |
我再介绍一个好用的工具,opensnoop 。它同属于 bcc 软件包,可以动态跟踪内核中的 open 系统调用。这样,我们就可以找出这些 txt 文件的路径。
root @ubuntu :/usr/share/bcc/tools# ./opensnoop PID COMM FD ERR PATH 87829 python 6 0 /tmp/6b5efe98- 4541 -11e9-a8bd-0242ac110002/ 812 .txt 87829 python 6 0 /tmp/6b5efe98- 4541 -11e9-a8bd-0242ac110002/ 813 .txt 87829 python 6 0 /tmp/6b5efe98- 4541 -11e9-a8bd-0242ac110002/ 814 .txt 87829 python 6 0 /tmp/6b5efe98- 4541 -11e9-a8bd-0242ac110002/ 815 .txt 87829 python 6 0 /tmp/6b5efe98- 4541 -11e9-a8bd-0242ac110002/ 816 .txt 87829 python 6 0 /tmp/6b5efe98- 4541 -11e9-a8bd-0242ac110002/ 817 .txt 87829 python 6 0 /tmp/6b5efe98- 4541 -11e9-a8bd-0242ac110002/ 818 .txt |
综合 filetop 和 opensnoop ,我们就可以进一步分析了。我们可以大胆猜测,案例应用在写入 1000 个 txt 文件后,又把这些内容读到内存中进行处理。我们来检查一下,这个目录中是不是真的有 1000 个文件:
root @ubuntu :/usr/share/bcc/tools# 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 再观察一会儿:
root @ubuntu :/usr/share/bcc/tools# ./opensnoop PID COMM FD ERR PATH 87829 python 6 0 /tmp/d607ce46- 4541 -11e9-a8bd-0242ac110002/ 851 .txt 87829 python 6 0 /tmp/d607ce46- 4541 -11e9-a8bd-0242ac110002/ 938 .txt 87829 python 6 0 /tmp/d607ce46- 4541 -11e9-a8bd-0242ac110002/ 771 .txt 87829 python 6 0 /tmp/d607ce46- 4541 -11e9-a8bd-0242ac110002/ 106 .txt |
原来,这时的路径已经变成了另一个目录。这说明,这些目录都是应用程序动态生成的,用完就删了。
结合前面的所有分析,我们基本可以判断,案例应用会动态生成一批文件,用来临时存储数据,用完就会删除它们。但不幸的是,正是这些文件读写,引发了 I/O 的性能瓶颈,导致整个处理过程非常慢。
小结:
- 首先,我们用 top、iostat,分析了系统的 CPU 和磁盘使用情况。我们发现了磁盘 I/O 瓶颈,也知道了这个瓶颈是案例应用导致的。
- 接着,我们试着照搬上一节案例的方法,用 strace 来观察进程的系统调用,不过这次很不走运,没找到任何 write 系统调用。
- 于是,我们又用了新的工具,借助动态追踪工具包 bcc 中的 filetop 和 opensnoop ,找出了案例应用的问题,发现这个根源是大量读写临时文件。
3.当数据库出现性能问题时,该如何分析和定位它的瓶颈呢?
预先安装 docker、sysstat 、git、make 等工具,如 apt install docker.io sysstat make git
今天的案例需要两台虚拟机,其中一台作为案例分析的目标机器,运行 Flask 应用,它的 IP 地址是 172.16.109.245;另一台则是作为客户端,请求单词的热度。
案例总共由三个容器组成,包括一个 MySQL 数据库应用、一个商品搜索应用以及一个数据处理的应用。其中,商品搜索应用以 HTTP 的形式提供了一个接口:
- /:返回 Index Page;
- /db/insert/products/:插入指定数量的商品信息;
- /products/:查询指定商品的信息,并返回处理时间。
1.首先,我们在第一个虚拟机中执行下面命令,拉取本次案例所需脚本:
$ git clone https: //github.com/feiskyer/linux-perf-examples $ cd linux-perf-examples/mysql-slow |
2.接着,执行下面的命令,运行本次的目标应用。正常情况下,你应该可以看到下面的输出:
# 注意下面的随机字符串是容器 ID,每次运行均会不同,并且你不需要关注它,因为我们只会用到名字 $ make run docker run --name=mysql -itd -p 10000 : 80 -m 800m feisky/mysql: 5.6 WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap. 4156780da5be0b9026bcf27a3fa56abc15b8408e358fa327f472bcc5add4453f docker run --name=dataservice -itd --privileged feisky/mysql-dataservice f724d0816d7e47c0b2b1ff701e9a39239cb9b5ce70f597764c793b68131122bb docker run --name=app --net=container:mysql -itd feisky/mysql-slow 81d3392ba25bb8436f6151662a13ff6182b6bc6f2a559fc2e9d873cd07224ab6 |
3.然后,再运行 docker ps 命令,确认三个容器都处在运行(Up)状态:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 9a4e3c580963 feisky/mysql-slow "python /app.py" 42 seconds ago Up 36 seconds app 2a47aab18082 feisky/mysql-dataservice "python /dataservice…" 46 seconds ago Up 41 seconds dataservice 4c3ff7b24748 feisky/mysql: 5.6 "docker-entrypoint.s…" 47 seconds ago Up 46 seconds 3306 /tcp, 0.0 . 0.0 : 10000 -> 80 /tcp mysql |
MySQL 数据库的启动过程,需要做一些初始化工作,这通常需要花费几分钟时间。你可以运行 docker logs 命令,查看它的启动过程。
当你看到下面这个输出时,说明 MySQL 初始化完成,可以接收外部请求了:
$ docker logs -f mysql ... ... [Note] mysqld: ready for connections. Version: '5.6.42-log' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL) |
而商品搜索应用则是在 10000 端口监听。你可以按 Ctrl+C ,停止 docker logs 命令;然后,执行下面的命令,确认它也已经正常运行。如果一切正常,你会看到 Index Page 的输出:
$ curl http: //127.0.0.1:10000/ Index P |
4.接下来,运行 make init 命令,初始化数据库,并插入 10000 条商品信息。这个过程比较慢,比如在我的机器中,就花了十几分钟时间。耐心等待一段时间后,你会看到如下的输出:
$ make init docker exec -i mysql mysql -uroot -P3306 < tables.sql curl http: //127.0.0.1:10000/db/insert/products/10000 insert 10000 lines |
5.接着,我们切换到第二个虚拟机,访问一下商品搜索的接口,看看能不能找到想要的商品。执行如下的 curl 命令:
$ curl http: //10.150.89.199:10000/products/geektime Got data: () in 15.364538192749023 sec |
稍等一会儿,你会发现,这个接口返回的是空数据,而且处理时间超过 15 秒。这么慢的响应速度让人无法忍受,到底出了什么问题呢?
既然今天用了 MySQL,你估计会猜到是慢查询的问题。
不过别急,在具体分析前,为了避免在分析过程中客户端的请求结束,我们把 curl 命令放到一个循环里执行。同时,为了避免给系统过大压力,我们设置在每次查询后,都先等待 5 秒,然后再开始新的请求。
6.所以,你可以在终端二中,继续执行下面的命令:
$ while true ; do curl http: //10.150.89.199:10000/products/geektime; done |
接下来,重新回到终端一中,分析接口响应速度慢的原因。不过,重回终端一后,你会发现系统响应也明显变慢了,随便执行一个命令,都得停顿一会儿才能看到输出。
这跟上一节的现象很类似,看来,我们还是得观察一下系统的资源使用情况,比如 CPU、内存和磁盘 I/O 等的情况。
7.我们在终端一执行 top 命令,分析系统的 CPU 使用情况:
$ top top - 12 : 02 : 15 up 6 days, 8 : 05 , 1 user, load average: 0.66 , 0.72 , 0.59 Tasks: 137 total, 1 running, 81 sleeping, 0 stopped, 0 zombie %Cpu0 : 0.7 us, 1.3 sy, 0.0 ni, 35.9 id, 62.1 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu1 : 0.3 us, 0.7 sy, 0.0 ni, 84.7 id, 14.3 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 8169300 total, 7238472 free, 546132 used, 384696 buff/cache KiB Swap: 0 total, 0 free, 0 used. 7316952 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 27458 999 20 0 833852 57968 13176 S 1.7 0.7 0 : 12.40 mysqld 27617 root 20 0 24348 9216 4692 S 1.0 0.1 0 : 04.40 python 1549 root 20 0 236716 24568 9864 S 0.3 0.3 51 : 46.57 python3 22421 root 20 0 0 0 0 I 0.3 0.0 0 : 01.16 kworker/u |
观察 top 的输出,我们发现,两个 CPU 的 iowait 都比较高,特别是 CPU0,iowait 已经超过 60%。而具体到各个进程, CPU 使用率并不高,最高的也只有 1.7%。
8.既然 CPU 的嫌疑不大,那问题应该还是出在了 I/O 上。我们仍然在第一个终端,按下 Ctrl+C,停止 top 命令;然后,执行下面的 iostat 命令,看看有没有 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 ... sda 273.00 0.00 32568.00 0.00 0.00 0.00 0.00 0.00 7.90 0.00 1.16 119.30 0.00 3.56 97.20 |
iostat 的输出你应该非常熟悉。观察这个界面,我们发现,磁盘 sda 每秒的读数据为 32 MB, 而 I/O 使用率高达 97% ,接近饱和,这说明,磁盘 sda 的读取确实碰到了性能瓶颈。
9.那要怎么知道,这些 I/O 请求到底是哪些进程导致的呢?当然可以找我们的老朋友, pidstat。接下来,在终端一中,按下 Ctrl+C 停止 iostat 命令,然后运行下面的 pidstat 命令,观察进程的 I/O 情况:
# -d 选项表示展示进程的 I/O 情况 $ pidstat -d 1 Linux 4.18 . 0 - 16 -generic (ubuntu) 03 / 14 / 2019 _x86_64_ ( 2 CPU) 12 : 13 : 49 AM UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 12 : 13 : 50 AM 0 2164 0.00 3.88 0.00 0 dockerd 12 : 13 : 50 AM UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 12 : 13 : 51 AM 0 333 0.00 4.00 0.00 0 systemd-journal 12 : 13 : 51 AM 102 662 8.00 8.00 0.00 0 rsyslogd 12 : 13 : 51 AM 999 2646 421896.00 0.00 0.00 0 mysqld 12 : 13 : 51 AM 0 2800 4.00 4.00 0.00 0 python |
从 pidstat 的输出可以看到,PID 为 2646 的 mysqld 进程正在进行大量的读,而且读取速度是 42 MB/s,跟刚才 iostat 的发现一致。两个结果一对比,我们自然就找到了磁盘 I/O 瓶颈的根源,即 mysqld 进程。
不过,这事儿还没完。我们自然要怀疑一下,为什么 mysqld 会去读取大量的磁盘数据呢?按照前面猜测,我们提到过,这有可能是个慢查询问题。
要分析进程的数据读取,当然还要靠上一节用到过的 strace+ lsof 组合。
10.接下来,还是在虚拟机一中,执行 strace 命令,并且指定 mysqld 的进程号 2646。我们知道,MySQL 是一个多线程的数据库应用,为了不漏掉这1些线程的数据读取情况,你要记得在执行 stace 命令时,加上 -f 参数:
$ strace -f -p 2646 [pid 28014 ] read( 38 , "934EiwT363aak7VtqF1mHGa4LL4Dhbks" ..., 131072 ) = 131072 [pid 28014 ] read( 38 , "hSs7KBDepBqA6m4ce6i6iUfFTeG9Ot9z" ..., 20480 ) = 20480 [pid 28014 ] read( 38 , "NRhRjCSsLLBjTfdqiBRLvN9K6FRfqqLm" ..., 131072 ) = 131072 [pid 28014 ] read( 38 , "AKgsik4BilLb7y6OkwQUjjqGeCTQTaRl" ..., 24576 ) = 24576 [pid 28014 ] read( 38 , "hFMHx7FzUSqfFI22fQxWCpSnDmRjamaW" ..., 131072 ) = 131072 [pid 28014 ] read( 38 , "ajUzLmKqivcDJSkiw7QWf2ETLgvQIpfC" ..., 20480 ) = 20480 |
观察一会,你会发现,线程 28014 正在读取大量数据,且读取文件的描述符编号为 38。这儿的 38 又对应着哪个文件呢?我们可以执行下面的 11.lsof 命令,并且指定线程号 28014 ,具体查看这个可疑线程和可疑文件:
$ lsof -p 28014 |
奇怪的是,lsof 并没有给出任何输出。实际上,如果你查看 lsof 命令的返回值,就会发现,这个命令的执行失败了。
12.我们知道,在 SHELL 中,特殊标量 $? 表示上一条命令退出时的返回值。查看这个特殊标量,你会发现它的返回值是 1。可是别忘了,在 Linux 中,返回值为 0 ,才表示命令执行成功。返回值为 1,显然表明执行失败。
$ echo $? 1 |
回过头我们看,mysqld 的进程号是 27458,而 28014 只是它的一个线程。而且,如果你观察 一下 mysqld 进程的线程,你会发现,mysqld 其实1还有很多正在运行的其他线程:
# -t 表示显示线程,-a 表示显示命令行参数 $ pstree -a -p 27458 mysqld, 27458 --log_bin=on --sync_binlog= 1 ... ├─{mysqld}, 27922 ├─{mysqld}, 27923 └─{mysqld}, 28014 |
13.找到了原因,lsof 的问题就容易解决了。把线程号换成进程号,继续执行 lsof 命令:
$ lsof -p 27458 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ... mysqld 27458 999 38u REG 8 , 1 512440000 2601895 /var/lib/mysql/test/products.MYD |
这次我们得到了 lsof
的输出。从输出中可以看到, mysqld 进程确实打开了大量文件,而根据文件描述符(FD)的编号,我们知道,描述符为 38 的是一个路径为
/var/lib/mysql/test/products.MYD 的文件。这里注意, 38 后面的 u 表示, mysqld
以读写的方式访问文件。
看到这个文件,熟悉 MySQL 的:
- MYD 文件,是 MyISAM 引擎用来存储表数据的文件;
- 文件名就是数据表的名字;
- 而这个文件的父目录,也就是数据库的名字。
换句话说,这个文件告诉我们,mysqld 在读取数据库 test 中的 products 表。
14.实际上,你可以执行下面的命令,查看 mysqld 在管理数据库 test 时的存储文件。不过要注意,由于 MySQL 运行在容器中,你需要通过 docker exec 到容器中查看:
$ docker exec -it mysql ls /var/lib/mysql/test/ db.opt products.MYD products.MYI products.frm |
从这里你可以发现,/var/lib/mysql/test/ 目录中有四个文件,每个文件的作用分别是:
- MYD 文件用来存储表的数据;
- MYI 文件用来存储表的索引;
- frm 文件用来存储表的元信息(比如表结构);
- opt 文件则用来存储数据库的元信息(比如字符集、字符校验规则等)。
当然,看到这些,你可能还有一个疑问,那就是,这些文件到底是不是 mysqld 正在使用的数据库文件呢?有没有可能是不再使用的旧数据呢?其实,这个很容易确认,查一下 mysqld 配置的数据路径即可。
15.你可以在终端一中,继续执行下面的命令:
$ docker exec -i -t mysql mysql -e 'show global variables like "%datadir%";' +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | datadir | /var/lib/mysql/ | +---------------+-----------------+ |
这里可以看到,/var/lib/mysql/ 确实是 mysqld 正在使用的数据存储目录。刚才分析得出的数据库 test 和数据表 products ,都是正在使用。
注:其实 lsof 的结果已经可以确认,它们都是 mysqld 正在访问的文件。再查询 datadir ,只是想换一个思路,进一步确认一下。
16.既然已经找出了数据库和表,接下来要做的,就是弄清楚数据库中正在执行什么样的 SQL 了。我们继续在终端一中,运行下面的 docker exec 命令,进入 MySQL 的命令行界面:
$ docker exec -i -t mysql mysql ... Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> |
下一步你应该可以想到,那就是在 MySQL 命令行界面中,执行 show processlist 命令,来查看当前正在执行的 SQL 语句。
17.不过,为了保证 SQL 语句不截断,这里我们可以执行 show full processlist 命令。如果一切正常,你应该可以看到如下输出:
mysql> show full processlist; +----+------+-----------------+------+---------+------+--------------+-----------------------------------------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+------+-----------------+------+---------+------+--------------+-----------------------------------------------------+ | 27 | root | localhost | test | Query | 0 | init | show full processlist | | 28 | root | 127.0 . 0.1 : 42262 | test | Query | 1 | Sending data | select * from products where productName= 'geektime' | +----+------+-----------------+------+---------+------+--------------+-----------------------------------------------------+ 2 rows in set ( 0.00 sec) |
这个输出中,
- db 表示数据库的名字;
- Command 表示 SQL 类型;
- Time 表示执行时间;
- State 表示状态;
- 而 Info 则包含了完整的 SQL 语句。
多执行几次 show full processlist 命令,你可看到 select * from products where productName=‘geektime’ 这条 SQL 语句的执行时间比较长。
再回忆一下,案例开始时,我们在终端二查询的产品名称 http://192.168.0.10:10000/products/geektime,其中的 geektime 也符合这条查询语句的条件。
我们知道,MySQL 的慢查询问题,很可能是没有利用好索引导致的,那这条查询语句是不是这样呢?我们又该怎么确认,查询语句是否利用了索引呢?
18.其实,MySQL 内置的 explain 命令,就可以帮你解决这个问题。继续在 MySQL 终端中,运行下面的 explain 命令:
# 切换到 test 库 mysql> use test; # 执行 explain 命令 mysql> explain select * from products where productName= 'geektime' ; +----+-------------+----------+------+---------------+------+---------+------+-------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+-------+-------------+ | 1 | SIMPLE | products | ALL | NULL | NULL | NULL | NULL | 10000 | Using where | +----+-------------+----------+------+---------------+------+---------+------+-------+-------------+ 1 row in set ( 0.00 sec) |
观察这次的输出。这个界面中,有几个比较重要的字段需要你注意,我就以这个输出为例,分别解释一下:
- select_type 表示查询类型,而这里的 SIMPLE 表示此查询不包括 UNION 查询或者子查询;
- table 表示数据表的名字,这里是 products;
- type 表示查询类型,这里的 ALL 表示全表查询,但索引查询应该是 index 类型才对;
- possible_keys 表示可能选用的索引,这里是 NULL;
- key 表示确切会使用的索引,这里也是 NULL;
- rows 表示查询扫描的行数,这里是 10000。
根据这些信息,我们可以确定,这条查询语句压根儿没有使用索引,所以查询时,会扫描全表,并且扫描行数高达 10000 行。响应速度那么慢也就难怪了。
4.Redis响应严重延迟,该如何分析和定位?
今天的案例由 Python 应用 +Redis 两部分组成。其中,Python 应用是一个基于 Flask 的应用,它会利用 Redis ,来管理应用程序的缓存,并对外提供三个 HTTP 接口:
- /:返回 hello redis;
- /init/:插入指定数量的缓存数据,如果不指定数量,默认的是 5000 条;
- 缓存的键格式为 uuid:
- 缓存的值为 good、bad 或 normal 三者之一
- /get_cache/<type_name>:查询指定值的缓存数据,并返回处理时间。其中,type_name 参数只支持 good, bad 和 normal(也就是找出具有相同 value 的 key 列表)。
今天的案例需要两台虚拟机,其中一台用作案例分析的目标机器,运行 Flask 应用,它的 IP 地址是 172.16.109.245;而另一台作为客户端,请求缓存查询接口。
接下来,打开两个终端,分别 SSH 登录到这两台虚拟机中。
1.首先,我们在第一个终端中,执行下面的命令,运行本次案例要分析的目标应用。正常情况下,你应该可以看到下面的输出:
# 注意下面的随机字符串是容器 ID,每次运行均会不同,并且你不需要关注它 $ docker run --name=redis -itd -p 10000 : 80 feisky/redis-server ec41cb9e4dd5cb7079e1d9f72b7cee7de67278dbd3bd0956b4c0846bff211803 $ docker run --name=app --net=container:redis -itd feisky/redis-app 2c54eb252d0552448320d9155a2618b799a1e71d7289ec7277a61e72a9de5fd0 |
然后,再运行 docker ps 命令,确认两个容器都处于运行(Up)状态:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2c54eb252d05 feisky/redis-app "python /app.py" 48 seconds ago Up 47 seconds app ec41cb9e4dd5 feisky/redis-server "docker-entrypoint.s…" 49 seconds ago Up 48 seconds 6379 /tcp, 0.0 . 0.0 : 10000 -> 80 /tcp redis |
今天的应用在 10000 端口监听,所以你可以通过 http://172.16.109.245:10000 ,来访问前面提到的三个接口。
比如,我们切换到第二个虚拟机,使用 curl 工具,访问应用首页。如果你看到 hello redis 的输出,说明应用正常启动:
$ curl http: //10.150.89.199:10000/ hello redis |
接下来,继续在虚拟机二中,执行下面的 curl 命令,来调用应用的 /init 接口,初始化 Redis 缓存,并且插入 5000 条缓存信息。这个过程比较慢,比如我的机器就花了十几分钟时间。耐心等一会儿后,你会看到下面这行输出:
# 案例插入 5000 条数据,在实践时可以根据磁盘的类型适当调整,比如使用 SSD 时可以调大,而 HDD 可以适当调小 #查看我的ubuntu虚拟机是HDD的,我调小到 3000 条 root @ubuntu :/home/xhong# curl http: //10.150.89.199:10000/init/5000 { "elapsed_seconds" : 29.00375247001648 , "keys_initialized" : 3000 } |
继续执行下一个命令,访问应用的缓存查询接口。如果一切正常,你会看到如下输出:
$ curl http: //10.150.89.199:10000/get_cache { "count" : 1677 , "data" :[ "d97662fa-06ac-11e9-92c7-0242ac110002" ,...], "elapsed_seconds" : 10.545469760894775 , "type" : "good" } |
我们看到,这个接口调用居然要花 9 秒!这么长的响应时间,显然不能满足实际的应用需求。
到底出了什么问题呢?我们还是要用前面学过的性能工具和原理,来找到这个瓶颈。
不过别急,同样为了避免分析过程中客户端的请求结束,在进行性能分析前,我们先要把 curl 命令放到一个循环里来执行。你可以在虚拟机二中,继续执行下面的命令:
$ while true ; do curl http: //10.150.89.199:10000/get_cache; done |
接下来,再重新回到虚拟机一,查找接口响应慢的“病因”。
最近几个案例的现象都是响应很慢,这种情况下,我们自然先会怀疑,是不是系统资源出现了瓶颈。所以,先观察 CPU、内存和磁盘 I/O 等的使用情况肯定不会错。
我们先在虚拟机一中执行 top 命令,分析系统的 CPU 使用情况:
观察 top 的输出可以发现,CPU0 的 iowait 比较高,已经达到了 82%;而各个进程的 CPU 使用率都不太高,最高的 python 和 redis-server ,也分别只有 5.3% 和 3%。再看内存,总内存 8GB,剩余内存还有 7GB 多,显然内存也没啥问题。
综合 top 的信息,最有嫌疑的就是 iowait。所以,接下来还是要继续分析,是不是 I/O 问题。
还在第一个虚拟机中,先按下 Ctrl+C,停止 top 命令;然后,执行下面的 iostat 命令,查看有没有 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 loop1 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 loop2 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 loop3 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 loop4 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 loop5 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 loop6 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 loop7 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 327.00 0.00 1776.00 0.00 117.00 0.00 26.35 0.00 1.70 0.86 0.00 5.43 2.64 86.40 loop8 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 |
观察 iostat 的输出,我们发现,磁盘 sda 每秒的写数据(wkB/s)为 2.5MB,I/O 使用率(%util)是 86.4%,可以看到I/O操作出现了瓶颈。
要知道 I/O 请求来自哪些进程,还是要靠我们的老朋友 pidstat。在虚拟机一中运行下面的 pidstat 命令,观察进程的 I/O 情况:
$ pidstat -d 1 Average: UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command Average: 0 296 0.00 3.98 0.00 4 jbd2/sda1- 8 Average: 1000 85313 1.00 0.00 0.00 0 gnome-terminal- Average: 100 90829 0.00 424.88 0.00 82 redis-server |
从 pidstat 的输出,我们看到,I/O 最多的进程是 PID 为 90829 的 redis-server,并且它也刚好是在写磁盘。这说明,确实是 redis-server 在进行磁盘写。
当然,光找到读写磁盘的进程还不够,我们还要再用 strace+lsof 组合,看看 redis-server 到底在写什么。
接下来,还是在第一个虚拟机中,执行 strace 命令,并且指定 redis-server 的进程号 90829:
# -f 表示跟踪子进程和子线程,-T 表示显示系统调用的时长,-tt 表示显示跟踪时间 $ strace -f -T -tt -p 90829 [pid 90829 ] 19 : 22 : 41.294318 write( 8 , "$6\r\nnormal\r\n" , 12 ) = 12 < 0.000188 > [pid 90829 ] 19 : 22 : 41.294582 epoll_pwait( 5 , [{EPOLLIN, {u32= 8 , u64= 8 }}], 10128 , 25 , NULL, 8 ) = 1 < 0.000032 > [pid 90829 ] 19 : 22 : 41.294758 read( 8 , "*2\r\n$3\r\nGET\r\n$41\r\nuuid:0a83bcb4-" ..., 16384 ) = 61 < 0.000045 > [pid 90829 ] 19 : 22 : 41.294935 read( 3 , 0x7ffeffd94fc7 , 1 ) = - 1 EAGAIN (Resource temporarily unavailable) < 0.000051 > [pid 90829 ] 19 : 22 : 41.295080 write( 8 , "$3\r\nbad\r\n" , 9 ) = 9 < 0.000371 > [pid 90829 ] 19 : 22 : 41.295623 epoll_pwait( 5 , [{EPOLLIN, {u32= 8 , u64= 8 }}], 10128 , 24 , NULL, 8 ) = 1 < 0.000107 > [pid 90829 ] 19 : 22 : 41.295859 read( 8 , "*2\r\n$3\r\nGET\r\n$41\r\nuuid:0bf30c08-" ..., 16384 ) = 61 < 0.000110 > [pid 90829 ] 19 : 22 : 41.296128 read( 3 , 0x7ffeffd94fc7 , 1 ) = - 1 EAGAIN (Resource temporarily unavailable) < 0.000038 > [pid 90829 ] 19 : 22 : 41.296240 write( 8 , "$4\r\ngood\r\n" , 10 ) = 10 < 0.000380 > [pid 90829 ] 19 : 22 : 41.296705 epoll_pwait( 5 , [{EPOLLIN, {u32= 8 , u64= 8 }}], 10128 , 23 , NULL, 8 ) = 1 < 0.000034 > [pid 90829 ] 19 : 22 : 41.296833 read( 8 , "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n0bf" ..., 16384 ) = 67 < 0.000032 > [pid 90829 ] 19 : 22 : 41.297216 read( 3 , 0x7ffeffd94fc7 , 1 ) = - 1 EAGAIN (Resource temporarily unavailable) < 0.000084 > [pid 90829 ] 19 : 22 : 41.297459 write( 7 , "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n0bf" ..., 67 ) = 67 < 0.000114 > [pid 90829 ] 19 : 22 : 41.297714 fdatasync( 7 ) = 0 < 0.003149 > [pid 90829 ] 19 : 22 : 41.301050 write( 8 , ":1\r\n" , 4 ) = 4 < 0.000380 > |
事实上,从系统调用来看, epoll_pwait、read、write、fdatasync 这些系统调用都比较频繁。那么,刚才观察到的写磁盘,应该就是 write 或者 fdatasync 导致的了。
注:fdatasync(),linux系统调用。用来刷新数据到磁盘。
接着再来运行 lsof 命令,找出这些系统调用的操作对象:
$ lsof -p 9085 root @ubuntu :/home/xhong# lsof -p 90829 lsof: WARNING: can't stat() fuse.gvfsd-fuse file system /run/user/ 1000 /gvfs Output information may be incomplete. COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME redis-ser 90829 systemd-network cwd DIR 8 , 1 4096 433582 /data redis-ser 90829 systemd-network rtd DIR 0 , 51 4096 433551 / redis-ser 90829 systemd-network txt REG 0 , 51 8191592 433522 /usr/local/bin/redis-server redis-ser 90829 systemd-network mem REG 8 , 1 433522 /usr/local/bin/redis-server (stat: No such file or directory) redis-ser 90829 systemd-network mem REG 8 , 1 1076110 /etc/localtime (path inode= 672428 ) redis-ser 90829 systemd-network mem REG 8 , 1 1191269 /lib/ld-musl-x86_64.so. 1 (stat: No such file or directory) redis-ser 90829 systemd-network 0u CHR 136 , 0 0t0 3 /dev/pts/ 0 redis-ser 90829 systemd-network 1u CHR 136 , 0 0t0 3 /dev/pts/ 0 redis-ser 90829 systemd-network 2u CHR 136 , 0 0t0 3 /dev/pts/ 0 redis-ser 90829 systemd-network 3r FIFO 0 , 12 0t0 2327083 pipe redis-ser 90829 systemd-network 4w FIFO 0 , 12 0t0 2327083 pipe redis-ser 90829 systemd-network 5u a_inode 0 , 13 0 11836 [eventpoll] redis-ser 90829 systemd-network 6u sock 0 , 9 0t0 2327085 protocol: TCP redis-ser 90829 systemd-network 7w REG 8 , 1 9662394 433591 /data/appendonly.aof redis-ser 90829 systemd-network 8u sock 0 , 9 0t0 2342499 protocol: TCP |
现在你会发现,描述符编号为 3 的是一个 pipe 管道,5 号是 eventpoll,7 号是一个普通文件,而 8 号是一个 TCP socket。
结合磁盘写的现象,我们知道,只有 7 号普通文件才会产生磁盘写,而它操作的文件路径是 /data/appendonly.aof,相应的系统调用包括 write 和 fdatasync。
如果你对 Redis 的持久化配置比较熟,看到这个文件路径以及 fdatasync 的系统调用,你应该能想到,这对应着正是 Redis 持久化配置中的 appendonly 和 appendfsync 选项。很可能是因为它们的配置不合理,导致磁盘写比较多。
注:redis持久化请看:https://blog.csdn.net/ljl890705/article/details/51039015
接下来就验证一下这个猜测,我们可以通过 Redis 的命令行工具,查询这两个选项的配置。
继续在虚拟机一中,运行下面的命令,查询 appendonly 和 appendfsync 的配置:
$ docker exec -it redis redis-cli config get 'append*' 1 ) "appendfsync" 2 ) "always" 3 ) "appendonly" 4 ) "yes |
从这个结果你可以发现,appendfsync 配置的是 always,而 appendonly 配置的是 yes。这两个选项的详细含义,你可以从 Redis Persistence 的文档中查到,这里做一下简单介绍。
Redis 提供了两种数据持久化的方式,分别是快照和追加文件。
快照方式:
会按照指定的时间间隔,生成数据的快照,并且保存到磁盘文件中。为了避免阻塞主进程,Redis 还会 fork 出一个子进程,来负责快照的保存。这种方式的性能好,无论是备份还是恢复,都比追加文件好很多。
不过,它的缺点也很明显。在数据量大时,fork 子进程需要用到比较大的内存,保存数据也很耗时。所以,你需要设置一个比较长的时间间隔来应对,比如至少 5 分钟。这样,如果发生故障,你丢失的就是几分钟的数据。
追加文件:
则是用在文件末尾追加记录的方式,对 Redis 写入的数据,依次进行持久化,所以它的持久化也更安全。
此外,它还提供了一个用 appendfsync 选项设置 fsync 的策略,确保写入的数据都落到磁盘中,具体选项包括 always、everysec、no 等。
- always 表示,每个操作都会执行一次 fsync,是最为安全的方式;
- everysec 表示,每秒钟调用一次 fsync ,这样可以保证即使是最坏情况下,也只丢失 1 秒的数据;
- 而 no 表示交给操作系统来处理。
回忆一下我们刚刚看到的配置,appendfsync 配置的是 always,意味着每次写数据时,都会调用一次 fsync,从而造成比较大的磁盘 I/O 压力。
当然,你还可以用 strace ,观察这个系统调用的执行情况。比如通过 -e 选项指定 fdatasync 后,你就会得到下面的结果:
$ strace -f -p 9085 -T -tt -e fdatasync strace: Process 9085 attached with 4 threads [pid 9085 ] 14 : 22 : 52.013547 fdatasync( 7 ) = 0 < 0.007112 > [pid 9085 ] 14 : 22 : 52.022467 fdatasync( 7 ) = 0 < 0.008572 > [pid 9085 ] 14 : 22 : 52.032223 fdatasync( 7 ) = 0 < 0.006769 > ... [pid 9085 ] 14 : 22 : 52.139629 fdatasync( 7 ) = 0 < 0.008183 > |
从这里你可以看到,每隔 10ms 左右,就会有一次 fdatasync 调用,并且每次调用本身也要消耗 7~8ms。
不管哪种方式,都可以验证我们的猜想,配置确实不合理。这样,我们就找出了 Redis 正在进行写入的文件,也知道了产生大量 I/O 的原因。
不过,回到最初的疑问,为什么查询时会有磁盘写呢?按理来说不应该只有数据的读取吗?这就需要我们再来审查一下 strace -f -T -tt -p 90829 的结果。
read( 8 , "*2\r\n$3\r\nGET\r\n$41\r\nuuid:53522908-" ..., 16384 ) write( 8 , "$4\r\ngood\r\n" , 10 ) read( 8 , "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535" ..., 16384 ) write( 7 , "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535" ..., 67 ) write( 8 , ":1\r\n" , 4 ) |
细心的你应该记得,根据 lsof 的分析,文件描述符编号为 7 的是一个普通文件 /data/appendonly.aof,而编号为 8 的是 TCP socket。而观察上面的内容,8 号对应的 TCP 读写,是一个标准的“请求 - 响应”格式,即:
- 从 socket 读取 GET uuid:53522908-… 后,响应 good;
- 再从 socket 读取 SADD good 535… 后,响应 1。
对 Redis 来说,SADD 是一个写操作,所以 Redis 还会把它保存到用于持久化的 appendonly.aof 文件中。
观察更多的 strace 结果,你会发现,每当 GET 返回 good 时,随后都会有一个 SADD 操作,这也就导致了,明明是查询接口,Redis 却有大量的磁盘写。
到这里,我们就找出了 Redis 写磁盘的原因。不过,在下最终结论前,我们还是要确认一下,8 号 TCP socket 对应的 Redis 客户端,到底是不是我们的案例应用
我们可以给 lsof 命令加上 -i 选项,找出 TCP socket 对应的 TCP 连接信息。不过,由于 Redis 和 Python 应用都在容器中运行,我们需要进入容器的网络命名空间内部,才能看到完整的 TCP 连接。
注意:下面的命令用到的 nsenter 工具,可以进入容器命名空间。如果你的系统没有安装,请运行下面命令安装 nsenter: docker run --rm -v /usr/local/bin:/target jpetazzo/nsenter
还是在虚拟机一中,运行下面的命令:
# 由于这两个容器共享同一个网络命名空间,所以我们只需要进入 app 的网络命名空间即可 $ PID=$(docker inspect --format {{.State.Pid}} app) # -i 表示显示网络套接字信息 $ nsenter --target $PID --net -- lsof -i COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME redis-ser 9085 systemd-network 6u IPv4 15447972 0t0 TCP localhost: 6379 (LISTEN) redis-ser 9085 systemd-network 8u IPv4 15448709 0t0 TCP localhost: 6379 ->localhost: 32996 (ESTABLISHED) python 9181 root 3u IPv4 15448677 0t0 TCP *:http (LISTEN) python 9181 root 5u IPv4 15449632 0t0 TCP localhost: 32996 ->localhost: 6379 (ESTABLISHED) |
这次我们可以看到,redis-server
的 8 号文件描述符,对应 TCP 连接 localhost:6379->localhost:32996。其中,
localhost:6379 是 redis-server 自己的监听端口,自然 localhost:32996 就是 redis
的客户端。再观察最后一行,localhost:32996 对应的,正是我们的 Python 应用程序(进程号为 9181)。
历经各种波折,我们总算找出了 Redis 响应延迟的潜在原因。总结一下,我们找到两个问题。
完了在虚拟机一中结束案例:
root @ubuntu :/home/xhong# docker rm -f app redis |
小结:
我们先用 top、iostat ,分析了系统的 CPU 、内存和磁盘使用情况,不过却发现,系统资源并没有出现瓶颈。这个时候想要进一步分析的话,该从哪个方向着手呢?
通过今天的案例你会发现,为了进一步分析,就需要你对系统和应用程序的工作原理有一定的了解。
比如,今天的案例中,虽然磁盘 I/O 并没有出现瓶颈,但从 Redis 的原理来说,查询缓存时不应该出现大量的磁盘 I/O 写操作。
顺着这个思路,我们继续借助 pidstat、strace、lsof、nsenter 等一系列的工具,找出了两个潜在问题,一个是 Redis 的不合理配置,另一个是 Python 应用对 Redis 的滥用。找到瓶颈后,相应的优化工作自然就比较轻松了。