上一节,我们研究了一个狂打日志引发 I/O 性能问题的案例,先来简单回顾一下。
日志,是了解应用程序内部运行情况,最常用也是最有效的工具。日志一般会分为调试、信息、警告、错误等多个不同级别
通常,生产环境只用开启警告级别的日志,这一般不会导致 I/O 问题。但在偶尔排查问题时,可能需要我们开启调试日志。调试结束后,很可能忘了把日志级别调回去。这时,大量的调试日志就可能会引发 I/O 性能问题。
你可以用 iostat ,确认是否有 I/O 性能瓶颈。再用 strace 和 lsof ,来定位应用程序以及它正在写入的日志文件路径。最后通过应用程序的接口调整日志级别,完美解决 I/O 问题。
不过,如果应用程序没有动态调整日志级别的功能,你还需要修改应用配置并重启应用,以便让配置生效。
今天,我们再来看一个新的案例。这次案例是一个基于 Python Flask 框架的 Web 应用,它提供了一个查询单词热度的 API,但是 API 的响应速度并不让人满意。
非常感谢携程系统研发部资深后端工程师董国星,帮助提供了今天的案例。

案例准备

本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
  • 机器配置:2 CPU,8GB 内存
  • 预先安装 docker、sysstat 等工具,如 apt install docker.io sysstat
为了方便你运行今天的案例,我把它打包成了一个 Docker 镜像。这样,你就只需要运行 Docker 命令就可以启动它。
今天的案例需要两台虚拟机,其中一台是案例分析的目标机器,运行 Flask 应用,它的 IP 地址是 192.168.0.10;而另一台作为客户端,请求单词的热度。我画了一张图表示它们的关系,如下所示:
接下来,打开两个终端,分别 SSH 登录到这两台虚拟机中,并在第一台虚拟机中,安装上述工具。
跟以前一样,案例中所有命令都默认以 root 用户运行,如果你是用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
温馨提示:案例中 Python 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,我依旧建议,操作之前别看源码,避免先入为主,而要把它当成一个黑盒来分析。这样,你可以更好把握,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用,以及瓶颈在应用中大概的位置。

案例分析

首先,我们在第一个终端中执行下面的命令,运行本次案例要分析的目标应用:
docker run --name=app -p 10000:80 -itd feisky/word-pop

然后,在第二个终端中运行 curl 命令,访问 http://192.168.0.10:1000/,确认案例正常启动。你应该可以在 curl 的输出界面里,看到一个 hello world 的输出:
curl http://192.168.0.10:10000/
hello world

接下来,在第二个终端中,访问案例应用的单词热度接口,也就是 http://192.168.0.10:1000/popularity/word。
curl http://192.168.0.10:1000/popularity/word

稍等一会儿,你会发现,这个接口居然这么长时间都没响应,究竟是怎么回事呢?我们先回到终端一来分析一下。
我们试试在第一个终端里,随便执行一个命令,比如执行 df 命令,查看一下文件系统的使用情况。奇怪的是,这么简单的命令,居然也要等好久才有输出。
df
Filesystem     1K-blocks    Used Available Use% Mounted on
udev             4073376       0   4073376   0% /dev
tmpfs             816932    1188    815744   1% /run
/dev/sda1       30308240 8713640  21578216  29% /

通过 df 我们知道,系统还有足够多的磁盘空间。那为什么响应会变慢呢?看来还是得观察一下,系统的资源使用情况,像是 CPU、内存和磁盘 I/O 等的具体使用情况。
这里的思路其实跟上一个案例比较类似,我们可以先用 top 来观察 CPU 和内存的使用情况,然后再用 iostat 来观察磁盘的 I/O 情况。
为了避免分析过程中 curl 请求突然结束,我们回到终端二,按 Ctrl+C 停止刚才的应用程序;然后,把 curl 命令放到一个循环里执行;这次我们还要加一个 time 命令,观察每次的执行时间:
while true; do time curl http://192.168.0.10:10000/popularity/word; sleep 1; done

继续回到终端一来分析性能。我们在终端一中运行 top 命令,观察 CPU 和内存的使用情况:
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

