日拱一卒,麻省理工教你Debug,從此debug不再脫髮
大家好,日拱一卒,我是梁唐。
今天我們繼續麻省理工missing smester課程——消失的課程,那些不會在課堂上講授的重要技能。
這節課的內容有兩塊,一塊是debug相關的技巧,另外一個部分是對系統的效能分析。由於整節課內容非常多,篇幅很長,所以分成了兩期,用兩篇文章寫完。今天我們先來看看debug技巧的部分,對於每個程式設計師來說,debug一定是不可避免的,所以花點時間瞭解一下debug技巧非常非常有必要,能夠大大提升之後開發過程的效率,降低尋找bug的痛苦。
和之前一樣,這節課的note質量同樣非常高。
本文是基於本節課note以及老師上課演示的內容,還有我個人的一些理解做的翻譯整理版本。日拱一卒,歡迎大家打卡一起學習。
前言
程式設計當中有一個鐵律:程式碼不是像你設想的那樣執行的,而是像你告訴它的那樣。有時候填平設想和實際的鴻溝是非常艱難的。在這節課當中,我們將會涵蓋一些有用的技術來處理bug以及資源管理。
Debugging
列印除錯日誌
最高效的debug工具就是縝密的思考配合恰當的輸出語句——Brain Kernighan,Unix for Beginners.
第一個debug程式的方式就是在你覺得可能出問題的地方加入一些print語句,持續迭代直到你搜集了足夠多的資訊瞭解到底是什麼導致了問題。
第二個方案是在你的程式當中使用日誌,而不是臨時新增print語句。相比於簡單的print語句,日誌擁有以下優勢:
- 可以將日誌寫入檔案、socket 或者甚至是傳送到遠端伺服器而不僅僅是標準輸出;
- 日誌可以支援嚴重等級(例如 INFO, DEBUG, WARN, ERROR等),這使您可以根據需要過濾日誌;
- 對於新發現的問題,很可能您的日誌中已經包含了可以幫助您定位問題的足夠的資訊。
下面這段python程式碼是一個使用log的例子:
```python import logging import sys
class CustomFormatter(logging.Formatter): """Logging Formatter to add colors and count warning / errors"""
grey = "\x1b[38;21m"
yellow = "\x1b[33;21m"
red = "\x1b[31;21m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
FORMATS = {
logging.DEBUG: grey + format + reset,
logging.INFO: grey + format + reset,
logging.WARNING: yellow + format + reset,
logging.ERROR: red + format + reset,
logging.CRITICAL: bold_red + format + reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
create logger with 'spam_application'
logger = logging.getLogger("Sample")
create console handler with a higher log level
ch = logging.StreamHandler() ch.setLevel(logging.DEBUG)
if len(sys.argv)> 1: if sys.argv[1] == 'log': ch.setFormatter(logging.Formatter('%(asctime)s : %(levelname)s : %(name)s : %(message)s')) elif sys.argv[1] == 'color': ch.setFormatter(CustomFormatter())
if len(sys.argv) > 2: logger.setLevel(logging.getattribute(sys.argv[2])) else: logger.setLevel(logging.DEBUG)
logger.addHandler(ch)
logger.debug("debug message")
logger.info("info message")
logger.warning("warning message")
logger.error("error message")
logger.critical("critical message")
import random import time for _ in range(100): i = random.randint(0, 10) if i <= 4: logger.info("Value is {} - Everything is fine".format(i)) elif i <= 6: logger.warning("Value is {} - System is getting hot".format(i)) elif i <= 8: logger.error("Value is {} - Dangerous region".format(i)) else: logger.critical("Maximum value reached") time.sleep(0.3) ```
一個我最喜歡的記錄日誌的技巧是對日誌進行上色。你可能已經發現了你的終端使用一些高亮顏色讓內容變得更加易讀。但它是怎麼實現的呢?像是grep這樣的程式使用了ANSI escape codes:https://en.wikipedia.org/wiki/ANSI_escape_code,它是一系列特殊的字元,可以讓你的shell改變輸出結果的顏色。
比如執行echo -e "\e[38;2;255;0;0mThis is red\e[0m"
會在終端打印出紅色的字串This is red
。
前提是你的終端支援true color,如果不支援的話(比如macOS的terminal就不支援),你可以使用更加通用的16色,比如echo -e "\e[31;1mThis is red\e[0m"
接下來的指令碼演示瞭如何在終端當中輸出多種RGB顏色(同樣這需要支援true color)
```shell
!/usr/bin/env bash
for R in $(seq 0 20 255); do for G in $(seq 0 20 255); do for B in $(seq 0 20 255); do printf "\e[38;2;${R};${G};${B}m█\e[0m"; done done done ```
第三方log
當你開始建立大規模的軟體系統的時候,你可能會使用一些依賴,這些依賴可能會獨立執行。網頁伺服器、資料庫以及訊息代理都是常見的第三方依賴。當和這些系統互動的時候,不可避免地會需要閱讀它們的log,因為僅僅靠客戶端的錯誤資訊不足以定位問題。
慶幸的是,大多數程式都會把它們的日誌記錄在你的系統的某處。在UNIX系統當中,程式通常會把它們的日誌寫在/var/log
當中。比如NGINX伺服器將它的日誌寫在/var/log/nginx
。
目前系統開始使用系統日誌,你所有的日誌訊息都會存在這裡。大多數(不是全部)Linux系統使用systemd
,這是一個系統守護程序,控制你係統當中的許多東西,比如一些服務的啟動和執行。systemd
將特殊格式的日誌存放在/var/log/journal
中,你可以使用journalctl
命令來展示這些訊息。類似的,在macOS上仍然有/var/log/system.log
,但越來越多的工具開始使用系統日誌,這些日誌可以通過log show
展示。對於大多數UNIX系統來說,你可以使用dmesg
命令來訪問核心日誌。
你可以使用logger
shell程式來記錄系統日誌,下面是一個使用logger
記錄日誌系統日誌,以及進行查詢的例子。並且,大多數程式語言都支援向系統日誌當中記錄日誌。
```shell logger "Hello Logs"
On macOS
log show --last 1m | grep Hello
On Linux
journalctl --since "1m ago" | grep Hello ```
就像是我們在資料處理那節課上看到的一樣,日誌通常非常冗長,並且需要進行一定程度的處理和過濾才能提取出有用的資訊。如果你發現通過jounrna
和log show
進行過濾非常麻煩,你可以試試使用它們的flag,可以先對結果進行一波過濾。同樣也有類似於lnav
這樣的工具,為日誌提供了更好的瀏覽和導航。
Debuggers
當我們列印debug資訊已經不足以找到問題的時候,你可以考慮使用debugger(偵錯程式)。偵錯程式是一些允許你可以互動式執行程式的工具,它允許你進行以下的操作:
- 當到達某一行時將程式暫停;
- 一次一條指令地逐步執行程式;
- 程式崩潰後檢視變數的值;
- 滿足特定條件時暫停程式;
- 其他高階功能。
許多程式語言支援偵錯程式,在Python當中,偵錯程式是pdb
下面是對pdb
支援的命令的一些簡單介紹:
- l(ist) - 顯示當前行附近的11行或繼續執行之前的顯示;
- s(tep) - 執行當前行,並在第一個可能的地方停止;
- n(ext) - 繼續執行直到當前函式的下一條語句或者 return 語句;
- b(reak) - 設定斷點(基於傳入的引數);
- p(rint) - 在當前上下文對錶達式求值並列印結果。還有一個命令是pp ,它使用 pprint 列印;
- r(eturn) - 繼續執行直到當前函式返回;
- q(uit) - 退出偵錯程式。
讓我們使用pdb
來修復下面的Python程式碼(參考講課影片)
```python def bubble_sort(arr): n = len(arr) for i in range(n): for j in range(n): if arr[j] > arr[j+1]: arr[j] = arr[j+1] arr[j+1] = arr[j] return arr
print(bubble_sort([4, 2, 1, 8, 7, 6])) ```
注意,由於Python是解釋型語言,我們可以使用pdb
shell來執行命令。ipdb
是pdb
的一個改進版本,使用IPython
並作為REPL開啟了tab不全、語法高亮、更好的回溯以及更好的檢查,同時還保持了和pdb
模組相同的介面。
對於一些更底層的語言,你可以使用gdb
(它的改進版pwndbg
)和lldb
。它們都對C-like語言除錯做了優化,允許你探索任意程序以及獲得當前機器的狀態:暫存器、棧、程式計數器等等。
特定工具
甚至當你在一個黑盒二進位制檔案中debug的時候,都有特定的工具可以幫到你。程式執行某些特定操作的時候必須要通過作業系統核心,這需要用到system call。有一些命令可以讓你追蹤你程式執行的system call。在Linux當中叫做strace
在macOS和BSD當中有dtrace
。
dtrace
用起來可能比較彆扭,因為它使用它自有的D語言,但也有封裝好的叫做dtruss
的工具,提供和strace
相似的介面。
下面是使用strace
或者dtruss
來展示執行ls
時,對stat system call的呼叫結果。如果想要深度瞭解strace
,可以閱讀這兩篇文章:https://blogs.oracle.com/linux/post/strace-the-sysadmins-microscope,https://jvns.ca/strace-zine-unfolded.pdf
```shell
On Linux
sudo strace -e lstat ls -l > /dev/null 4
On macOS
sudo dtruss -t lstat64_extended ls -l > /dev/null ```
對於 web 開發, Chrome/Firefox 的開發者工具非常方便,功能也很強大:
- 原始碼 -檢視任意站點的 HTML/CSS/JS 原始碼;
- 實時地修改 HTML, CSS, JS 程式碼 - 修改網站的內容、樣式和行為用於測試(從這一點您也能看出來,網頁截圖是不可靠的);
- Javascript shell - 在 JS REPL中執行命令;
- 網路 - 分析請求的時間線;
- 儲存 - 檢視 Cookies 和本地應用儲存。
靜態分析
對於一些問題,你不需要執行程式碼就可以發現。比如說,仔細觀察一段程式碼,你就能發現你的迴圈變數覆蓋了一個已有的變數名或函式名。或者是有一個變數在讀取之前沒有被定義。這種情況下靜態分析工具就可以派上用場了。靜態分析工具將原始碼作為輸入,基於程式設計規則對它進行分析,找出其中的問題。
在下面這個Python程式碼片段當中存在一些錯誤。首先,我們的迴圈變數foo覆蓋了先前定義的函式foo。我們同樣把最後一行的變數bar寫成了baz,所以程式完成sleep之後會崩潰。
```python import time
def foo(): return 42
for foo in range(5): print(foo) bar = 1 bar *= 0.2 time.sleep(60) print(baz) ```
靜態分析工具可以定位這類問題。當我們執行pyflakes
之後,我們可以得到一些關於bug的錯誤提醒。mypy
是另外一個工具,可以幫助我們檢查型別問題。這裡mypy
將會警告我們bar
這個變數一開始是int型別,但後來被強制轉換成了float。再次強調,這些問題都可以在不執行程式碼的情況下被發現。
```shell $ pyflakes foobar.py foobar.py:6: redefinition of unused 'foo' from line 3 foobar.py:11: undefined name 'baz'
$ mypy foobar.py foobar.py:6: error: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") foobar.py:9: error: Incompatible types in assignment (expression has type "float", variable has type "int") foobar.py:11: error: Name 'baz' is not defined Found 3 errors in 1 file (checked 1 source file) ```
在shell工具那節課當中,我們介紹了shellcheck,這是一個類似的工具,不過是用來檢查shell指令碼的。
大多數編輯器和IDE支援將這些工具的輸出結果展示在介面裡,對有警告和錯誤的地方進行高亮。這個過程通常被稱為code linting。同樣,其他型別的問題也可以同樣被展示,比如程式碼風格檢查和安全檢查。
在vim當中,ale
和syntastic
外掛可以讓你做到這些。對於Python來說,pylint
和pep8
是兩種用於進行程式碼風格檢查的工具。bandit
則可以用來進行安全檢查。
對於其他語言來說,人們編譯、整合了一系列擁有的靜態檢查的工具,比如awesome static analysis
:https://github.com/analysis-tools-dev/static-analysis(你可以檢視一下Writing章節),對於lint工具,也有awesome linters
:https://github.com/caramelomartins/awesome-linters
一個完善的工具用來做風格檢查通常被叫做code formatter,比如Python中的black
,Go中的gofmt
,Rust中的rustfmt
或者是JavaScript、HTML、CSS中的prettier
。這些工具會自動格式化你的程式碼,讓你的程式碼和常用的風格保持統一。雖然你可能並不想讓這些工具控制你的程式碼,但標準的程式碼格式可以幫助其他人更好的理解和閱讀你的程式碼,同樣也會更方便你去閱讀其他人的程式碼。
Exercises
- 使用Linux中的
journalctl
或者是macOS中的log show
來獲取最近一天超級使用者(root)登入以及所執行的命令。如果找不到任何記錄,你可以手動執行一些無傷大雅的命令,比如ls
- 學習這份
pdb
指南:https://github.com/spiside/pdb-tutorial,並熟悉相關命令,你可以參考這份教程:https://realpython.com/python-debugging-pdb/ - 安裝
shellcheck
並且嘗試對下面的指令碼進行檢查,它當中有什麼問題?修復它。在你的編輯器當中安裝一個linter外掛,這樣的話它可以自動警告你
```shell
!/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 ```
- (進階)請閱讀這份可逆除錯文件:https://undo.io/resources/reverse-debugging-whitepaper/,並使用
rr
或者RevPDB
執行一個簡單的例子
- LeetCode周賽309,米哈遊贊助的比賽居然不收簡歷……
- LeetCode周賽311,中規中矩的裸題大賽……
- 日拱一卒,麻省理工教你資訊保安和密碼學
- LeetCode周賽301,離大譜,手速場掉分,質量場掉大分……
- 日拱一卒,超程式設計不是元宇宙,麻省理工教你makefile、依賴管理和CI
- LeetCode周賽300,差點AK,剛拿到的勳章要丟了……
- 日拱一卒,麻省理工教你效能分析,火焰圖、系統呼叫棧,黑科技滿滿
- 日拱一卒,麻省理工教你Debug,從此debug不再脫髮
- 日拱一卒,麻省理工教你學Git,工程師必備技能
- 日拱一卒,配置開發環境不再愁,麻省理工手把手教你
- 日拱一卒,麻省理工CS入門課,命令列這樣用也太帥了
- 日拱一卒,麻省理工YYDS,一節課讓我學會用vim
- 日拱一卒,麻省理工教你CS基礎,那些酷炫無比的命令列工具
- LeetCode周賽297,一小時AK你也可以
- 聽說你入行好幾年還只會cd和ls,麻省理工開了這門課……
- 日拱一卒,伯克利CS61A完結撒花
- 日拱一卒,伯克利教你用SQL處理資料
- LeetCode周賽296,難度較低的新手練習場
- 日拱一卒,伯克利教你Python核心技術,迭代器和生成器
- 日拱一卒,伯克利CS61A,教你用Python寫一個Python直譯器