日拱一卒,麻省理工教你Debug,從此debug不再脫髮

語言: CN / TW / HK

大家好,日拱一卒,我是梁唐。

今天我們繼續麻省理工missing smester課程——消失的課程,那些不會在課堂上講授的重要技能。

這節課的內容有兩塊,一塊是debug相關的技巧,另外一個部分是對系統的效能分析。由於整節課內容非常多,篇幅很長,所以分成了兩期,用兩篇文章寫完。今天我們先來看看debug技巧的部分,對於每個程式設計師來說,debug一定是不可避免的,所以花點時間瞭解一下debug技巧非常非常有必要,能夠大大提升之後開發過程的效率,降低尋找bug的痛苦。

B站影片連結

和之前一樣,這節課的note質量同樣非常高。

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 ```

就像是我們在資料處理那節課上看到的一樣,日誌通常非常冗長,並且需要進行一定程度的處理和過濾才能提取出有用的資訊。如果你發現通過jounrnalog 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是解釋型語言,我們可以使用pdbshell來執行命令。ipdbpdb的一個改進版本,使用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當中,alesyntastic外掛可以讓你做到這些。對於Python來說,pylintpep8是兩種用於進行程式碼風格檢查的工具。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

  1. 使用Linux中的journalctl或者是macOS中的log show來獲取最近一天超級使用者(root)登入以及所執行的命令。如果找不到任何記錄,你可以手動執行一些無傷大雅的命令,比如ls
  2. 學習這份pdb指南:https://github.com/spiside/pdb-tutorial,並熟悉相關命令,你可以參考這份教程:https://realpython.com/python-debugging-pdb/
  3. 安裝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 ```

  1. (進階)請閱讀這份可逆除錯文件:https://undo.io/resources/reverse-debugging-whitepaper/,並使用rr或者RevPDB執行一個簡單的例子