观察 top 的输出可以发现,两个 CPU 的 iowait 都非常高。特别是 CPU0, iowait 已经高达 94 %,而剩余内存还有 3GB,看起来也是充足的。
再往下看,进程部分有一个 python 进程的 CPU 使用率稍微有点高,达到了 14%。虽然 14% 并不能成为性能瓶颈,不过有点嫌疑——可能跟 iowait 的升高有关。
那这个 PID 号为 12280 的 python 进程,到底是不是我们的案例应用呢?
我们在第一个终端中,按下 Ctrl+C,停止 top 命令;然后执行下面的 ps 命令,查找案例应用 app.py 的 PID 号:
ps aux | grep app.py
root     12222  0.4  0.2  96064 23452 pts/0    Ss+  14:37   0:00 python /app.py
root     12280 13.9  0.3 102424 27904 pts/0    Sl+  14:37   0:09 /usr/local/bin/python /app.py

从 ps 的输出,你可以看到,这个 CPU 使用率较高的进程,正是我们的案例应用。不过先别着急分析 CPU 问题,毕竟 iowait 已经高达 94%, I/O 问题才是我们首要解决的。

iostat

接下来,我们在终端一中,运行下面的 iostat 命令,其中:
  • -d 选项是指显示出 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

再次看到 iostat 的输出,你还记得这个界面中的性能指标含义吗?先自己回忆一下,如果实在想不起来,一定要先查看上节内容,或者用 man iostat 查明白。
明白了指标含义,再来具体观察 iostat 的输出。你可以发现,磁盘 sda 的 I/O 使用率已经达到 98% ,接近饱和了。而且,写请求的响应时间高达 18 秒,每秒的写数据为 32 MB,显然写磁盘碰到了瓶颈。
那要怎么知道,这些 I/O 请求到底是哪些进程导致的呢?我想,你已经还记得上一节我们用到的 pidstat。
在终端一中,运行下面的 pidstat 命令,观察进程的 I/O 情况:
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

从 pidstat 的输出,我们再次看到了 PID 号为 12280 的结果。这说明,正是案例应用引发 I/O 的性能瓶颈。
走到这一步,你估计觉得,接下来就很简单了,上一个案例不刚刚学过吗?无非就是,先用 strace 确认它是不是在写文件,再用 lsof 找出文件描述符对应的文件即可。
到底是不是这样呢?我们不妨来试试。还是在终端一中,执行下面的 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

从 strace 中,你可以看到大量的 stat 系统调用,并且大都为 python 的文件,但是,请注意,这里并没有任何 write 系统调用。
由于 strace 的输出比较多,我们可以用 grep ,来过滤一下 write,比如:
strace -p 12280 2>&1 | grep write

遗憾的是,这里仍然没有任何输出。
难道此时已经没有性能问题了吗?重新执行刚才的 top 和 iostat 命令,你会不幸地发现,性能问题仍然存在。
我们只好综合 strace、pidstat 和 iostat 这三个结果来分析了。很明显,你应该发现了这里的矛盾:iostat 已经证明磁盘 I/O 有性能瓶颈,而 pidstat 也证明了,这个瓶颈是由 12280 号进程导致的,但 strace 跟踪这个进程,却没有找到任何 write 系统调用。
这就奇怪了。难道因为案例使用的编程语言是 Python ,而 Python 是解释型的,所以找不到?还是说,因为案例运行在 Docker 中呢?这里留个悬念,你自己想想。
文件写,明明应该有相应的 write 系统调用,但用现有工具却找不到痕迹,这时就该想想换工具的问题了。怎样才能知道哪里在写文件呢?

filetop

这里我给你介绍一个新工具, filetop。它是 bcc 软件包的一部分,基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,主要跟踪内核中文件的读写情况,并输出线程 ID(TID)、读写大小、读写类型以及文件名称。
eBPF 的工作原理,你暂时不用深究,后面内容我们会逐渐接触到,先会使用就可以了。
至于老朋友 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 这个目录中。接下来我们就用这个工具,观察一下文件的读写情况。
首先,在终端一中运行下面的命令:
# 切换到工具目录
cd /usr/share/bcc/tools # -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

你会看到,filetop 输出了 8 列内容,分别是线程 ID、线程命令行、读写次数、读写的大小(单位 KB)、文件类型以及读写的文件名称。
这些内容里,你可能会看到很多动态链接库,不过这不是我们的重点,暂且忽略即可。我们的重点,是一个 python 应用,所以要特别关注 python 相关的内容。
多观察一会儿,你就会发现,每隔一段时间,线程号为 514 的 python 应用就会先写入大量的 txt 文件,再大量地读。
线程号为 514 的线程,属于哪个进程呢?我们可以用 ps 命令查看。先在终端一中,按下 Ctrl+C ,停止 filetop ;然后,运行下面的 ps 命令。这个输出的第二列内容,就是我们想知道的进程号:
ps -efT | grep 514
root     12280  514 14626 33 14:47 pts/0    00:00:05 /usr/local/bin/python /app.py

