一、案例环境描述

1、环境准备

2CPU,4GB内存

预先安装docker sysstat工具

2、温馨提示

案例中 Python 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,
我依旧建议,操作之前别看源码,避免先入为主,要把它当成一个黑盒来分析。这样 你可以更好把握住,怎么从系统的资源使用问题出发,分析出瓶颈
所在的应用,以及瓶颈在应用中大概的位置

3、测试环境准备

1、运行目标应用

docker run --name=app -p 10000:80 -itd feisky/word-pop 

2、确认应用正常启动

[root@luoahong ~]# ps aux | grep app.py
root 10130 0.0 0.5 95700 23392 pts/0 Ss+ 10:29 0:00 python /app.py
root 10167 30.8 0.7 104924 30372 pts/0 Sl+ 10:29 4:32 /usr/local/bin/python /app.py
root 10256 0.0 0.0 112716 2288 pts/1 S+ 10:44 0:00 grep --color=auto app.py

二、故障现象

1、发现故障

1、接下来,在第二个终端中,访问案例应用的单词热度接口

[root@luoahong ~]# curl http://192.168.118.115:10000
hello world[root@luoahong ~]# curl http://192.168.118.115:10000/popularity/word
{
"popularity": 0.0,
"word": "word"
}

稍等一会儿,你会发现,这个接口居然这么长时间都没响应,究竟是怎么回事呢?我们先回到终端一来分析一下。

2、我们试试在第一个终端里,随便执行一个命令,居然也要等好久才能输出

[root@luoahong ~]# df
Filesystem 1K-blocks Used Available Use% Mounted on
devtmpfs 1995624 0 1995624 0% /dev
tmpfs 2007620 0 2007620 0% /dev/shm
tmpfs 2007620 9336 1998284 1% /run
tmpfs 2007620 0 2007620 0% /sys/fs/cgroup
/dev/sda2 50306052 29273120 21032932 59% /
tmpfs 401524 0 401524 0% /run/user/0
overlay 50306052 29273120 21032932 59% /var/lib/docker/overlay2/0bc7de96c86ea3d2fe1059ccf2dea175b05a5434cc0a73858b5292b610699530/merged
shm 65536 0 65536 0% /var/lib/docker/containers/f0b72f14052f48a2a6eaf034d11e2fea77b76250bd87863e50d2f8aeb22c9918/mounts/shm

2、故障现象

1、top

进程部分有一个 python 进程的 CPU 使用率稍微有点达到了 40.4%。虽然 40.1%并不能成为性能瓶颈,不过有点嫌疑——可能跟 iowait 的升高有关

那这个 PID 号为 10167 的 python 进程,到底是不是我们的案例应用呢?

2、然后执行下面的 ps 命令,查找案例应用 app.py 的 PID 号:

[root@luoahong ~]# ps aux | grep app.py
root 10130 0.0 0.5 95700 23392 pts/0 Ss+ 10:29 0:00 python /app.py
root 10167 30.8 0.7 104924 30372 pts/0 Sl+ 10:29 4:32 /usr/local/bin/python /app.py
root 10256 0.0 0.0 112716 2288 pts/1 S+ 10:44 0:00 grep --color=auto app.py

从 ps 的输出,你可以看到,这个 CPU 使用率较高的进程,不过先别着急分析 CPU 问题,毕竟 iowait 已经高达92%

三、分析过程

1、观察系统I/O使用情况

1、案例

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

2、实际测试

root@luoahong ~]# iostat -d -x 1
Linux 5.1.0-1.el7.elrepo.x86_64 (luoahong) 05/30/2019 _x86_64_ (2 CPU) Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz aqu-sz %util
sda 2.14 834.77 0.01 0.44 23.88 390.85 75.84 85205.18 0.60 0.79 191.95 1123.42 0.00 0.00 0.00 0.00 0.00 0.00 14.57 10.79 Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz aqu-sz %util
sda 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 0.00 0.00 0.00 0.00 0.00 Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz aqu-sz %util
sda 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 0.00 0.00 0.00 0.00 0.00 Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz aqu-sz %util
sda 0.00 0.00 0.00 0.00 0.00 0.00 125.00 103592.00 1.00 0.79 39.18 828.74 0.00 0.00 0.00 0.00 0.00 0.00 4.83 14.90

-d 选项是指显示出I/O的性能指标;

-x 选项是指显示出扩展统计信息(即显示所有I/O指标)

