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 基地址相加,我们就可以得到每个局部变量的地址,获取到每个变量的内容。