bug的本意是指昆虫、小虫、损坏、缺陷等意思,在互联网时代还有一种引申意义,用来形容某人/物超乎想象的厉害,那简直就是开挂的人生,系统的bug!
一般地,在码农的世界里,bug是在电脑系统或程序代码中隐藏着的一些未被发现的缺陷或问题,可以简称为程序缺陷。从广义上看,还包括软件需要改进的细节、或与需求文档存在差异的功能实现等等。
bug 是如何与程序缺陷联系起来的呢?
Bug的由来
时光回溯到一台计算机可以装满整个房间的时代,大约在1945年9月9日,Grace Hopper发现了Harvard Mark II 计算机的第一个bug。Grace Hopper是数据处理方面的专家,在1952年为UNIVAC开发了第一个编译器,能够把人读得懂的高级语言翻译成计算机能够识别的机器语言。
那一天,Grace Hopper对Harvard Mark II设置好的17000个继电器进行编程后,技术人员正在进行整机运行,它突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。死去的飞蛾被夹扁在触点中间,从而“卡”住了机器的运行。
所以在报告中,Grace Hopper用胶条贴上飞蛾,并用“bug”来表示“一个在电脑程序里的错误”。后来,人们在电脑系统或程序代码中隐藏着的那些未被发现的缺陷或问题,也叫“bug”,同时 把排除程序故障叫DEBUG,这一“称呼”成为计算机领域的专业术语。
BUG和DEBUG的中文译为“缺陷”和“调试”。“缺陷”可能更反映事物的本质,因为“bug”是从外面爬进去的,并非程序本身有问题。而程序本身存在的问题,是程序原来就具有的。
程序代码中Bug的产生原因
一般地,在程序设计中的术语, Bug是在软件运行中因为程序代码本身有错误而造成的功能不正常、体验不佳、数据丢失、非正常中断、死机等现象。
Bug 的产生原因多种多样,千奇百怪,例如:
- 改错了文件
- 改对了文件,但放错了位置,或者根本忘了保存
- 改对了文件但没有重新编译
- 认为把那个条件变量开启/关闭了,但实际上弄反了
- 运行了错误的版本
- 改正了问题,但忘了提交
- 改正了问题,也提交了,但其他代码都依赖于之前有问题的版本
......
软件系统是一个丰富多彩的世界,总有Bug在里面飞来飞去。任何软件在发布时都不可能是绝对的零Bug,因为谁都不敢保证,自己写的代码没有任何问题。
bug的生命周期和分类
实际上, bug的生命周期可能是这样的:
产生--被发现--被解决或者变成了另一个bug。
但是从软件工程尤其是QA的角度看,任何一个bug的一般生命周期包括这样几个阶段:
新建--指派--已解决--待验--关闭
如果等待检验的bug在验证时没有解决好,则需要重新打开bug,开启循环并继续指派。
由于bug众多,我们在fix bug的时候往往本着要事优先的原则,处理那些影响较大的bug,这需要根据bug 的严重程度分类,例如:critical,major,minor,de-effeicency,也就是所谓的P0/P1/P2/P3, 当然粒度也可以分得更细或者粗一点。
根据不同视角,可以对bug有不同的分类,根据bug 所影响的领域分类,QA的测试领域可以参见《程序员眼中的测试》。
另外,bug的数量往往被用来作为衡量软件质量的一个指标。在CMM中规定的软件质量标准如下(Bug个数/千行源代码):
- CMM1级 11.95
- CMM2级 5.52
- CMM3级 2.39
- CMM4级 0.92
- CMM5级 0.32
因此,我们往往会在生产bug 和 debug中徘徊,写代码的时间与排错时间的比例有时会高达2:8。既然debug 是我们工作生涯中不可或缺的组成部分,容易混淆的是,debug 尽管在更多时候是一个过程,但有时候指的是一个程序——debugger。
Debugger是个程序
Debugger为一种调试软件,工程师或程序员可以用来验证算法。在一般的硬件设备上,都有着专门用于debug 的接口,在不太遥远的DOS世界里,一行debug 命令可以起到让操作系统初始化的效果。
在windows平台上,WinDbg是微软发布的一款相当优秀的源码级(source-level)调试工具,可以用于Kernel模式调试和用户模式调试,还可以调试Dump文件。
在Linux平台上,一般使用GDB,又称GNU调试器,是用来帮助调试程序的工具。gdb的主要功能如下:
很多跨平台的编程语言都一般会有各自的Debugger。PHP的调试方法最基本的是echo或者var_dump。还有就是使用zend debug 或者Xdebug的调试插件。
pdb是 The Python Debugger 的缩写,是Python标准库的一个模块。pdb模块规定了一个Python程序交互式源代码调试器,支持在设置断点(包括条件断点),也支持源码级单步调试,支持栈帧监视,支持源代码列出,支持任意栈帧上下文的随机Python代码估值。它还支持事后调试,并且能在程序控制下被调用。
例如:
import pdb import mymodule pdbn('myfoo.test()')
也可以命令行调用 python -m pdb myfoo.py 。
总之,Debugger是我们在单机debug中非常重要的工具。但是,对于分布式系统而言,这些debugger 都有着很大的局限,虽然也有一些debugger工具,但多是面向特定系统的。
那么分布式系统的debug 主要依赖什么呢?日志。关于日志的重要性,可以参考《全栈必备 Log日志》。
Debug的原则
不论是单机上的应用,还是分布式系统,debug是遵循问题隔离的原则,便于定位问题。
问题隔离可能是所有debug中最强大的核心原则. 问题是否可重现是非常重要的。如果不能重现这个问题的产生方式, 解决起来会变得异常困难。问题隔离使我们拥有了控制变量。
从空间隔离上看,程序代码中具有不同的库或者框架, 并且可以包含许多同事的提交, 其中, 可能有一些已经不再在这个代码库上工作了。问题隔离有助于消除问题的非必要部分, 以便专注于一个解决方案. 问题隔离的目的是弄清楚是发生冲突的根本原因,了解是否存在竞争条件。
从时间隔离上看,避免一次性的大量修改,越少改动代码越好,这样有助于发现问题。连续反馈的一致性结果越多, bug的跟踪就越容易。所以在调试时, 尽量不要安装任何新的软件或组件, 或者引入新的依赖。如果发现每次静态输入却返回了不同的错误, 应该马上提高警惕, 并且全力解决它。
Debug 时的一些雕虫小技
通过二分法去定位问题是Debug的一般方法,即便如此,面对各种不同的编程语言,还有千姿百态的软件系统,我们很难有高效快速的通用方法。
但是, 有一些debug时的关注点,可以看作雕虫小技。
1.环境检查
复现一个Bug,一般从环境检查开始。从底层到应用层逐层检查,要耐心确认。同时,关注帮助文档、手册或数据表,关注工作目录或者运行环境的路径。
2. 日志检查
不要害怕被大量的调试日志,看起来很吓人,但实际上是来帮忙的。大多数时候它会告诉我们真正的问题是什么和在哪里!在25年前,debug C 语言程序的时候,自己好像只能依赖printf来定位问题。
根据日志思考:什么导致了这个错误?错误是如何触发的?...
3. 代码核查
先看看拼写错误吧,typo 或者简单的语法疏忽有时不易发现。有时候括弧或者大括弧的幸运碰撞,有时候context成了content,fetch成了fecth,如果对自己的代码熟视无睹,可以让别人帮着看一眼。
同样,“以终为始”,先检查是否接收了正确的数据类型,一般的防御式编程都可以看到接收的参数或数据。如果接收方合乎预期,跟随调用链的脚步,看调用者的函数,一步一步逼近bug的所在地。
如果定位到了函数,尤其是那些执行的迭代方法(尤其如for、while、do等循环处理),尝试在console上显示一步一步地执行结果。
4. 定向验证
对指定的函数或者逻辑输入固定的数据,或者执行固定的代码来做定向验证,可以是正向的,也可以是逆向的。
如果增加了大量输入还看不到所需输出的话,需要确认一下这些代码真的在执行吗?虽然有了条件语句(如if,switch等),但实际上执行的是在这里么?在条件语句中注入简单的代码,比如打印一个简单的“hello world”,可以帮助您看到程序执行的流程。
当然了,如果有完整一点的单元测试,往往方便的多。
5. 注释代码
如果对于bug发生的逻辑模块位置迟迟不能定位,往往还要回归到通过二分法定位。逐行注释代码的方法可能是可行的,但不是最有效的方法。如果对问题所在没有什么想法的话,这可能是一条必经之路。
6. 手画流程
“好脑子不让烂笔头”,有时候,我们需要一支笔和一张纸,就可以从所有的技术细节中解放出来,就能从视觉上看到事情是如何运作的,把逻辑流程画出来能够帮助我们梳理和定位代码逻辑上的问题。这有点儿像反向工程,尤其对于那些不熟悉的代码,阅读代码并画出流程会提供较大的帮助。
随笔结语
“Zero Bug” 可能是程序员追求的目标,但现实中存在着较大的困难。程序员的日常离不开debug,宽泛一点说是trouble shooting(故障排除)。故障排除在很多时候依赖于经验,反复实践几乎是不二法门,但是,我们可以通过归纳总结自己的经验形成一个“心智模型”,可能是树状的,也可能是金字塔结构,也就是所谓的“套路”。
我们都有一个共同的梦想——成为更棒的程序员,但是如何做?如何学习和精进自己的技术?如何做业务分析和架构设计?如何做技术管理?本书就广大程序员都很关注的问题提供一些思路和方法。