1、你可以发现,磁盘 sda 的 I/O 使用率已经达到 98%接近饱和了
2、而且,写请求的响应时间高达 18 秒,每秒的写数据为32MB,虽然写磁盘碰到了瓶颈
3、这些I/O请求到底是那些进程导致的呢?

2、知道了进程PID,具体要怎么查看写的情况呢?

[root@luoahong ~]# pidstat -d 1
Linux 5.1.0-1.el7.elrepo.x86_64 (luoahong) 05/30/2019 _x86_64_ (2 CPU) 11:19:22 AM UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
11:19:23 AM 0 10167 0.00 124549.02 0.00 0 python
11:19:23 AM 0 10191 0.00 0.00 0.00 108 kworker/u256:1+flush-8:0 11:19:23 AM UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
11:19:24 AM 0 10167 0.00 126168.00 0.00 0 python
11:19:24 AM 0 10191 0.00 0.00 0.00 100 kworker/u256:1+flush-8:0

走到这一步,你估计觉得,接下来就很简单了,上一个案例不刚刚学过吗?无非就是,先用 strace 确认它是不是在写文件,再用 lsof 找出文件描述符对应的文件即可。

1、strace查看制定PID调用情况

[root@luoahong ~]# strace -p 10167
strace: Process 10167 attached
select(0, NULL, NULL, NULL, {0, 403619}) = 0 (Timeout)
......
stat("/usr/local/lib/python3.7/stringprep.py", {st_mode=S_IFREG|0644, st_size=12917, ...}) = 0
stat("/usr/local/lib/python3.7/stringprep.py", {st_mode=S_IFREG|0644, st_size=12917, ...}) = 0
stat("/usr/local/lib/python3.7/_bootlocale.py", {st_mode=S_IFREG|0644, st_size=1801, ...}) = 0
stat("/usr/local/lib/python3.7/_bootlocale.py", {st_mode=S_IFREG|0644, st_size=1801, ...}) =

由于 strace 的输出比较多,我们可以用 grep ,来过滤一下 write,比如:

[root@luoahong ~]# strace -p 10167 2>&1 |grep write

遗憾的是没有任何输出

2、filetop

它是 bcc 软件包的一部分,基于 Linux 内核的eBPF(extended Berkeley Packet Filters)机制,主要跟踪内核中文件的读写情况,并输出线
程 ID(TID)、读写大小、读写类型以及文件名称。

bcc的安装方法:https://github.com/iovisor/bcc

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@luoahong tools]# ./filetop -C
Tracing... Output every 1 secs. Hit Ctrl-C to end 11:54:58 loadavg: 2.37 1.27 0.54 2/185 9851 TID COMM READS WRITES R_Kb W_Kb T FILE
9850 python 2 0 3662 0 R 995.txt
9850 python 2 0 3564 0 R 998.txt
9850 python 2 0 3466 0 R 986.txt
9850 python 2 0 3466 0 R 994.txt
9850 python 2 0 3222 0 R 988.txt
9850 python 2 0 3173 0 R 993.txt
9850 python 2 0 2929 0 R 992.txt
9850 python 2 0 2832 0 R 990.txt
9850 python 2 0 2734 0 R 989.txt
9850 python 2 0 2490 0 R 997.txt
9850 python 2 0 2441 0 R 999.txt
9850 python 2 0 2294 0 R 987.txt
9850 python 2 0 2246 0 R 996.txt
9850 python 2 0 2099 0 R 984.txt
9850 python 2 0 1806 0 R 985.txt
9850 python 2 0 1660 0 R 991.txt
9847 filetop 1 0 4 0 R retprobe
9847 filetop 1 0 4 0 R type
9847 filetop 2 0 2 0 R loadavg
9851 sleep 1 0 0 0 R libc-2.17.so

线程号为 514 的线程,属于哪个进程呢?

[root@luoahong tools]# ps -efT|grep 9891
root 9798 9891 9755 46 11:59 pts/0 00:00:07 /usr/local/bin/python /app.py
root 9894 9894 9805 0 12:00 pts/1 00:00:00 grep --color=auto 9891

filetop 只给出了文件名称,却没有文件路径,还得继续找啊

3、opensnoop

它同属于 bcc 软件包,可以动态跟踪内核中的 open 系统调用。这样,我们可以找出这些txt文件的路径

[root@luoahong tools]# ./opensnoop
PID COMM FD ERR PATH
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/898.txt
9921 opensnoop -1 2 /usr/lib64/python2.7/encodings/ascii.so
9921 opensnoop -1 2 /usr/lib64/python2.7/encodings/asciimodule.so
9921 opensnoop 12 0 /usr/lib64/python2.7/encodings/ascii.py
9921 opensnoop 13 0 /usr/lib64/python2.7/encodings/ascii.pyc
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/899.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/900.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/901.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/902.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/903.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/904.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/905.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/906.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/907.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/908.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/909.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/910.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/911.txt
9798 python 6 0 /tmp/9ef81916-828f-11e9-960a-0242ac110002/912.txt

