調試代碼#
打印調試法和日誌#
- 您可以將日誌寫入文件、socket 或者甚至是發送到遠端伺服器而不僅僅是標準輸出;
- 日誌可以支持嚴重等級(例如 INFO, DEBUG, WARN, ERROR 等),這使您可以根據需要過濾日誌;
- 對於新發現的問題,很可能您的日誌中已經包含了可以幫助您定位問題的足夠的信息。
第三方日誌系統#
- Unix
/var/log
dmesg
- Linux
systemd
/var/log/journal
journalctl
- MacOS
/var/log/system.log
log show
logger
向系統日誌中寫日誌
logger "Hello Logs"
# 在 macOS 上
log show --last 1m | grep Hello
# 在 Linux 上
journalctl --since "1m ago" | grep Hello
lnav
日誌查看器,更好的展現和瀏覽方式
調試器#
ipdb
增強型 pdb
Python 調試器
- l(ist) - 顯示當前行附近的 11 行或繼續執行之前的顯示;
- s(tep) - 執行當前行,並在第一個可能的地方停止;
- n(ext) - 繼續執行直到當前函數的下一條語句或者 return 語句;
- b(reak) - 設置斷點(基於傳入的參數);
- p(rint) - 在當前上下文對表達式求值並打印結果。還有一個命令是pp ,它使用
pprint
打印; - r(eturn) - 繼續執行直到當前函數返回;
- q(uit) - 退出調試器。
對於更底層的編程語言,您可能需要了解一下 gdb
( 以及它的改進版 pwndbg
) 和 lldb
。
它們都對類 C 語言的調試進行了優化,它允許您探索任意進程及其機器狀態:寄存器、堆棧、程序計數器等。
專門工具#
追蹤程序執行的系統調用
- Linux
strace
- MacOS BSD
dtrace
,用dtruss
封裝使其具有和strace
類似的接口
# 在 Linux 上
sudo strace -e lstat ls -l > /dev/null
4
# 在 macOS 上
sudo dtruss -t lstat64_extended ls -l > /dev/null
Chrome/Firefox 的開發者工具
- 源碼 - 查看任意站點的 HTML/CSS/JS 源碼;
- 實時地修改 HTML, CSS, JS 代碼 - 修改網站的內容、樣式和行為用於測試(從這一點您也能看出來,網頁截圖是不可靠的);
- Javascript shell - 在 JS REPL 中執行命令;
- 網絡 - 分析請求的時間線;
- 存儲 - 查看 Cookies 和本地應用存儲。
靜態分析#
將程序的源碼作為輸入然後基於編碼規則對其進行分析並對代碼的正確性進行推理
Python: pyflakes
mypy
Shell 腳本:shellcheck
code linting 風格檢查或安全檢查
Vim: ale
syntastic
Python: pylint
pep8
風格檢查 bandit
安全檢查
對於其他語言的開發者來說,靜態分析工具可以參考這個列表:Awesome Static Analysis (您也許會對 Writing 一節感興趣) 。對於 linters 則可以參考這個列表: Awesome Linters。
性能分析#
計時#
- 真实时间 - 從程序開始到結束流失掉的真實時間,包括其他進程的執行時間以及阻塞消耗的時間(例如等待 I/O 或網絡);
- User - CPU 執行用戶代碼所花費的時間;
- Sys - CPU 執行系統內核代碼所花費的時間。
$ time curl https://missing.csail.mit.edu &> /dev/null
real 0m2.561s
user 0m0.015s
sys 0m0.012s
性能分析工具(profilers)#
CPU#
CPU 性能分析工具有兩種: 追蹤分析器(tracing)及採樣分析器(sampling)。 追蹤分析器 會記錄程序的每一次函數調用,而採樣分析器則只會周期性的監測(通常為每毫秒)您的程序並記錄程序堆棧。
內存#
像 C 或者 C++ 這樣的語言,內存泄漏會導致您的程序在使用完內存後不去釋放它。為了應對內存類的 Bug,我們可以使用類似 Valgrind 這樣的工具來檢查內存泄漏問題。
對於 Python 這類具有垃圾回收機制的語言,內存分析器也是很有用的,因為對於某個對象來說,只要有指針還指向它,那它就不會被回收。
事件分析#
在我們使用 strace
調試代碼的時候,您可能會希望忽略一些特殊的代碼並希望在分析時將其當作黑盒處理。perf
命令將 CPU 的區別進行了抽象,它不會報告時間和內存的消耗,而是報告與您的程序相關的系統事件。
例如,perf
可以報告不佳的緩存局部性(poor cache locality)、大量的頁錯誤(page faults)或活鎖(livelocks)。下面是關於常見命令的簡介:
perf list
- 列出可以被 perf 追蹤的事件;perf stat COMMAND ARG1 ARG2
- 收集與某個進程或指令相關的事件;perf record COMMAND ARG1 ARG2
- 記錄命令執行的採樣信息並將統計數據儲存在perf.data
中;perf report
- 格式化並打印perf.data
中的數據。
可視化#
使用分析器來分析真實的程序時,由於軟件的複雜性,其輸出結果中將包含大量的信息。人類是一種視覺動物,非常不善於閱讀大量的文字。因此很多工具都提供了可視化分析器輸出結果的功能。
對於採樣分析器來說,常見的顯示 CPU 分析數據的形式是 火焰圖,火焰圖會在 Y 軸顯示函數調用關係,並在 X 軸顯示其耗時的比例。火焰圖同時還是可交互的,您可以深入程序的某一具體部分,並查看其棧追蹤。
調用圖和控制流圖可以顯示子程序之間的關係,它將函數作為節點並把函數調用作為邊。將它們和分析器的信息(例如調用次數、耗時等)放在一起使用時,調用圖會變得非常有用,它可以幫助我們分析程序的流程。 在 Python 中您可以使用 pycallgraph
來生成這些圖片。
資源監控#
- 通用監控 - 最流行的工具要數
htop
, 了,它是top
的改進版。htop
可以顯示當前運行進程的多種統計信息。htop
有很多選項和快捷鍵,常見的有:<F6>
進程排序、t
顯示樹狀結構和h
打開或折疊線程。還可以留意一下glances
,它的實現類似但是用戶界面更好。如果需要合併測量全部的進程,dstat
是也是一個非常好用的工具,它可以實時地計算不同子系統資源的度量數據,例如 I/O、網絡、 CPU 利用率、上下文切換等等; - I/O 操作 -
iotop
可以顯示實時 I/O 占用信息而且可以非常方便地檢查某個進程是否正在執行大量的磁碟讀寫操作; - 磁碟使用 -
df
可以顯示每個分區的信息,而du
則可以顯示當前目錄下每個文件的磁碟使用情況( disk usage)。-h
選項可以使命令以對人類(human)更加友好的格式顯示數據;ncdu
是個交互性更好的du
,它可以讓您在不同目錄下導航、刪除文件和文件夾; - 內存使用 -
free
可以顯示系統當前空閒的內存。內存也可以使用htop
這樣的工具來顯示; - 打開文件 -
lsof
可以列出被進程打開的文件信息。 當我們需要查看某個文件是被哪個進程打開的時候,這個命令非常有用; - 網絡連接和配置 -
ss
能幫助我們監控網絡包的收發情況以及網絡接口的顯示信息。ss
常見的一個使用場景是找到端口被進程占用的信息。如果要顯示路由、網絡設備和接口信息,您可以使用ip
命令。注意,netstat
和ifconfig
這兩個命令已經被前面那些工具所代替了。 - 網絡使用 -
nethogs
和iftop
是非常好的用於對網絡占用進行監控的交互式命令行工具。
如果您希望測試一下這些工具,您可以使用 stress
命令來為系統人為地增加負載。
專用工具#
有時候,您只需要對黑盒程序進行基準測試,並依此對軟件選擇進行評估。 類似 hyperfine
這樣的命令行可以幫您快速進行基準測試。
和 debug 一樣,瀏覽器也包含了很多不錯的性能分析工具,可以用來分析頁面加載,讓我們可以搞清楚時間都消耗在什麼地方(加載、渲染、腳本等等)。 更多關於 Firefox 和 Chrome的信息可以點擊鏈接。
課後練習#
調試#
-
使用 Linux 上的
journalctl
或 macOS 上的log show
命令來獲取最近一天中超級用戶的登錄信息及其所執行的指令。如果找不到相關信息,您可以執行一些無害的命令,例如sudo ls
然後再次查看。 -
安裝
shellcheck
並嘗試對下面的腳本進行檢查。這段代碼有什麼問題嗎?請修復相關問題。在您的編輯器中安裝一個 linter 插件,這樣它就可以自動地顯示相關警告信息。#!/bin/sh ## Example: a typical script with several problems for f in $(ls *.m3u) do grep -qi hq.*mp3 $f \ && echo -e 'Playlist $f contains a HQ file in mp3 format' done
性能分析#
-
這裡 有一些排序算法的實現。請使用
cProfile
和line_profiler
來比較插入排序和快速排序的性能。兩種算法的瓶頸分別在哪裡?然後使用memory_profiler
來檢查內存消耗,為什麼插入排序更好一些?然後再看看原地排序版本的快排。附加題:使用perf
來查看不同算法的循環次數及緩存命中及丟失情況。 -
這裡有一些用於計算斐波那契數列 Python 代碼,它為計算每個數字都定義了一個函數:
#!/usr/bin/env python def fib0(): return 0 def fib1(): return 1 s = """def fib{}(): return fib{}() + fib{}()""" if __name__ == '__main__': for n in range(2, 10): exec(s.format(n, n-1, n-2)) # from functools import lru_cache # for n in range(10): # exec("fib{} = lru_cache(1)(fib{})".format(n, n)) print(eval("fib9()"))
將代碼拷貝到文件中使其變為一個可執行的程序。首先安裝
pycallgraph
和graphviz
(如果您能夠執行dot
, 則說明已經安裝了 GraphViz.)。並使用pycallgraph graphviz -- ./fib.py
來執行代碼並查看pycallgraph.png
這個文件。fib0
被調用了多少次?我們可以通過記憶法來對其進行優化。將註釋掉的部分放開,然後重新生成圖片。這回每個fibN
函數被調用了多少次? -
我們經常會遇到的情況是某個我們希望去監聽的端口已經被其他進程占用了。讓我們通過進程的 PID 查找相應的進程。首先執行
python -m http.server 4444
啟動一個最簡單的 web 伺服器來監聽4444
端口。在另外一個終端中,執行lsof | grep LISTEN
打印出所有監聽端口的進程及相應的端口。找到對應的 PID 然後使用kill <PID>
停止該進程。 -
限制進程資源也是一個非常有用的技術。執行
stress -c 3
並使用htop
對 CPU 消耗進行可視化。現在,執行taskset --cpu-list 0,2 stress -c 3
並可視化。stress
占用了 3 個 CPU 嗎?為什麼沒有?閱讀man taskset
來尋找答案。附加題:使用cgroups
來實現相同的操作,限制stress -m
的內存使用。 -
(進階題)
curl ipinfo.io
命令或執行 HTTP 請求並獲取關於您 IP 的信息。打開 Wireshark 並抓取curl
發起的請求和收到的回覆報文。(提示:可以使用http
進行過濾,只顯示 HTTP 報文)