那些让人睡不着觉的 bug,你有没有遭遇过?

我先讲一个小故事,之前在外企工做时的一个亲身经历。前端

当时我所在的team,负责手机上多媒体Library方面的开发。有一天,一个具备最高等级的bug被转到了个人手上。这个bug很是诡异,光是重现它就须要花很长时间。在公司内部的issue追踪系统上,测试人员描述了详尽的重现步骤,大概意思是说,用某个指定产品线的手机去播放一段《黑客帝国》的视频,大概播放到半个小时左右的时候,程序就会忽然崩溃。程序员

你能够想象,形成崩溃问题的可能缘由实在太多了,好比某个局部算法的实现发生地址越界了,或者多线程执行的时序混乱了,再或者传给解码器的参数传递错了,等等,诸如此类。问题可能出在上层应用,也可能出在中间的Library,或者编解码器有问题,甚至是底下的内核或硬件不稳定(我当时所在的公司是一家手机制造商,软件和硬件都是本身设计),总之,你能想到的或想不到的都有可能。算法

事实上,对于这个问题的分析也是按照从上至下的顺序进行的。首先,既然播放会崩溃,那至少看起来是播放器的问题啊,OK,issue会先转给播放器的开发团队。播放器的开发团队通过分析以后发现,并非他们的代码引起的问题,接下来他们把分析结果附在issue的处理历史上,并把issue转给下一个团队处理。那应该转给哪一个团队呢?就要看上一个团队的分析结果了。他们在分析的过程当中,会追踪到最终崩溃的地方是发生在他们引用的哪一个代码模块中,而后就有专门的人负责找到维护相关模块的团队。就这样,这个bug从上层开始,通过层层流转,终于有一天来到了个人手上。数据库

一个团队被分发到这样一个最高等级的bug,就意味着必须停下正在进行的一些工做,当即分出人手来处理它。这就像一个烫手的山芋,谁也不想让它在本身的团队里待上太长时间。我通过大半天的分析,终于证实了崩溃的精确位置并不在咱们负责维护的代码区域里,而是在咱们调用的更下层的一个模块中。OK,在系统中填上分析结果,附上分析日志,再起草一封总结邮件,个人处理工做就此愉快地结束。可是,bug依然存在!c#

有人会好奇,这个bug后来怎么样了?它就这样在issue追踪系统上转了个把月,最后因为对应的产品线被cancel了(也就是那个产品线被砍掉了),天然全部相关的bug也就不必再去解决了。这个bug就这样不了了之了......后端


我所说的这家外企,曾经以完善的工做流程和质量管理体系而著名。无论是开发新特性,仍是解bug,都是靠流程去推进。公司员工众多,而且分布在全球范围,这样的一套内部管理流程天然是必不可少的。假设当时那个bug,若是最后不是被cancel了,那么它会不会在流程的推进下最终被解决呢?我以为,会,必定会。只要时间足够长,最终它确定会被转到真正可以解决它的人手里。只不过,这里的问题是,总体运转的效率过低下了。安全

与传统的IT公司不一样,互联网公司通常被认为是运转效率更高的。但有些bug其实更加难解,由于互联网产品运行的环境更加复杂多变。面对一些难解决的问题,好比对于某些用户报出来的但咱们本身却没法重现的问题,咱们有时候会碰到这样一幕:后端的同窗查完,宣称后端没有发现问题;而后客户端或前端同窗查完,也宣称没有发现问题。最后,你们也不能一直耗在这一个问题上,后面还有数不清的开发需求在排队,因而,问题也一样不了了之了。等过了一个月,两个月,甚至是一年,有些「老大难」的问题依然存在。服务器

这显然不是咱们但愿看到的结果。那么问题到底出在哪呢?微信

首先,没有人能了解全貌。像我开头讲的外企中的那个例子,每一个团队基本只了解本身负责的模块,没有人知道问题真正出在哪。这时候最理想的状况是,公司有一些元老级的技术专家,他们可能在公司初创的时候就在,随着公司一块儿成长,既懂业务又懂技术,可以从上层一直分析到底层,最终把问题解决掉,或者至少分析到足够的细节再转给真正能解决问题的人。但事实每每事与愿违,公司就算有一些元老的员工,他们也每每过早地脱离了技术。他们一般很忙,忙着开各类各样的会(固然开会并非一个贬义词)...... 那实际中咱们若是没有这样了解全盘的人该怎么办呢?这就须要责任心极强的人,可以把解决问题的各方串起来。网络