综合 filetop 和 opensnoop ,我们就可以进一步分析了。我们可以大胆猜测,案例应用在写入1000 个 txt 文件后又把这些内容读到内存中进行处理
我们来检查一下,这个目录中是不是真的有 1000 个文件:

[root@luoahong tools]# ls /tmp/9ef81916-828f-11e9-960a-0242ac110002 |wc -l
ls: cannot access /tmp/9ef81916-828f-11e9-960a-0242ac110002: No such file or directory
0

操作后却发现,目录居然不存在了,怎么回事呢?我们回到 opensnoop 再观察一会儿

[root@luoahong tools]# ./opensnoop
PID COMM FD ERR PATH
9798 python 6 0 /tmp/d94f7bec-829c-11e9-960a-0242ac110002/351.txt
10589 opensnoop -1 2 /usr/lib64/python2.7/encodings/ascii.so
10589 opensnoop -1 2 /usr/lib64/python2.7/encodings/asciimodule.so
10589 opensnoop 12 0 /usr/lib64/python2.7/encodings/ascii.py
10589 opensnoop 13 0 /usr/lib64/python2.7/encodings/ascii.pyc
9798 python 6 0 /tmp/d94f7bec-829c-11e9-960a-0242ac110002/352.txt
9798 python 6 0 /tmp/d94f7bec-829c-11e9-960a-0242ac110002/353.txt
9798 python 6 0 /tmp/d94f7bec-829c-11e9-960a-0242ac110002/354.txt
9798 python 6 0 /tmp/d94f7bec-829c-11e9-960a-0242ac110002/355.txt
9798 python 6 0 /tmp/d94f7bec-829c-11e9-960a-0242ac110002/356.txt
9798 python 6 0 /tmp/d94f7bec-829c-11e9-960a-0242ac110002/357.txt

原来,这时的路径已经变成了另一个目录,这说明,这些目录都是应用程序动态生成的,用完就删了。

结合前面的所有分析,我们基本可以判断,案例应用会动态生成一批文件,用来临时存储数据,用完就会删除它们。但不幸的是,正是这些文件读写,引发了 I/O 的性能瓶颈,

导致整个处理过程非常慢

4、确认猜想(查看源代码)

@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})

四、解决方案

1、问题总结

源码中可以看到,这个案例应用

1、在每个请求的处理过程中毛都会生成一批临时文件。
2、然后读入内存处理,
3、最后把整个目录删除掉

这是一种常见的利用磁盘空间处理大量数据技巧,不过,本次案例中I/O请求太重。导致磁盘I/O利用率过高

2、算法优化

要解决这一点其实就是算法优化问题,比如在内存充足时,就可以把所有的数据存放到内存中处理,这样就避免I/O的性能问题

你可以检验一下,在中断二中分别访问:http://192.168.0.10:10000/popularity/word和http://192.168.0.10:10000/popular/word对比前后的效果

http://192.168.0.10:10000/popularity/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

http://192.168.0.10:10000/popular/word

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 树等各种算法,进一步优化单词处理的效率。

