斷點除錯和日誌除錯之間的平衡點 --- 函式計算除錯之python篇
python 除錯篇
很多初學者喜歡使用斷點除錯,方便之處是可以查到執行期各種棧內的變數值,來幫助debug。
但這一點如果脫離了IDE,其實是非常困難的。在伺服器的執行過程中,更需要使用attach的方式才可能做到這點。
對於一些生產環境的錯誤定位,用斷點除錯幾乎是完全不可能的。
而使用日誌來做錯誤定位,對於一些指令碼語言,尤其弱型別的語言,當你將一個變數經過多個函式傳遞的過程中,如果傳遞過程中不小心有拼寫錯誤,只有最後使用到這個變數的地方才報出錯誤來,使用日誌的方式要定位什麼地方寫錯了非常困難,對於生產環境中多分支呼叫鏈極長的邏輯,更是難上加難。
本文將介紹一種結合函式堆疊,捕獲棧中local變數的方式來達到快速定位bug的目的。
相對於其他runtime,python可以獲取到很多執行時資訊。通常來說,通過異常捕獲,python 預設的traceback 給出的棧和錯誤資訊已經能幫助開發者除錯了。但時常來說,這並不是萬能的,偶爾會遇到一些問題,必須加日誌才能解決。但如果開發者日誌加得不夠細,生產環境中也很難立即重現,此時有什麼好辦法呢?
下面我們通過一個簡單的例子來講解如何在斷點除錯和日誌除錯中找一個平衡點定位python指令碼中的bug。
def bar(c): x = [1,2,3,4] return x[c] def foo(a, b): c = a + b bar(c) def test(): try: foo(2, 5) except: print "traceback"
假設 bar 中陣列訪問越界,實際的bug在c = a + b
那行,其實本應該是c = a - b
,我們沒有日誌,此時如何定位到這個bug?
使用範例:
- 我們先建立程式碼目錄:
mkdir ~/sandbox/fc/traceback/python
-
複製貼上以下程式碼到
~/sandbox/fc/traceback/python/main.py
import tracebackturbo as traceback def bar(c): x = [1,2,3,4] return x[c] def foo(a, b): c = a + b bar(c) def handler(event, context): try: foo(5, 2) except: print traceback.format_exc(with_vars=True) if __name__ == '__main__': handler(None, None)
-
然後切換到
~/sandbox/fc/traceback
目錄 -
執行 shell 命令:
fcli shell
,關於ofollow,noindex" target="_blank">fcli -
執行下述命令,其中
-d python
指的是當前程式碼所在目錄python
,而python2.7
指python2.7
runtime
sbox -d python -t python2.7
- 接下來我們使用 pip 安裝tracebackturbo 這個庫
pip install --target=$(pwd) tracebackturbo
- 本地可以先做一下測試:
python main.py
測試結果:
Traceback Turbo (most recent call last): File "main.py", line 13, in test Local variables: foo(5, 2) File "main.py", line 9, in foo Local variables: a = 5 b = 2 c = 7 bar(c) File "main.py", line 5, in bar Local variables: c = 7 x = [1, 2, 3, 4] return x[c] IndexError: list index out of range
我們可以看到棧中每個local變數都已經被print了出來,在生產環境中,我們可以在 service 上設定 logstore,將這部分錯誤資訊輸出到日誌服務。
比較
我們可以比較全日誌
及traceback
日誌的優缺點:
-
全日誌
-
優點
- 可以隱藏敏感資訊
- 對於無報錯,無異常丟擲的程式碼也可以做有效記錄
-
缺點
- 日誌可能記錄不全,線上問題調查很困難
- 需要記錄大量日誌,太多的日誌會導致效能低下
-
-
traceback 日誌
-
優點
- 報錯時可以拿到整個棧的資訊,分析問題可以非常全面
- 日誌簡潔,在沒有報錯的時候,不會有其他資訊干擾
- 由於只在報錯才有日誌,正常情況下只有try的開銷,相對來說效能更高
-
缺點
- 區域性變數中含有敏感資訊,可能會暴露給日誌檢視人員
-
實現原理簡介
接下來我們瞭解一下這個庫的實現原理,簡要提一下計算機系統執行時棧的結構:
棧結構
stack top | |
---|---|
frame 0 (bar) | |
frame 1 (foo) | |
frame 2 (...) | |
... | |
frame n() |
frame 的結構
name | comment |
---|---|
function proto | 函式資訊地址 |
frame base | frame 基地址 |
args | 函式引數地址 |
ret | 函式返回地址 |
var1 | 第一個區域性變數的空間 |
var2 | 第二個區域性變數的空間 |
... | ... |
varN | 第N個區域性變數空間 |
通常來說,各類計算機語言的 runtime 實現(實現細節及名稱可能各不相同)都會包含上述資訊。
function proto 結構
name | comment |
---|---|
filename | 實現檔名 |
line start, end | 函式實現具體行 |
local variables | 區域性變數資訊 |
每個變數包含
- 宣告行
- 相對於frame 基地址偏移
- 區域性變數宣告週期對應指令集
對於任何一個未 return 的函式,如果我們拿到了這個棧,就可以獲取到棧頂的若干 frame ,找到function proto,就可以找到各個區域性變數的偏移,通過 frame 基地址相加,我們就可以得到每個區域性變數的地址,獲取到每個變數的內容。