其次,缺乏足够的分析问题的手段和工具。对于知道如何重现的问题,通常来讲都比较容易解决,工程师经过调试,一步步跟踪,总能找到问题所在。但对于那些很差重现的问题,每每使人束手无策,由于咱们不知道问题发生时的真实状况,也就是抓不到「现场」。

记得刚开始出来创业那会儿,咱们的服务器发生了一件奇怪的事。每隔一两天,就会有台Web服务器莫名地死掉。当时的报警机制也不太完善,问题发生时又可能是在深夜,等问题出现时去看的时候,服务器已经登陆不了了,因而只能重启解决,而重启以后问题也就消失了。经过一些监控工具去观察,只能看到机器重启前CPU暴涨,跑到了100%,多是因为用的是虚拟机的缘故,那个时候机器就陷入「假死」了。通过反复追踪,终于有一次抓到「现场」了,在CPU跑满以前把流量从出问题的机器上卸了下来,结果那台机器的CPU竟依然居高不下。最后使用jstack分析了半天,发现有一些线程出现了死循环(仔细看才能看出来),原来是有一个HashMap被用在了多线程的环境下,结果内部的数据结构发生混乱了,在JDK内部对Map进行遍历操做的时候出现了死循环,最后把CPU跑满了。原本是个线程安全问题,表现出来倒是一个性能问题。如今回想起来,若是当时有更完善的监控工具,就能尽早地发现问题;若是对程序的栈结构和jstack工具备更深的了解,就能更快地分析出问题缘由。

另外,对于互联网产品上常常出现的那种用户侧有问题,而咱们却没法重现的状况,技术同窗感受到解决困难的缘由,也每每是供他分析的「资料」不足。

第三,也是最重要的,咱们须要的是持之以恒的精神。顽固的bug就像狡猾的猎物,它会激起出色猎手的兴趣,而普通的猎手则会轻易放弃。出色的猎手会一直追踪它,直到最终捕获。对待顽固的那些bug,真正的解决之道其实只有一个,那就是你要比它们更加顽固。

不少人会产生这样一种想法,认为解bug纯粹是个体力活,不值得长时间投入。实际上,对于技术专业自己的进阶来讲,这倒是打怪升级,使得技艺登堂入室的必要一步。一方面,当你一直在留心观察某一个问题的时候,你对于系统相关的运行模式会愈发地熟悉。你知道正常状况下的参数水平,也能识别出每个异常状况。几乎没有别的方式能让你对系统的了解达到如此深刻和敏感的程度。另外一方面,我以前在《技术攻关:从零到精通》一文中也提到过,研究某个具体问题自己就可能引起整个架构的调整。 当旧的架构怎么修补也没法解决问题的时候,它最终将化茧成蝶、浴火重生,全部这些因素逼迫系统的架构向着更高的层次进化。


实际中咱们通常会碰到哪些比较顽固的问题呢?至少有这么三类:

  • 不必定何时出现的;
  • 跟性能有关的(找性能瓶颈);
  • 只在特定环境中出现的。

我前面提到的那个CPU跑满的例子,就属于第一类。对这种问题,一方面,要仔细研究代码,另外一方面,就是在问题出现以前作好充分的准备,记录下足够多的日志信息,这样才能在问题真正出现时「抓住」它。

跟性能有关的问题,它的难点就在于当问题出现时它所表现出来的各个因素相互影响,分不清哪一个是因哪一个是果。咱们有时须要进行复杂的Profiing(动态的性能分析)才能找到缘由。客户端的问题相对单纯一点,有不少成熟的Profiling的工具,而服务器的状况相对复杂一些。忽然想到了胡峰同窗在他的公众号「瞬息之间」上翻译过的一篇文章《认清性能问题》,写得很好,值得一读。文章对于响应时间和吞吐量的关系,以及性能拐点的描述,使人印象深入,颇有指导意义。原文地址以下:

mp.weixin.qq.com/s/-M2EfUc_X…