Linux性能优化实战学习笔记:第二十七讲的更多相关文章

  1. Linux性能优化实战学习笔记:第四十一讲

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

  2. Linux性能优化实战学习笔记:第九讲

    一.中断的魅力 1.中断在生活的魅力 比如你订了一份外卖,但是不确定外卖什么时候送到,也没有别的方法了解外卖的进度,但是,配送员送外卖是不等人的,到了你这儿没人取的话,就直接走人了.所以你指能苦苦等着 ...

  3. Linux性能优化实战学习笔记:第五十七讲

    一.上节回顾 上一节,我带你一起梳理了常见的性能优化思路,先简单回顾一下.我们可以从系统和应用程序两个角度,来进行性能优化. 从系统的角度来说,主要是对 CPU.内存.网络.磁盘 I/O 以及内核软件 ...

  4. Linux性能优化实战学习笔记:第四十五讲

    一.上节回顾 专栏更新至今,四大基础模块的最后一个模块——网络篇,我们就已经学完了.很开心你还没有掉队,仍然在积极学习思考和实践操作,热情地留言和互动.还有不少同学分享了在实际生产环境中,碰到各种性能 ...

  5. Linux性能优化实战学习笔记:第三十六讲

    一.上节总结回顾 上一节,我们回顾了经典的 C10K 和 C1000K 问题.简单回顾一下,C10K 是指如何单机同时处理 1 万个请求(并发连接 1 万)的问题,而 C1000K 则是单机支持处理 ...

  6. Linux性能优化实战学习笔记:第四十三讲

    一.上节回顾 上一节,我们了解了 NAT(网络地址转换)的原理,学会了如何排查 NAT 带来的性能问题,最后还总结了 NAT 性能优化的基本思路.我先带你简单回顾一下. NAT 基于 Linux 内核 ...

  7. Linux性能优化实战学习笔记:第四十四讲

    一.上节回顾 上一节,我们学了网络性能优化的几个思路,我先带你简单复习一下. 在优化网络的性能时,你可以结合 Linux 系统的网络协议栈和网络收发流程,然后从应用程序.套接字.传输层.网络层再到链路 ...

  8. Linux性能优化实战学习笔记:第五十二讲

    一.上节回顾 上一节,我们一起学习了怎么使用动态追踪来观察应用程序和内核的行为.先简单来回顾一下.所谓动态追踪,就是在系统或者应用程序还在正常运行的时候,通过内核中提供的探针,来动态追踪它们的行为,从 ...

  9. Linux性能优化实战学习笔记:第五十五讲

    一.上节回顾 上一节,我们一起学习了,应用程序监控的基本思路,先简单回顾一下.应用程序的监控,可以分为指标监控和日志监控两大块. 指标监控,主要是对一定时间段内的性能指标进行测量,然后再通过时间序列的 ...

  10. Linux性能优化实战学习笔记:第五十八讲

    一.上节回顾 专栏更新至今,咱们专栏最后一部分——综合案例模块也要告一段落了.很高兴看到你没有掉队,仍然在积极学习思考.实践操作,并热情地分享你在实际环境中,遇到过的各种性能问题的分析思路以及优化方法 ...

随机推荐

  1. JMS简介与入门

    1:JMS引入 如果手机只能进行实时通话,没有留言和短信功能会怎么样?一个电话打过来,正好没有来得及接上,那么这个电话要传递的信息肯定就收不到了.为什么不能先将信息存下来,当用户需要查看信息的时候再去 ...

  2. SpringDataSolr入门

    1 Spring Data Solr简介 虽然支持任何编程语言的能力具有很大的市场价值,你可能感兴趣的问题是:我如何将Solr的应用集成到Spring中?可以,Spring Data Solr就是为了 ...

  3. Element-ui 下拉列表 全选 多选时 select全选 新增一个选择所有的选项

    项目里经常会用到,在一个多选下拉框里新增一个选择所有的选项,例如: <!DOCTYPE html> <html lang="en"> <head> ...

  4. Spring @CrossOrigin 通配符 解决跨域问题

    @CrossOrigin 通配符 解决跨域问题 痛点: 对很多api接口需要 开放H5 Ajax跨域请求支持 由于环境多套域名不同,而CrossOrigin 原生只支持* 或者具体域名的跨域支持 所以 ...

  5. sudo: ulimit: command not found

    在这看到的:http://stackoverflow.com/questions/17483723/command-not-found-when-using-sudo-ulimit 修改系统文件打开数 ...

  6. 【UOJ#62】【UR #5】怎样跑得更快(莫比乌斯反演)

    [UOJ#62][UR #5]怎样跑得更快(莫比乌斯反演) 题面 UOJ 题解 众所周知,\(lcm(i,j)=\frac{ij}{gcd(i,j)}\),于是原式就变成了: \[\sum_{j=1} ...

  7. C# 实现 奇数偶数排序,奇数在前,偶数在后

    public static void SortByOddAndEven(int []arr) { for (int i = 0; i < arr.Length; i++) { for (int ...

  8. hashmap与hashtable的本质区别

    HashMap 底层数据结构是哈希表.线程不安全,效率高                哈希表依赖两个方法:hashCode()和equals()                执行顺序:       ...

  9. Java编程基础——数组和二维数组

    Java编程基础——数组和二维数组 摘要:本文主要对数组和二维数组进行简要介绍. 数组 定义 数组可以理解成保存一组数的容器,而变量可以理解为保存一个数的容器. 数组是一种引用类型,用于保存一组相同类 ...

  10. Winform中怎样跨窗体获取另一窗体的控件对象

    场景 Winform中实现跨窗体获取ZedGraph的ZedGraphControl控件对象: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/de ...