调试也是个很重要的问题,不可能保证代码没bug,要命的是有时候写代码完成功能的时间还没调试的时间多,编码不要过度求快,逻辑正确更重要。 复现是排错的第一步,之后通过各种方式确定原因(访问日志、邮件报的异常记录)等,通过走查代码、断点调试(二分法等)确定错误位置,确定好错误原因了就好改了。修复后最好反思下问题的原因、类型等,哪些地方可以改进,争取下次不犯一样的错,慢慢减少错误才能越来越高效。
尽量写出对自己也对其他人负责的代码,上边费了牛劲都是在阐述这个显而易见但是没多少人严格遵守的东西。用动态语言写大型项目维护起来要稍麻烦, 很多新手写代码不注重可维护性,甚至自己写的代码回头自己看都一脸懵逼,问了一句这代码TM是干啥的? 一开始的负责会为以后协作和维护带来极大便利(当然你想干两天就走让其他人擦屁股就当我没说)。 最后,很多东西我也在摸索,上面的玩意你就当小白的踩坑记录,随着理解和经验的加深我会不定期更新本篇内容。另外我发现网上大部分是教程性的东西,对于python相关的工程性的东西很少,我很疑惑难道大部分公司的python项目都写得相当规范?没人吐槽?反正我是踩过坑,希望看到过本章的人能把python代码质量重视起来。
如何定位和修复 bug:复现和定位。定位需要找到 bug 出现时候的上下文信息,可以用 log,sentry,kibana 日志系统等查看。确认之后通过走查代码、断点调试等方式寻找代码逻辑错误。
- 第一步是复现,偶尔才复现的代码是很难排查错误的。如果不好复现但是有 sentry 之类的记录工具也是极好的,sentry 会记录当前栈信息和变量信息,非常有利于排错。
- 走查代码。使用 pylint 等静态检测工具排除低级错误(你应该把它集成到开发工具里)。
- 看提交日志。最近代码的修改记录,是否是别人的代码引入了 bug。是否可以回滚到上一个可用部署解决呢?(注意一旦一个新的上线出问题,应该先回滚部署而不是回滚代码)
- 看日志,各种日志(logging, nginx),看 sentry 异常信息。很多框架或者工具都有 debug 模式,打开 debug 模式可以获取到更多有用信息(但是要注意线上慎用 debug 级别日志)
- 加日志。如果已有的日志没能排查出来关键信息,可以适当增加 debug 日志记录更充分的数据。比如关键函数的输入和输出,关键rpc调用/数据库查询/第三方库调用的输入和输出等。
- 问同事,问源码作者(脸皮要厚),让同事帮忙 review 审查代码。有时候人有思维定势,你自己看不出来的别人可能一眼就看出来了。
- 借助搜索引擎。很多问题 google/stackoverflow/github 上都可以搜到,善用搜索引擎解决问题。
- 小黄鸭调试法,桌子上放个小黄鸭(小黄鸡儿也行),然后尝试从头到尾给它讲解有问题的代码段,说不定就在你给它代码描述过程中发现了问题。
- 断点调试。看变量值。二分法(分而治之)排查代码位置,快速试错定位。比如一个地方很有隐秘的错误,但是又不好快速确定位置,我们就可以用二分加断点的方式快速定位到具体哪一块出了问题。
- 使用调试器(命令行or IDE 调试工具)。 ipdb/pdb 断点配合 python 一些内置方法比如 print/vars/locals/pprint 等断点调试,使用 curl/chrome 开发者工具/mitmproxy 等调试请求。代码异常可以通过 import traceback; traceback.print_exc() 打印出来。
- 日志比对/输入输出对拍。在重构系统的时候,首先保持原有系统和重构之后代码的正确性。可以通过比对日志,比对输入和输出值的方式确保正确
- 排除法。不断记录灵感/想法/可能的原因等,做排除法,缩小问题范围,说不定就可以发现 bug 的藏身点。
- 依赖库bug。一般经过广泛使用的第三方库是可以信赖的,但是公司自己造的轮子(尤其是文档和单测都没有的),还是有可能出 bug 的。有可能是依赖而非自己代码逻辑 bug。
- 升级后出问题。是否有完善的功能测试和单元测试保证回归没有问题?升级代码修改了哪些部分?降级之后能否复现?
- 服务超载。重点关注指标 cpu/io/memory/磁盘/log/连接数 是否被打满,是否无法继续正常服务,如果是服务器负载问题也会导致服务失败,不一定是主逻辑代码有问题(当然也有可能是连接池使用不当导致)。
- 是否是缓存的问题?缓存数据过期了么?缓存是否一致呢?能否清理缓存解决?测试环境禁用一下缓存看看表现如何(笔者之前改完代码一直 debug 没生效结果发现是缓存还在导致一直是旧数据)
- 服务之间的依赖关系如何?有没有分布式链路追踪,哪一步调用关系出了问题?是否是没有降级,有没有碰到雪崩?服务间有没有循环调用?
- 监控报警。各种服务指标监控是否有报警?报警是否正常?如果没有及时监控到是否可以增加相关指标的报警?
发现并且修复问题之后,我们需要通过之前的 bug 来涨涨记性,如何避免类似的问题再犯,养成良好的编码和思维习惯。
- 重视静态检查/编译器/IDE 开发工具的缺陷提示,尽量连 warning 提示都不要留,及时修复缺陷,保证高质量的代码可以有效减少 bug 产生。
- 不要死磕,一个法子不行换一个。死磕可能会耗费太长时间并且容易进入死胡同(思维定势),在一个大型复杂系统中定位 bug 原因是对技术、经验、毅力、灵感、心理素质的很大考验,休息一会甚至睡一觉醒来可能就解决了。
- 极难排查和复现的 bug 可以无限期搁置,bug 永远修不完的
- 找到 bug 修复以后增加相应单元测试用例,这样对回归测试非常有利,同时避免重复犯一样的错误。tricky 的地方要加上注释。
- 修复原因而非现象。你要排查出来真正导致 bug 的原因,而不是仅仅通过魔改代码修复了不合理现象。又比如仅仅依赖重启解决内存泄露等问题,而不去排查真正泄露的原因(当然可能排查起来很艰难)
- 真的是代码的问题么?还是非代码因素:比如代码是否正确部署上线等(比如之前脑残查一个 bug 无解最后发现是部署系统失败部署到线上压根没成功,还是老代码,根本没起作用)。如果实在没发现代码级别错误,单测也比较完善,可能就要考虑下非代码因素。
- 配置/环境问题。是否是因为配置而非代码逻辑 bug 导致的,线上/测试/开发环境 的配置是否正确,是否脑子抽了写串了,比如测试环境的配置写到了正式环境(这种看似低级的错误笔者在工作中就遇到过)
- 建立个人 bug 清单和上线核对清单,避免再次出现犯过的错误。你的每一个错误都应该自己用一个笔记软件或者小本本记录下来,避免再次犯错(小心被扣工资)。上线之前检查日志等级,进程数设置是否正确,建立核对清单,养成好的思维习惯
- bug 总结:建立错误检查表(核对清单),哪些可以避免的记录下来,防止以后再犯。(团队的知识财富)。比如笔者在关闭一个 bug 单的时候会注明 bug 产生的原因和修复方式,而不是修复完成之后就不长记性了
大多数 bug 都可以通过复杂度控制、设计复审、代码审查、代码静态分析、单测/功能测试等找出来,我们可以综合利用以上手段尽量减少代码缺陷,大幅减少给代码擦屁股的时间。
- 《软件调试修炼之道》 笔者比较推荐的一本书,告诉你正确的思维方式
- 《调试九法》
- 《Python ipdb 调试大法[视频]》 笔者经常在服务器上进行命令行调试,一些技巧
打算记录一下自己犯过和见过别人犯过的一些常见 bug 类型,尽量避免重复犯错,笔者会长期不定期更新这个错误列表,不断吸取自己 和别人的经验。
- 拼写错误。不要笑,这个错误其实很常见,推荐打开编辑器的拼写检查,可以消除一些类似问题。
- 类型错误。在动态语言和弱类型语言当中比较常见的一种错误(动态语言确实更容易出 bug),可以借助类型强转,type hint 工具。
- 资源没有关闭。打开的文件等资源一定要关闭,防止资源泄露。go 的 defer 和 python 的 with 最好用上
- 连接池使用不当打满连接数。连接池应该是全局共享的(单例),而不是每个请求都要去创建连接池。连接池应该是一个应用共享的
- 深浅拷贝问题。不同语言可能又不同的拷贝模型,确定你的参数是深拷贝还是浅拷贝,能否修改,修改了之后是否有副作用。
- 请求超时。网络请求的 client 是否有设置超时,比如有些 go 的 client 需要显示自己传进去超时参数
- 忽略错误/异常。除非你有 100% 的把握可以忽略,否则至少要在发生错误或者异常的地方加上日志,否则出问题很难排查
- 数组越界错误。注意涉及到数组的时候使用的下标是否会越界。
- 数据库查询参数错误。查询数据库的时候可能因为一些不合法参数导致数据库慢查询,可以在入口处做一下限制。比如限制limit 大小
- 路径错误。编写一些脚本需要处理文件的时候,使用绝对路径比较不容易出错。
- 空值错误。比如直接赋值一个 go 里边的 map 会 panic,你需要先给 map make 一个值,很多 go 新手会重复犯这个错(go slice 却可以直接声明之后 append)
- 闭包问题。循环里闭包引用的是最后一个循环变量的值,需要注意一下,很多语言都有类似问题,可以通过临时变量或者传参的方式避免
- 遍历修改列表问题。一边遍历,一边修改可能会使得迭代器失效而出错,最好不要遍历的时候修改列表。
- 遍历修改元素值问题。这一点 go 和 python 表现不同,go 比如你去循环一个 []Struct 是无法修改每个元素的,go 会拷贝每一个元素值,需要通过下标或者指针修改
- 线程安全。如果不是线程安全的操作,应该通过加锁等方式做数据同步。比如 go 里边如果并发读写 map 程序会出错
- 日志级别错误。线上使用了 debug 级别,可能导致日志打满,如果没有滚动日志可能会导致服务器磁盘打满。一定要注意不同环境日志级别
- 日志参数错误。日志语句对应的占位符要和传参的个数一致。
- 配置环境写错。看起来是一个很傻的错误,但是凡是以来人的编辑迟早有可能出错,注意不同环境配置是否对的上。
- 服务启动命令是否写错。有些服务依赖命令行启动的时候容易写错参数,建议通过配置文件的形式传进去。