在创业的这几年中,随着访问量的增大,性能问题一个接着一个(特别是数据库的性能问题)。但真正印象深入的仍是创业初期碰到的那些问题,也许是由于当时经验不足,因此才感触比较深吧。记得有一天早上流量高峰期,几台Web服务器相继宕机。重启以后内存渐渐走高,坚持不了几分钟,内存就又爆掉。你们简单地分析以后,仍是不肯定是什么具体缘由。因而我跟坐在旁边的李甫同窗商量,要不你负责把服务器按期地提早重启一下,别等它本身OOM了......至少线上服务不会一点都不可用,而我来负责用工具分析一下。而后用jmap把整个heap都dump了出来,把dump文件拷贝到一台空闲机器上,再启动jhat来观察。结果一会儿看得比较明显了,是源于ConcurrentHashMap相关的一些数据结构形成了内存泄露。分析到这里,若是没有相关的经验,可能仍然不知道是怎么回事。可是结合网上的资料,就能把怀疑指向一点:可能代码中使用了HttpSession,而HttpSession是由ConcurrentHashMap来管理的。查找一下工程代码,果真,用到了request.getSession()。在分布式的Web架构中,HttpSession就是个鸡肋,不少有开发经验的公司都会禁止程序员使用它,但总有人忘了这件事。还好,这些调用的地方通常都比较容易消除。改掉以后内存问题也就随之消失了。

第三类「只在特定环境中出现的」问题,更是难以解决。一般它们只影响少部分用户,因此人们的重视通常也不够。这种问题客户端开发遭遇的会比较多,特别是安卓客户端,主要是执行环境太复杂了。它们有时候只在某些特定机型上出现,有时候只对于某些特定用户出现,还有时候甚至只在特定的网络环境下才出现。

好比有一次,有些用户报告咱们的App里某个游戏打不开,缘由是资源下载老是失败,并且只在手机链接WIFI的时候下载失败,若是换成了3G信号就能够下载成功了。咱们天然是重现不了,直到后来拿到了一些用户手机上下载的部分文件才弄清怎么回事。原来是咱们的游戏资源中包含一个XML配置文件,而这个文件被插入了一段JS代码,因而对于这个文件咱们的下载程序就校验不过去了。那是谁插入的JS代码呢?答案是运营商。为何要插入呢?是为了显示一个广告......没错,这就是传说中的被运营商「流量劫持」了。运营商的程序把这个XML文件误认为是一个网页了。

还有一次,忽然有用户报告在某些vivo机型上,QQ登陆失败。咱们本身重现不了,咱们拿现有的vivo手机也重现不了,甚至尝试把设置里的「不保留活动」选项打开,仍然重现不了。记得当时是皇甫同窗在解决这个问题,花了很多力气,最终想办法拿到了用户手机上的执行日志,才搞清了是什么情况。原来是在跳转到QQ去登陆以前,咱们的程序在内存里保存了一个变量,等从QQ登陆完跳回来以后,这个变量的值消失了。这个变量保存在一个单例的实例里面,按说不该该被释放。但多是因为那个手机型号上系统资源严重不足,或者是系统有些特殊的设置,致使跳转到QQ以后,咱们的进程被系统KILL了(这在安卓系统上应该属于正常的行为),天然全部内存的值也都保存不住了。

你们可能已经看出来了,解决在用户侧发生的特定问题,关键是可以收集到用户的本地运行日志。好比微信开放出来的Mars Xlog,就是作这个事情的,是很好的一个工具。听说做者最近还会放出一些新的特性。若是你用的是iOS版微信,那么在添加朋友的时候输入「:up」,就能看到微信的日志上报界面(安卓版微信我不知道怎么能点出来,有知道的同窗能够在下面留言)。这里上报的日志听说就是经过Xlog打印出来的。

总之,当用户报此类「只在特定环境中出现的」问题时,因为咱们通常都没法重现,开发人员首先会以为很是奇怪。但奇怪自己并解决了不了任何问题。当客户端技术人员在排查完以后宣称代码没有问题的时候,极可能他并不了解用户侧真正发生的状况,而他得出的结论也只是一种「猜想」而已。用事实说话,而不是凭借主观臆断,应该是技术人员行事的基本原则。若是必定要违背这个原则才能作出论断,那咱们宁肯不作这个论断。


事情永远不可能完美,这个世界也没有完美的状态。程序在运转的过程当中也总会出错。

记得之前在一个测试技术的培训会上,有一位讲师说过,「 哪怕只碰到一次的问题,也是问题。」关键在于,咱们能认可和接受这种不完美,而不是去逃避或视而不见。要知道,工程技术的核心,就是设法让完美的逻辑模型在不完美的世界中可以畅快运行的一项技艺

只要咱们持续努力,就必定会比过去作得更好。

(完)

其它精选文章

相关文章
相关标签/搜索