2CPU,4GB內存python
預先安裝docker sysstat工具linux
案例中 Python 應用的核心邏輯比較簡單,你可能一眼就能看出問題,但實際生產環境中的源碼就複雜多了。因此,
我依舊建議,操做以前別看源碼,避免先入爲主,要把它當成一個黑盒來分析。這樣 你能夠更好把握住,怎麼從系統的資源使用問題出發,分析出瓶頸
所在的應用,以及瓶頸在應用中大概的位置ios
一、運行目標應用git
docker run --name=app -p 10000:80 -itd feisky/word-pop
二、確認應用正常啓動github
[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
一、接下來,在第二個終端中,訪問案例應用的單詞熱度接口算法
[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" }
稍等一下子,你會發現,這個接口竟然這麼長時間都沒響應,到底是怎麼回事呢?咱們先回到終端一來分析一下。docker
二、咱們試試在第一個終端裏,隨便執行一個命令,竟然也要等很久才能輸出json
[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
進程部分有一個 python 進程的 CPU 使用率稍微有點達到了 40.4%。雖然 40.1%並不能成爲性能瓶頸,不過有點嫌疑——可能跟 iowait 的升高有關ubuntu
那這個 PID 號爲 10167 的 python 進程,究竟是不是咱們的案例應用呢?bash
[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%
一、案例
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
二、實際測試
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指標)
一、你能夠發現,磁盤 sda 的 I/O 使用率已經達到 98%接近飽和了
二、並且,寫請求的響應時間高達 18 秒,每秒的寫數據爲32MB,雖然寫磁盤碰到了瓶頸
三、這些I/O請求究竟是那些進程致使的呢?
[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 找出文件描述符對應的文件便可。
[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
遺憾的是沒有任何輸出
它是 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 只給出了文件名稱,卻沒有文件路徑,還得繼續找啊
它同屬於 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 的性能瓶頸,
致使整個處理過程很是慢
@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 樹等各類算法,進一步優化單詞處理的效率。