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
,關於 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 基地址相加,我們就可以得到每個局部變量的地址,獲取到每個變量的內容。