我先讲一个小故事,以前在外企工作时的一个亲身经历。
当时我所在的team,负责手机上多媒体Library方面的开发。有一天,一个具有最高等级的bug被转到了我的手上。这个bug非常诡异,光是重现它就需要花很长时间。在公司内部的issue追踪系统上,测试人员描述了详尽的重现步骤,大概意思是说,用某个指定产品线的手机去播放一段《黑客帝国》的视频,大概播放到半个小时左右的时候,程序就会突然崩溃。
你可以想象,造成崩溃问题的可能原因实在太多了,比如某个局部算法的实现发生地址越界了,或者多线程执行的时序混乱了,再或者传给解码器的参数传递错了,等等,诸如此类。问题可能出在上层应用,也可能出在中间的Library,或者编解码器有问题,甚至是底下的内核或硬件不稳定(我当时所在的公司是一家手机制造商,软件和硬件都是自己设计),总之,你能想到的或想不到的都有可能。
事实上,对于这个问题的分析也是按照从上至下的顺序进行的。首先,既然播放会崩溃,那至少看起来是播放器的问题啊,OK,issue会先转给播放器的开发团队。播放器的开发团队经过分析之后发现,并不是他们的代码引发的问题,接下来他们把分析结果附在issue的处理历史上,并把issue转给下一个团队处理。那应该转给哪个团队呢?就要看上一个团队的分析结果了。他们在分析的过程中,会追踪到最终崩溃的地方是发生在他们引用的哪个代码模块中,然后就有专门的人负责找到维护相关模块的团队。就这样,这个bug从上层开始,经过层层流转,终于有一天来到了我的手上。
一个团队被分发到这样一个最高等级的bug,就意味着必须停下正在进行的一些工作,立即分出人手来处理它。这就像一个烫手的山芋,谁也不想让它在自己的团队里待上太长时间。我经过大半天的分析,终于证明了崩溃的精确位置并不在我们负责维护的代码区域里,而是在我们调用的更下层的一个模块中。OK,在系统中填上分析结果,附上分析日志,再起草一封总结邮件,我的处理工作就此愉快地结束。但是,bug依然存在!
有人会好奇,这个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的工具,而服务器的情况相对复杂一些。突然想到了胡峰同学在他的公众号「瞬息之间」上翻译过的一篇文章《认清性能问题》,写得很好,值得一读。文章对于响应时间和吞吐量的关系,以及性能拐点的描述,令人印象深刻,很有指导意义。原文地址如下: