|
|
51CTO旗下网站
|
|
移动端

懵逼了!一个历时五天的Bug......

一个程序员在没有成长为架构师之前,几乎都要跟 Bug 为伴,程序员有很多时间都是花在了查找各种 Bug 上。

作者:大飞码字来源:51CTO技术栈|2019-05-23 09:30

【大咖·来了 第7期】10月24日晚8点观看《智能导购对话机器人实践》

一个程序员在没有成长为架构师之前,几乎都要跟 Bug 为伴,程序员有很多时间都是花在了查找各种 Bug 上。

我印象深刻的一个 Bug,是一个服务器网络框架无锁队列的 Bug 。那个 Bug 连续查找了五天的时间,才***定位出来。

当时我们的分布式存储系统出现了性能瓶颈,定位后发现瓶颈是在服务器网络框架上,所以我们决定为此替换一个***研发的网络框架。这个新的网络框架为了追求高的性能,采用了无锁队列的设计。

***天编码测试完成后,在测试环境跑,完全正常,特地搞了一堆 Log 来重放请求,程序跑得特别欢快。

解决了当时的性能瓶颈,感觉特别的开心,但好景不长,服务部署到现网环境,跑不到一个小时,就 Core Dump 了。

尝试上线了几次,每次都是跑半个多小时,就 Core Dump 。

当时的***反应是新网络框架的问题,这是很直觉的反应,但很快就产生了怀疑,因为这个框架也有其它模块在用,也没产生问题,当时觉得诡异了。

仔细查看了代码的修改记录,特定检查了版本管理系统的 Log,做了代码 Diff,确定确实只有这部分的修改。

凭着经验,我说服自己,这个时候应该坚信最明确的逻辑,不要走到其他歪路上去。

第二天,我把这个框架单独拎了出来,特地写了一个测试的模块,并用测试代码生成了一堆的请求数据,发送给测试模块。

它疯狂的运转起来,跑了一个多小时,跑得很欢快,一切正常,啥 Bug 都没有。

开大了并发,继续压,依然没有问题。懵逼了!不知咋回事。再次小心得灰度系统上线,跑不到一个小时还是 Core Dump 了,这个时候,我开始怀疑人生了,这个是咋回事了,都想砸键盘了都。

然后我冷静了下来,经验告诉我,这时应该按照正常流程完整地跑一遍测试模块。

于是我把那个测试模块打包成了现网模块,切走了一台现网机器的流量,把测试模块给上线到了一台现网机器。

之后用工具往现网机器发送数据,不到一个小时,Core Dump 了。终于复现了这个 Corde Dump, 那一刻犹如哥伦布发现了新大陆,简直欣喜若狂啊。这个时候已经是第三天了。

我复现了 Bug,但依然没办法定位出具体的原因。Core Dump 出来的栈是全乱的,没有任何价值,接下来,就开始用 Log 跟踪法了。

我依据数据的流转过程,在每个关键点,都打上 Log,Log 包含了所在的函数,行数,程序逻辑的编号,全部的关键数据等等。

我仔细地设计了这个 Log,争取打得不多不少,太多容易看晕头,而且太多无效的信息,会掩盖了真正的问题;太少,信息不足,又不足以判断,所以这种情况下打 Log 也是个艺术活。

通过精心设计的 Log,终于发现数据在一个特定的环节混乱后,程序就一定会 Core Dump。

分析 Log , 发现数据包在***时刻是完整的,但包似乎出现了乱序和重复。这个时候,才开始意识到可能是无锁队列的问题,因为只有队列出问题,包的进出顺序才会乱掉。

然后又花了半天的时间,专门为无锁队列写了测试用例,用数据疯狂地怼。在测试环境,依然一切安好,但上线到正式环境,压测半小时后,终于挂了。终于看到了胜利的曙光!这个时候已经是第四天了!

到现在已经很明确是无锁队列的问题了。但这个数据结构的代码不到 200 行。我拉了两个同事一起 Review,都没发现问题。但就是 Core Dump 了。

奇葩了,又陷入了人生怀疑,开始怀疑内存,怀疑 CPU,结果换了机器,还是一样。

后来,仔细地对比了现网环境和测试环境的区别,机器类型,操作系统版本都一样。然后编译器?咦!编译器?上去看了一下,结果发现编译器的版本不一样!

这段时间我所使用的现网编译环境升级了新的 GCC 版本,但测试编译环境,还是旧的版本的。(这个也比较坑)

当时的直觉是肯定跟编译器相关,但代码都一样,难道是编译器 Bug?不可能吧 ?!

后来想,不如将它们转换成汇编看看吧。于是用两个版本的编译器将 C 的代码各自转换成了汇编。然后 Diff 汇编代码,哇!发现真的有一行是不同的!

后来自己分析对比,发现是因为我们开启了 GCC ***级别的代码性能优化,不同版本的 GCC 在一些没有特定依赖的语句上的优化是不同的。

说人话,就是有一段代码,如果加了锁,两个版本的编译器下,都会产生一样的汇编,如果没有加锁,代码有一行的顺序被调整了,当然,从编译器优化的角度讲是对的,是我们使用姿势不对。

但无论怎么样,终于找出了这个问题。苍天啊! 找了五天呀!***当然是开开心心地上线了。

查这个 Bug 确实花费了很多的时间,不过也是没办法,你不解决 Bug,就不能上线,但中间也收获不少,特别是对编译器优化有了很深的印象,也算是为自己的查 Bug 能力,又贡献了一波经验吧。

对于 Bug , 我分享下自己的一些认识和建议。

面对 Bug 的态度

只要你持续地写代码,就一定会持续的产生 Bug,所以***个事情是要摆正对 Bug 的心态。我遇到过两个极端。

***个极端

遇到过一个 Leader,对系统质量相当重视,对我们写的代码要求很高,每次设计并写完一个新的系统,他喜欢跟你算这次的系统上线,产生了多少次故障,这半年时间产生了多少个 Bug,每个 Bug 的影响范围如何。

我们一堆人被搞得特别累,战战兢兢,到后面,大家都比较排斥去做优化,去重构代码,只求无过,不求有功了。

所以,我觉得这种方式不好,对 Bug 带着一种比较包容的态度去看待,可以减少不少的心理负担。

第二个极端

后来去了一个新的团队。新的团队很重视业务和工程迭代的速度,所以对代码质量和 Bug 的容忍度很高。如果是一个新上线的业务,是默许 Bug 存在的。

这种对质量过于松散的要求,在后面也带来不好的影响。大家对 Bug 太免疫了,以至于出现 Bug 和故障的时候,大家都不够紧张。

系统质量有一段时间出现比较大的问题,还因此被部门经理特训了一番,后面通过各种措施,才慢慢提高了整体的系统质量。

上面两种极端都不可取,应该很重视 Bug,尽量避免 Bug,但也不应该唯 Bug 多少论业绩。

具体到 Bug 的查找上,我说说我的一些经验。

Bug 的复现

我把 Bug 分为可重现的 Bug 和不可复现的 Bug 。

对于可重现的 Bug , 查找起来比较容易,比如可以用”二分查找“的方式,从模块层面开始定位起,每次折半,每次折半地缩小范围,一直到代码层面。

在代码层面,遵循一些常用的原则,比如:

  • 看到内存拷贝,直觉上要想到内存越界。
  • 看到数组,就要考虑是否索引越界。
  • 看到指针,就要考虑是否正确解构。
  • 看到多线程,就要考虑是否线程安全。
  • 对于不可重现的 Bug,***步就是要把它重现出来。

有时候特别的难,特别是并发形态下产生的 Bug,出现的时机和触发条件都不清楚。对于这种 Bug,只能通过各种尝试去复现它。

比如将多并发调整为单并发的方式,看能否复现,如果可以复现,就可以转化为可复现的 Bug,用”二分查找“的方式去排查。

如果不能复现,那极大概率是并发问题。这个时候***先停止排查,仔细分析程序在并发状态下可能出问题的点。

大部分并发问题的根源,是互斥数据没有被正确读写,或者一些共享状态被错误修改。

静态代码检查

利用 Coverity 等代码检查工具进行代码的静态检查可以发现很多潜在的问题,而且修复的成本很低。团队后来引入了这个检查工具,确实带来了不错的效果。类似变量未初始化,疑似的内存越界等都有可能被检查出来。

编译器的 Warning。有些同学一开始的时候对 Warning 不重视。我们团队早期也遇到过这个情况。

那时候产品迭代的速度很快,所以大家写完代码,能够编译通过,就进行各种测试,然后准备上线了。

一开始的一两个 Warning,不太理会。后面发现越积越多,到***终于成为一个不得不解决的问题。

部门还为此特地立项,来消除 Warning。先是在内部多次强调了这个理念,然后从基础库,基础模块开始实施,基础代码部分统一 Fix Warning,然后开启编译器把 Warning 当 Error 的开关。

完成之后,再逐步地推业务模块进行修改。反正折腾了好一段时间。

工欲善其事必先利其器

代码 Bug 出现的时候,善用一些排查工具可以极大得提升效率。比如对于 C/C++ 的 GDB 调试,内存泄漏时候 Valgrind 的检测,Linux 下面用 Perf Top 来析 CPU 的消耗等。

一开始的时候,我对这些工具不重视,老是觉得真正使用的时候,去查文档就行。

后面才发现,用工具查着问题时候,遇到不会用的命令或功能,再去查文档,是个痛苦的事情,来回切换的开销也使得效率低下。

后面就对这些辅助工具的使用重视了起来,专门花时间去学习和练习使用,反而提升了不少的效率。

打 Log 的艺术

很多时候,出现一个 Bug,未能定位出来,需要打上更多额外的 Log 来辅助排查。一开始的时候,是想到一点,打一个 Log,后面发现这么做没有章法,逻辑不清晰, 排查效率低下。

后来学会了,遇到 Bug 后,先在脑子里面分析一番,然后花一两个小时详细地设计 Log 的格式和打 Log 的位置。发现这种方式对查问题的效率提升很大。

所以遇到 Bug 的时候不要急躁,先静下来心来分析,在脑海里尽力重现出完整的运行逻辑,然后仔细地进行 Log 设计,包括 Log 包含的字段,打 Log 的点等。这样能极大的提升排查问题的效率。

结语

Bug 是程序员最不愿意面对,但又经常出现的一个 “诡异生物“。对待 Bug 要有正确的心态,***不要跟业绩强绑定,也不能太过于疏忽。

Bug 的排查是个很复杂的事情,每个人都有自己的方式和做法,如果你有好的做法和建议,也欢迎在留言区分享给大家!

【编辑推荐】

  1. 19款超好用的免费数据挖掘工具大汇总!
  2. 华为向Oracle、SAP下战书 发布数据库替换Salesforce
  3. 大数据分析&人工智能:技术内容价值观的辨析
  4. 激活数据价值 永洪科技的智胜法则
  5. 面试陷阱:数十亿级别数据下,如何提高ES查询效率?
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

这就是5G

这就是5G

5G那些事儿
共15章 | armmay

115人订阅学习

16招轻松掌握PPT技巧

16招轻松掌握PPT技巧

GET职场加薪技能
共16章 | 晒书包

371人订阅学习

20个局域网建设改造案例

20个局域网建设改造案例

网络搭建技巧
共20章 | 捷哥CCIE

758人订阅学习

读 书 +更多

精通JBuilder 2006

JBuilder 2006是一款强大的Java企业级开发平台,其集成了几乎所有的Java技术,涵盖了软件开发生命周期的各个过程。本书深入浅出地介绍了JBu...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微