企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
认识到bug的重要性,有的bug会给公司带来巨大的经济损失,有的bug会给用户带来人身安全。如果航天飞机的操作系统出了问题怎么办?如果银行交易系统出了问题怎么办? # 确认问题的存在 不能重现的bug无法修复。如果能重现,必然某个地方出错了。 能重现,看看版本库和团队,是否已经被修复过,是否有解决方案。 没解决过,大致分析下解决要多少时间成本。如果你有很多bug修复,先从简单的开始,一个一个解决。 # 分析问题可能是什么原因导致的 首先,问题往往有其表象。而真正导致错误的原因才是我们想知道的。有的时候bug很简单,错的地方就是其表象那样。有的时候,你看到的错误不是问题真正的错误。 在我们,解决问题前,我们必须知道我们要找的是什么问题。我们要弄清“发生了什么?应该发生什么?”。我们写的每行代码都是一个指令,告诉计算机软件运行(web应用是访问url时)程序应该给予什么处理和输出的结果。所以我们应该知道当可能有问题的现象出现,系统发生了什么,以及正常情况下应该发生什么。 其次,我们根据错误的发生,来排查简单的情况。比如《东半球最先进的debug技巧》中提到的: - 你改错了文件 - 你改对了文件,但却是在别人的机器上 - 你改对了文件,但忘了保存 - 你改对了文件,但忘了重新编译 - 你认为你把那个东西开启了,但实际上你把它关闭了 - 你认为你把那个东西关闭了,但实际上你把它开启了 - 会议中,你应该用心听。 - 你运行了错误的版本 - 你运行了正确的版本,但却是在别人的机器上 - 你改正了问题,但忘了提交 - 你改正了问题,也提交了,但忘了push到版本库中 - 你改正了问题,也提交了,也push了。然而,很多用户的工作都依赖于之前有问题的版本,于是你必须回滚。 本文属翻译作品,英文原文标题是:Cutting edge debugging。 文章转载自:开源中国社区 [http://www.oschina.net] 本文标题:东半球最先进的 debug 技巧 本文地址:http://www.oschina.net/news/54244/cutting-edge-debugging 尽管不同语言的报错可能不太一样,但是他们基本上分为两种结构: 1. 系统级报错 2. 应用自定义级报错 ## 系统级报错 一般是 `可能错误的原因 + 错误文件路径 + 错误行号 [+错误级别]` 像下面的是最简单的PHP报错: ![2015-05-10/554eaf5d9634b](http://box.kancloud.cn/2015-05-10_554eaf5d9634b.png) ## 应用自定义级报错 这就头疼了,应为作为应用来说,是不希望用户看到明显的程序错误的,那样不友好,且不安全(因为源码路径暴露了)。 所以应用会在代码中将默认不处理的情况手动判断了报出人性化的错误,比如上传文件时超过大小限制了 ,会提示`“您上传的文件过大,超过2M”`之类的。并且肯定是人话,本国语言的。 像TP默认非调试模式时的任意出错都会到404页面 提示系统出错了。 这样的时候我们只能反向去找错误了。通过错误提示去源码里搜索报错的这个可能位置。 当然TP肯定会记录错误的,PHP应用出错了都会记录错误日志,方便我们去排查,像上面的情况,我们就要看Runtime 目录下的logs目录下的日志文件了。 后面一章节,我已经把PHP常见错误总结了下,下次看到报错信息,不要不认识了。 当然,以前TP错误不是很友好,模板错误直接白页了。让我们去猜。现在好多了。 框架有时候也不能做的尽善尽美,所以我们会依赖一些组件,来提高我们的排错流程。 # 在可能问题的地方断点、输出数据进行观察 其实,这个标题的意思是,重现问题的方法。我们解决问题时,一定要先重现问题,再去解决问题。 > 不能重现的问题是无法修复的。 比方说爱因斯坦只有一个, 你无法在没有爱因斯坦的时候,去推测他对于现在的问题的观点。 有的时候我们解决问题的优先级也是 先一般,再特殊。 80%的问题都解决了,剩下的都是小打小闹了哈。 而且,重现问题才能帮助我们取得解决问题的进展。 如果问题,不能100%的按照某些指定流程出现,你怎么证明这问题真是存在。 有的时候特例的问题,往往时操作人员的不当操作。 如果你连问题怎么重现实现不了,如何证明你的修复行为,已经解决问题了。 一旦我们能重现问题了,我们就能大概知道正确处理这块业务的代码在哪,以及可以提供推测提现这个问题的影响因素了。 > 有因必有果。 从程序的角度来说,错误的输出必定经过了错误的输入。 输入也有可能来自上层,传递过来。 当我们确定一个大概的报错地点时,我们要进行断点排查。 系统级别的错误信息有时,我们只需定位行就行了。 应用级别的错误,我们查找到报错提示(有可能这个提示是动态的), 我们先利用强大编辑器工具的文件搜索,进行直接搜索。 搜到了,如果只有一个位置,那就是他了。多个需要我们去看代码的上下文,确定当前出错的提示会不会在这个位置出现。懒一点的办法是加一个报错。 比方有两个错误位置,我们可以在错误位置上加上一行输出:“错误1”,另外的出错提示代码前加上“错误2” 这样,我们测试环境里运行时,看到底是错误1 还是错误2 就可以精确定位了。 当然,有时候系统会过滤一切输出,只输出应用定义的错误,这时候我们可以借助工具,如写文件,用**Socketlog**这样的工具,将错误信息输出到浏览器里。 如果错误提示搜索不到,我们可以将 错误提示分词,先搜关键的错误,比如上传,和大小分别搜。 搜上传,我们找到上传类,搜大小我们找到判断大小的地方。然后要做的就是改提示和配置了。 有的时候,我们看似无解的问难,实际上都能解决。以前实习的时候,有个html页面乱了,我找半天没找到什么原因呢,最后在同学的排除下,将一个页面一块一块的删除,定位了,错误的样式发生位置,解决了那个问题。 有的时候查找问题很吃力,几个小时没进展,我们可能会沮丧,但是不要放弃, 任何问题都是能找到并解决的。只是难易程度罢了。 # 解决问题 ## 一次只解决一个问题。 由于软件运行是个复杂的过程。如果我们看到一些简单的问题,就扑上去解决掉。这样有可能会有问题。解决多个问题的前提是,确保多个问题是独立的。不会相互影响的。 在精确定位的基础上,我们回顾那个代码片段的作用,应该发生什么,正确结果,以及现在的错误结果,怎么改让其返回正确结果。怎么改让用户接受错误提示。 确保你现在的处理符合设计,不确定时问一下产品经理。 ## 将问题最小化 有时候,我们找到一个问题代码范围,想要修复了,但是运行到这个代码块,要经过好多步骤。比方说,新增用户,修改资料,某些行为等。 Wait,既然我们已经确定位置了,我们何不把错误代码拿出来,只取一次的过程数据作为输入, 写个小测试代码文件,在这基础上修复问题呢?只要我的测试文件跑没问题,相应修改丢入系统中运行也没问题,其他人员测试页没问题,这个问题基本上就解决了。 # 测试问题是否解决 修改后,再次运行程序,到那一步。看看本地的输出是否正确。 再交给测试人员看各种场景是否到那一步结果都正确。 确保大家的输出结果都一致,问题就解决了。 # 反思自己为什么犯这种错误 解决问题了,你认为就该走开,该干嘛就干嘛去了吗? 不行,你得停下来思考,我为什么会犯这个错,这个错是属于什么类型的。 最好有个记事本,将自己犯的错记录下来。 还要深层次的思考,这个错是不是还有其他问题。 比方说我们写代码,开始写插入数据,有一个表:要求标题title不能重复。我写了没做判断。 测试人员测试出来了,我们解决了。查询判断了。然后我们要想的是,标题唯一是一种验证,我们是不是缺少数据插入前的全部验证判断,然后向产品询问,其他地方插入数据的条件,最后自己再补上其他模型的自动验证,这才是将一类问题解决掉了。 # 错误是什么炼成的 对于当前系统来说, 错误的产生由三个地方引入: ## 1. 上层系统引入的非法参数。 对于非法参数引入的错误, 可以通过参数校验和前置条件校验来截获错误; ##2. 与下层系统交互产生的错误。 与下层交互产生的错误, 有两种: a. 下层系统处理成功了,但是通信出错了, 这样会导致子系统之间的数据不一致; 对于这种情况, 可以采用超时补偿机制,预先将任务记录下来,通过定时任务在后续将数据订正过来。更好的设计方案? b. 通信成功了,但是下层处理出错了。 对于这种情况, 需要与下层开发人员沟通, 协调子系统之间的交互; 需要根据下层返回的错误码和错误描述做适当的处理或给予合理的提示信息。 无论哪一种情况, 都要假设下层系统可靠性一般, 做好出错的设计考虑。 ## 3. 本层系统处理出错。 本层系统产生错误的原因: 原因一: 疏忽导致。疏忽是指程序员能力完全可避免此类错误但实际上没做到。比如将 && 敲成了 & , == 敲成了 = ; 边界错误, 复合逻辑判断错误等。 疏忽要么是程序员注意力不够集中, 比如处于疲倦状态、加班通宵、边开会边写程序; 要么是急着实现功能,没有顾及程序的健壮性等。 改进措施: 使用代码静态分析工具,通过单元测试行覆盖可有效避免此类问题。 原因二: 错误与异常处理不够周全导致的。比如输入问题。 计算两个数相加, 不仅要考虑计算溢出问题, 还要考虑输入非法的情形。对于前者,可能通过了解、犯错或经验就可以避免, 而对于后者,则必须加以限定,以使之处于我们的智商能够控制的范围内,比如使用正则表达式过滤掉不合法的输入。对于正则表达式必须进行测试。对于不合法输入, 要给出尽可能详细、易懂、友好的提示信息、原因及建议方案。 改进措施: 尽可能周全地考虑各种错误情形和异常处理。在实现主流程之后,增加一个步骤:仔细推敲可能的各种错误和异常,返回合理错误码和错误描述。每个接口或模块都有效处理好自己的错误和异常,可有效避免因场景交互复杂导致的bug. 譬如,一个业务用例由场景A.B.C交互完成。实际执行A.B成功了,C失败了,这时B需要根据C返回合理的代码和消息进行回滚并返回给A合理的代码和消息,A根据B的返回进行回滚,并返回给客户端合理的代码和消息。这是一种分段回滚的机制,要求每个场景都必须考虑异常情况下的回滚。 原因三: 逻辑耦合紧密导致。由于业务逻辑耦合紧密, 随着软件产品一步步发展, 各种逻辑关系错综复杂, 难以看到全局状况, 导致局部修改影响波及到全局范围,造成不可预知的问题。 改进措施: 编写短函数和短方法, 每个函数或方法最好不超过 50 行。 编写无状态函数和方法, 只读全局状态, 相同的前提条件总是会输出相同的结果, 不会依赖外部状态而变更自己的行为; 定义合理的结构、 接口和逻辑段, 使接口之间的交互尽可能正交、低耦合; 对于服务层, 尽可能提供简单、正交的接口; 持续重构, 保持应用模块化和松耦合, 理清逻辑依赖关系。对于有大量业务接口相互影响的情况, 必须整理各个业务接口的逻辑流程及相互依赖关系, 从整体上进行优化; 对于有大量状态的实体, 也需要梳理相关的业务接口, 整理状态之间的转换关系。 原因四: 算法不正确导致。 改进措施: 首先将算法从应用中分离出来。 若算法有多种实现, 可以通过交叉校验的单元测试找出来, 比如排序操作; 如果算法具有可逆性质, 可以通过可逆校验的单元测试找出来, 比如加密解密操作。 原因五: 相同类型的参数,传入顺序错误导致。比如,modifyFlow(int rx, int tx), 实际调用为 modifyFlow(tx,rx) 改进措施: 尽可能使类型具体化, 该用浮点数就用浮点数, 该用字符串就用字符串, 该用具体对象类型就用具体对象类型; 相同类型的参数尽可能错开; 如果上述都无法满足, 就必须通过接口测试来验证, 接口参数值务必是不同的。 原因六: 空指针异常。空指针异常通常是对象没有正确初始化, 或者使用对象之前没有对对象是否非空做检测。 改进措施: 对于配置对象, 检测其是否成功初始化; 对于普通对象, 获取到实体对象使用之前, 检测是否非空。 原因七: 网络通信错误。网络通信错误通常是因为网络延迟、阻塞或不通导致的错误。网络通信错误通常是小概率事件, 但小概率事件很可能会导致大面积的故障、 难以复现的BUG。 改进措施: 在前一个子系统的结束点和后一个子系统的入口点分别打 INFO 日志。 通过两者的时间差提供一点线索。 原因八: 事务与并发错误。事务与并发结合在一起, 很容易产生非常难以定位的错误。 改进措施:对于程序中的并发操作, 涉及到共享变量及重要状态修改的, 要加 INFO 日志。更有效的做法??? 原因九: 配置错误。 改进措施: 在启动应用或启动相应配置时, 检测所有的配置项, 打印相应的INFO日志, 确保所有配置都加载成功。 原因十: 业务不熟悉导致的错误。在中大型系统, 部分业务逻辑和业务交互都比较复杂, 整个的业务逻辑可能存在于多个开发同学的大脑里, 每个人的认识都不是完整的。这很容易导致业务编码错误。 改进措施: 通过多人讨论和沟通, 设计正确的业务用例, 根据业务用例来编写和实现业务逻辑; 最终的业务逻辑和业务用例必须完整存档; 在业务接口中注明该业务的前置条件、处理逻辑、后置校验和注意事项; 当业务变化时, 需要同步更新业务注释; 代码REVIEW。 业务注释是业务接口的重要文档, 对业务理解起着重要的缓存作用。 原因十一: 设计问题导致的错误。比如同步串行方式会有性能、响应慢的问题, 而并发异步方式可以解决性能、响应慢的问题, 但会带来安全、正确性的隐患。异步方式会导致编程模型的改变, 新增异步消息推送和接收等新的问题。使用缓存能够提高性能, 但是又会存在缓存更新的问题。 改进措施: 编写和仔细评审设计文档。 设计文档必须阐述背景、需求、所满足的业务目标、要达到的业务性能指标、可能的影响、设计总体思路、详细方案、预见该方案的优缺点及可能的影响; 通过测试和验收, 确保改设计方案确实满足业务目标和业务性能指标。 原因十二: 未知细节问题导致的错误。比如缓冲区溢出、 SQL 注入攻击。 从功能上看是没有问题的, 但是从恶意使用上看, 是存在漏洞的。 再比如, 选择 jackson 库做 JSON 字符串解析, 默认情况下, 当对象新增字段时会导致解析出错。必须在对象上加@JsonIgnoreProperties(ignoreUnknown=true) 注解才能正确应对变化。如果选用其他 JSON 库就不一定有这个问题。 改进措施: 一方面要通过经验积累, 另一方面, 考虑安全问题和例外情况, 选择成熟的经过严格测试的库。 原因十三: 随时间变化而出现的bug。有些解决方案在过去看来是很不错的,但在当前或者未来的情景中可能变得笨拙甚至不中用,也是常见的事情。比如像加密解密算法, 在过去可能认为是完善的, 在破解之后就要慎重使用了。 改进措施: 关注变化以及漏洞修复消息,及时修正过时的代码、库、行为。 原因十四: 硬件相关的错误。比如内存泄露, 存储空间不足, OutOfMemoryError 等。 改进措施: 增加对应用系统的 CPU / 内存 / 网络等重要指标的性能监控。 系统出现的常见错误: 1. 实体在数据库中的记录不存在, 必须指明是哪个实体或实体标识; 2. 实体配置不正确, 必须指明是哪个配置有问题,正确的配置应该是什么; 3. 实体资源不满足条件, 必须指明当前资源是什么,资源要求是什么; 4. 实体操作前置条件不满足, 必须指明需要满足什么前置条件,当前的状态是什么; 5. 实体操作后置校验不满足, 必须指明需要满足什么后置校验, 当前的状态是什么; 6. 性能问题导致超时, 必须指明是什么导致的性能问题,后续如何优化; 7. 多个子系统交互通信出错导致之间的状态或数据不一致? 一般难以定位的错误会出现在比较底层的地方。 因为底层无法预知具体的业务场景, 给出的错误消息都是比较通用的。 这就要求在业务上层提供尽可能丰富的线索。错误的产生一定是多个系统或层次交互的过程中在某一层栈上不满足前置条件导致。在编程时, 在每一层栈中尽可能确保所有必须的前置条件满足,尽可能避免错误的参数传递到底层, 尽可能地将错误截获在业务层。 大多数错误都是由多种原因组合产生。 但每一种错误必定有其原因。 在解决错误之后, 要深入分析错误是如何发生的, 如何避免这些错误再次发生。 努力就能成功, 但是:反思才能进步 ! 最后,错误的代码,有时是由错误的人错误的认知导致的,如果发现一个人经常犯低级错误,是否应该先停下他的工作,让他重新将知识复习一遍。确保他的知识没问题再回来修复错误。 > 不要低估一个低级程序员的破坏力