我们看到,这个线程正是案例应用 12280 的线程。终于可以先松一口气,不过还没完,filetop 只给出了文件名称,却没有文件路径,还得继续找啊。

opensnoop

我再介绍一个好用的工具,opensnoop 。它同属于 bcc 软件包,可以动态跟踪内核中的 open 系统调用。这样,我们就可以找出这些 txt 文件的路径。
接下来,在终端一中,运行下面的 opensnoop 命令:
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

这次,通过 opensnoop 的输出,你可以看到,这些 txt 路径位于 /tmp 目录下。你还能看到,它打开的文件数量,按照数字编号,从 0.txt 依次增大到 999.txt,这可远多于前面用 filetop 看到的数量。
综合 filetop 和 opensnoop ,我们就可以进一步分析了。我们可以大胆猜测,案例应用在写入 1000 个 txt 文件后,又把这些内容读到内存中进行处理。我们来检查一下,这个目录中是不是真的有 1000 个文件:
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 再观察一会儿:
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.py,
@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 的性能问题。
你可以检验一下,在终端二中分别访问 http://192.168.0.10:10000/popularity/word 和 http://192.168.0.10:10000/popular/word ,对比前后的效果:
time curl http://192.168.0.10:10000/popularity/word
{ "popularity": 0.0, "word": "word"
}
real    2m43.172s
user    0m0.004s
sys    0m0.007s
time curl http://192.168.0.10:10000/popular/word
{"popularity": 0.0,"word": "word"
}real    0m8.810s
user    0m0.010s
sys    0m0.000s

新的接口只要 8 秒就可以返回,明显比一开始的 3 分钟好很多。
当然,这只是优化的第一步,并且方法也不算完善,还可以做进一步的优化。不过,在实际系统中,我们大都是类似的做法,先用最简单的方法,尽早解决线上问题,然后再继续思考更好的优化方法。

小结

今天,我们分析了一个响应过慢的单词热度案例。
首先,我们用 top、iostat,分析了系统的 CPU 和磁盘使用情况。我们发现了磁盘 I/O 瓶颈,也知道了这个瓶颈是案例应用导致的。
接着,我们试着照搬上一节案例的方法,用 strace 来观察进程的系统调用,不过这次很不走运,没找到任何 write 系统调用。
于是,我们又用了新的工具,借助动态追踪工具包 bcc 中的 filetop 和 opensnoop ,找出了案例应用的问题,发现这个根源是大量读写临时文件。
找出问题后,优化方法就相对比较简单了。如果内存充足时,最简单的方法,就是把数据都放在速度更快的内存中,这样就没有磁盘 I/O 的瓶颈了。当然,再进一步,你可以还可以利用 Trie 树等各种算法,进一步优化单词处理的效率。
思考
最后,给你留一个思考题,也是我在文章中提到过的,让你思考的问题。
今天的案例中,iostat 已经证明,磁盘 I/O 出现了性能瓶颈, pidstat 也证明了这个瓶颈是由 12280 号进程导致的。但是,strace 跟踪这个进程,却没有发现任何 write 系统调用。
这究竟是怎么回事?难道是因为案例使用的编程语言 Python 本身是解释型?还是说,因为案例运行在 Docker 中呢?
这里我小小提示一下。当你发现性能工具的输出无法解释时,最好返回去想想,是不是分析中漏掉了什么线索,或者去翻翻工具手册,看看是不是某些默认选项导致的。
写文件是由子线程执行的,所以直接strace跟踪进程没有看到write系统调用,可以通过pstree查看进程的线程信息,再用strace跟踪。或者,通过strace -fp pid 跟踪所有线程。
作者回复:

27 | 案例篇:为什么我的磁盘I/O延迟很高?相关推荐

  1. 这篇 LaTeX 简单介绍的文章艺术含量很高哒!

    目录 一. 工欲利其事,必先利其器 二. 言语互不相通,论文无法完工 三. 标题写起来 ! 四. 报告不能没有目录,就像西方不能没有耶路撒冷 五. 图片 - 文章的灵魂 六. 可不敢抄袭啊,参考文献 ...

  2. Linux 查看磁盘IO并找出占用IO读写很高的进程

    根据磁盘IO告警,找到占用磁盘IO (util)读写很高的进程. 背景-线上告警 线上一台服务器告警,磁盘利用率 disk.util > 90,并持续告警. 登录该服务器后通过 iostat - ...

  3. mysql(基础案例篇)

    mysql(基础案例篇) 1.CRUD1 (创建Create.检索Retrieve.更改Update.删除Delete) 查:select * from 表名 where 条件 删:delect fr ...

  4. Shader案例篇二《镜子2》

    Shader案例篇二<镜子2> 二.Unity中制作原理 1.简单说明:其实这个原理就是用一个摄像机去拍镜子上面的物体将得到的图像投影给Plane,最后主摄像机就能看到Plane上物体的镜 ...

  5. mysql实例和数据的安全_数据库安全--案例篇

    目的 由于<数据库安全浅析>一文中,主要介绍了数据库安全方面的一些基本安全策略,缺少了必要的案例分析和处理过程.为了进一步丰富数据库安全的内容,特撰文案例篇,主要介绍数据库安全上遇到的案例 ...

  6. asp.net多图片上传案例_会计小明的故事-成本核算案例篇

    因涉及成本核算案例篇,所需要的图表比较多,但是知乎不同于微信可以直接将文档图表复制过来,知乎专栏文章,所有图表必须先截图,然后以图片形式展示.但是成本核算案例图表实在是太多,一一截图,一则影响整体观感 ...

  7. 08 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(下)

    上一节,我给你讲了 Linux 进程状态的含义,以及不可中断进程和僵尸进程产生的原因,我们先来简单复习下. 使用 ps 或者 top 可以查看进程的状态,这些状态包括运行.空闲.不可中断睡眠.可中断睡 ...

  8. 42 | 案例篇:如何优化 NAT 性能?(下)

    上一节,我们学习了 NAT 的原理,明白了如何在 Linux 中管理 NAT 规则.先来简单复习一下. NAT 技术能够重写 IP 数据包的源 IP 或目的 IP,所以普遍用来解决公网 IP 地址短缺 ...

  9. 41 | 案例篇:如何优化 NAT 性能?(上)

    上一节,探究了网络延迟增大问题的分析方法,并通过一个案例,掌握了如何用 hping3.tcpdump.Wireshark.strace 等工具,来排查和定位问题的根源. 简单回顾一下,网络延迟是最核心 ...

最新文章

  1. Reactor实例解析
  2. a=a+b与a+=b有什么区别吗?
  3. 朴素贝叶斯Naïve Bayes分类算法在Hadoop上的实现
  4. 你所需要的java基础篇和提升篇大总结
  5. 集成CCFlow工作流与GPM的办公系统驰骋CCOA介绍(二)
  6. 说白了 枚举是一个对象中属性的参数值
  7. 零基础学sql要多久_零基础如何学习游戏3D建模,要学些什么内容?学多久?
  8. tcp 裸流 发送 html,ffmpeg 命令学习
  9. java 解析xml saxreader_Java中使用DOM和SAX解析XML文件的方法示例
  10. vue-pdf安装之后,运行报错can not resolve ‘pdfjs-dist/es5/web/pdf_viewer‘
  11. azw3怎么在Mac电脑上打开?
  12. LeetCode347. 前 K 个高频元素(含详细解析JAVA实现)
  13. Mac python 安装信息安全,Pycrypto 出现,C compiler cannot create executablesC编辑器不能创建可执行文件
  14. [洛谷P3939]数颜色
  15. 第一个安卓应用小程序--浅浅仿照微信发现界面
  16. 图形学基础之透视校正插值
  17. SendMessage函数完全使用手册 (解读WM_)
  18. 怎么看Mac电脑的序列号,Mac序列号是多少
  19. winform做的单机登录界面和账号注册界面
  20. 【思维导图训练1】--思维导图的基本概述

热门文章

  1. python综合管理系统_学生综合信息管理系统
  2. integer加1_利用Abaqus UEL开发自定义单元1
  3. python实现数据库事务回滚_使用Python脚本实现MySQL误操作的快速回滚
  4. apache相对路径 php,php简单实现相对路径转绝对路径-PHP问题
  5. php+数学计算公式,PHP数学计算函数总结
  6. ci mysql操作_CI框架数据库各类操作
  7. javascript控制html高,Javascript可以控制css吗?
  8. java system_java System类
  9. 分享几段祖传的Python代码,拿来直接使用!
  10. 人脸识别的时候,一定要穿上衣服!否则 ...