我其实真的是后知后觉的一个人,随着岁数增加,越来越跟不上时代,很多新东西的尝试都比同行要晚很多,这是最近几天,才第一次注册airbnb,第一次通过airbnb预订房间,然后,就遇到了问题,在注册邮件验证的环节,系统报503错误, pc,手机均无法通过验证,于是,第一次尝试失败。
当然,我今天不是要吐槽airbnb的技术,但作为老程序员,我总会有个代入感,设身处地的想,如果我们遇到了,会怎样。
今天说说这个问题,一个正在线上运营的系统,如果遇到有反馈说,产品出现了bug,作为运维,作为研发,正确的应对策略和步骤,应该是怎样的。
那很多人会说,出bug第一步就去检查代码呗。
但代码哪是那么容易检查的。
一个程序员水平高低,写代码的时候你很可能看不出来,但出bug的时候,分析代码的时候,有经验有思路的,其处理效率往往比缺乏经验没思路的高一个数量级。
一般而言,一个大公司,已经上线的系统,bug应该不是说那种非常表露的,往往可能需要一定条件才会触发。
我一般的思路和建议是
第一步,评估影响范围
这个问题究竟是影响了多大范围的用户,以及什么范围的用户。这是作为线上运营人员需要非常明确的,而且对问题的解决优先级也有重要的意义。
同时,基于影响范围的判断,也对问题的可能性做了初步的排查。
那么,怎么评估影响范围?
两个方向,一是通过统计数据,二是通过日志分析。
用户反映网站或APP打开慢,打开卡,或者打开报错无法登陆。
这种属于全局问题,这种问题建议看统计数据来评估,比如当前活跃用户,日活跃用户及活跃用户的时间分布,与,比如说上周同期对比。
一个简单的实时在线对比, 可以快速定位出故障的严重程度和影响用户规模。 那么,如何进一步排查呢?
这要看你平时记录的数据全不全,比如,在当前在线或最新访问记录里,是否有地区分布,用户上网环境分布,用户客户端浏览器分布,用户手机类型和操作系统型号分布,这样可以快速做一个排查。
那么,如果不是全局性问题,比如我这次遇到的问题,是邮件验证出错,这种问题去看同时在线或访问数据就没有意义了,可以通过日志数据做快速分析,比如,最近用户邮件验证的成功率是多少,以前是多少,如果有历史数据对比,就可以知道大概是不是有较多用户出现了问题,然后,继续细分,比如说,不同邮件服务商的验证成功率多少,不同账号注册方式的邮件验证成功率多少,(比如FB注册,微信注册,或者微博注册等) 以及不同网络环境,或者客户端版本下的邮件验证成功率多少。
这样,这个问题的影响范围,就可以快速确认了。
如果以上数据全都正常,那么说明这个问题属于极个别用户在极个别环境下才会触发的问题,当然问题依然需要解决,但至少不用太焦虑。
很多人一看日志就会默认认为是错误日志,但是很多时候,用户反馈的代码逻辑问题很可能并没有体现在错误日志里,不过在airbnb这个案例里,系统反馈的是503,应该是可以从日志里看到的。
有人可能会说日志审计系统会不会很复杂,或者要不要推荐好用的日志审计系统,我个人的经验,如果我们只是针对我们的业务定制开发,这个工作其实并不复杂,而且针对性强。当然,如果有好用的日志审计系统,可以快速搭建并且能够完整的体现出业务诉求,那自然也值得推荐,但我一直都反对简单问题复杂化,有时候很简单的诉求,弄一套很复杂的日志审计系统,然后研发和运维都需要很长的学习时间和繁琐的配置流程,并且每次修改调整都需要大动干戈,那就得不偿失了,还不如自己针对业务简单写写。
第二步,试图重现问题
评估完影响范围后,根据评估的的范围特征,试图用同样的方式重现问题。
通常,问题都会在特定条件下触发,而问题就在于,你无法明确知道,这个特定条件究竟包含了哪些因素。
如果根据用户反馈的信息,以及如上基于数据分析的结论,能够明确该问题的触发条件,并实现完整的问题重现,那么对解决问题,就会非常有帮助,研发和运维就可以在测试环境里逐步调试问题。
但如果,用户反馈信息有限,以及基于数据分析的结论并不能重现问题,也就是存在不明确的触发条件,这个重现就比较困难。
这时候,我常用的方法是,在与问题有关的代码中,增加非常多的记录节点,加入更具体的追踪代码,将更具体的信息获取,并输出到日志。通过定期对这些日志的分析,找到出问题用户的日志,并基于日志深入分析问题。也就是,基于用户的真实行为的数据记录,重现和回放问题。
这里一个很重要的方法就是,跟踪定位中间数据。
我发现一些程序员修bug很没有效率,往往是缺乏一个概念,你如果得到的结果与预期不一致,你应该把程序流转中每个节点的中间数据,中间变量都拿出来看看,是不是和预期一致,才能更容易定位问题。这是定位很多程序逻辑bug的重要手段。
在校园里,我们编写程序的逻辑相对简单,输入输出之间的数据流转相对直接,中间过程相对较少。但在工作环境里,数据流转,中间过程,往往复杂的要命,所以一旦中间出错,调试的复杂度就非常高。现在流行说面向对象编程,很多数据逻辑和操作逻辑被对象封装,简化了程序员的思考复杂度,同时也提升了团队协同作战能力,以及代码的可维护性,但这里的问题是,一旦代码逻辑出错,你可能需要深入到每个对象中寻找潜在问题点,或者,在没有bug的情况下,涉及优化的问题,也需要深入到每个对象去分析性能的瓶颈点和潜在优化点。
所以,这里总结就是
尽量明确问题的触发条件,实现问题重现,在不能明确问题触发条件的情况下,在代码中增加节点跟踪,通过线上用户实际操作中的数据反馈,获得bug可能的触发条件和代码的逻辑漏洞。
一个大规模的网站和应用app,如果bug问题的影响范围并非是极个别情况,那么在实际用户操作中,就会有一定的重现率。
隔一段时间,你应该有一定的判断条件,知道日志中,哪些日志记录的是重现问题的用户,然后通过对这些日志的分析,相当于重现了问题并锁定问题所发生的具体节点。
第三,临时方案和终极方案。
这也是一个运营中常见的技术策略,如果问题符合以下两个条件
影响范围较大
难以定位,或者可以定位但解决成本很高
那么,需要尽快提供临时替代方案,或补丁方案。
比较常见的思路
临时替代方案,通过功能降级或者一定程度的减少易用性或其他产品特性,保证基本业务逻辑的顺畅,保证用户可以完整完成基本的业务诉求。这个方案与原有系统的功能模块是替换关系。 当问题影响范围非常大的时候,适合这样,但对用户的体验伤害也是很大的。
补丁方案,不替代现有方案,但针对现有遇到问题和障碍的用户,额外提供第二种选择,以功能降级或一定程度减少易用性或其他产品特性,保证基本业务逻辑的顺畅及用户完整完成业务诉求的能力。 当问题影响范围不能被忽视,但大多数用户并不受影响的时候,建议使用这样的方案,保证大部分用户的体验毫无损害,并尽可能让少部分遇到问题的用户可以平稳过渡。
但以上都是过渡方案,但有时候,有些过渡方案可以用好几年,甚至很久很久,甚至在系统已经彻底完成升级和修补后,为避免其他潜在的未知问题,补丁方案仍然静悄悄的躲在代码库的角落里,等待哪个不走运的用户去触发。
这里强调一点,我们在做解决方案的时候,要尽可能多留一个心眼,就是,永远要考虑系统异常该怎么办。
降级方案,补丁方案,事故跳转方案,类似这样的策略,也是系统设计,系统架构中需要认真思考的,这种策略也许永远不会生效,当然这样最好,但一旦生效,有可能就是救命的策略。否则,产品用户群断崖式下跌可能随时都会发生。
第四步,风险评估及持续优化
实际上很多公司都会遇到一个尴尬的问题,解决一个旧问题,带来几个新问题,这是技术演进中经常遇到的障碍,我们看这么多互联网巨头,这么多成熟的线上产品,哪个产品说,成熟度很高,不需要程序员继续维护,继续修补了?几乎没有。更多bug解读:www.yangfenzi.com/tag/bug
而且就算是当前状态下没有问题的产品,谁敢说随着使用频次,使用人群,使用场景的增加,后面就没有问题。
修补问题的时候,不要头疼医头,脚疼医脚,不要只是为了解决当前问题而思考,也要从综合层面思考。
对系统的整体是否有影响,对其他相关的功能和特性是否有影响,修补是否带来额外的负载和性能开销。
此外,对业务逻辑的梳理,如果这个业务已经属于打满了各种目的的补丁,很多补丁各种影响,那么就应该从全局思考,是否可以有更优雅,逻辑上更加顺畅的解决方案,而不是一个补丁接着一个补丁的去应付,当然,短期靠一个补丁去应对是必要的,但长期呢?
不过这事也不绝对,对于即将放弃,或者属于下坡路,只在维持状态的产品,可能打个补丁就够了,不会再考虑架构优化的事情了,对一些价值明显偏低的项目投入太多技术资源也是不合理的。所以,技术人员看这个事情的时候,也要理解公司业务层面的考量。
以上总结一下,以前也提到过,数据记录,统计数据,日志数据,越详尽,越完整,对问题分析越有帮助。
遇到难以定位的bug,如果本地难以重现,增加线上的数据日志,通过分析线上重现问题的用户的相关代码日志记录,来重现和定位问题。
懂技术之外,也要懂业务,这样在处理过程中,才能充分体现出抓大放小,优先级和技术资源的投入才能有的放矢,一些临时方案,补丁方案的选择,也必须从业务的优先级出发,用低成本高效率的方式来保障在问题结局前满足基本的用户体验。
到这里必须说一下,我反馈了airbnb的这个问题,第二天晚些时候,这个故障消失了,我也终于第一次用airbnb完成了订房。但我并不知道这个问题的影响范围是多大,触发条件是什么,也不知道他们的分析和处理逻辑是什么。 也许比我说的高明很多,也许只是上了个临时方案,谁知道呢。
————————————————————————————
吐槽时间
我个人认为,我还是尽量容忍和聆听不同的声音,但我必须承认,我很难做到完全的平和,从我这边的留言反馈,第一,反对和反面的声音我不会遮掩,一般而言,对留言的策略,只要不是太空洞,我会尽量放出来,此外,按时间来,超过100篇就对不起了,所以有些激烈的评论没有放出来,是因为微信的评论数限制和发表的时间比较靠后,我个人基本不会因为好恶决定放谁不放谁。 第二,我不会去对负面的声音做言论上的攻击。
但我讲句实话,我其实挺理解那些对评论说脏话的大V,真的,某些评论者的素质真的让人忍不住想爆粗口。
其实我觉得,如果有专业人士,提供非常好的案例或数据,来批驳我,我还是很欢迎的,毕竟,正如我常说的,分享即学习,我讲错了,有人来打脸,其实我赚到了,真的赚到了。
我这里举个例子,有个评论限于100篇我最终没放出来,但我很感谢对方提供的数据,讲双11的那篇,有个用户提到了一个数据,阿里的财报收入,和公开的淘宝+天猫营业额相比,只有不到3个百分点,是否可以理解为,流量成本并没有那么高。这其实是在反驳我所说的关于流量成本的问题。
坦白说,这个数据以前我没在意过,开始对方提到的时候我也没理解明白,但是后来想想非常有道理,虽然这里存在一些可以解释的理由,比如淘宝卖家的外部流量采购,比如虚假交易在交易额中的占比等等,但这些理由对这个数据的影响比例会有多大呢,所以一边很多商家反映流量成本过高,另一边这个数据显示其实远低于线下门店的流量成本。(店租,商场分成等)。所以这个问题我现在还在思考,但也非常感谢类似这样的反对声音。
比较烦的是那种,什么料都没有,居高临下甩几句话恶心你的。
典型类似言论如,
建议作者不要写不擅长的领域。
外行,鉴定完毕。
你不懂就不要乱写。
我也不知道对方有多专业,反正他们爽一句就跑了,好像当年我写e租宝的时候也跳出来一批,结果后来这些人也不知道跳哪里去了。
此外,还有逻辑混乱,或者完全不知道逻辑为何物的。
其实,我觉得吧,如果您想点拨我,纠正我,您只要讲的有道理,我会感谢您;如果您就是瞧不起我,看我SB,很简单,我也没求着您天天赞我赏我捧我,说的现实一点,我也没指着靠这个吃饭。
我信箱里压着一堆各种商务合作的邮件一单都不敢接我图什么呢我。
最后的最后,公开回答一个评论,云舒童鞋昨天提到了哥德尔不完备定理,这个定理表达的意思大概是,世界上存在这样的命题,不可证明,亦不可证伪。
那么这种东西我是不懂的,所以请教了一个数学学霸女粉丝。
对方表示,哥德尔不完备定理,这个描述的内容,本身是可以证明的,(补充一句,我查了一下,在百科里都有简单到证明案例)。 但描述中提到的不可证明的命题,是不可以被称作数学定理。所以数学,科学中存在很多假设,猜想。这也是科学严谨性的体现之一。
这个逻辑,不知道读者能否体会。
然后我觉得我也是自己作,学学咪蒙搞搞鸡汤,学学占豪扯扯强国梦,又吸粉又吸钱,整这跟读者折腾逻辑,回个评论都要去搜很多新名词,我特么的是不是自作自受。
【文/曹政 “caoz 的梦呓”(微信号:caozsay)】
·氧分子网(http://www.yangfenzi.com)延伸阅读:
➤ 腾讯云研发总监郑立峰:打造一款亿级APP你会碰到哪些问题?
➤ 阿里抢月饼事件五员工被开除引发社会对价值观热议|附蒋芳内部信
➤ 技术人如何才不至于虚度一生?应该持续学习、尽可能的帮助别人
难以重现的bug一般有几类。
并发相关的。并发高,负载重时,随机出现。这类bug,有可能在测试环境通过load testing 重现。加log,尽可能缩小范围,细化粒度,然后请经验丰富的同事 code review。
数据相关的。只有某些数据,甚至某种处理数据的顺序,才能触发bug。测试环境中跑mock data没用。可以把生产环境的数据拿下来,在测试环境里跑。
环境相关的。生产环境和测试环境的framework,lib,jvm,或os某个版本不同。这类是比较坑的,有时一个os的patch不同,都可能有影响。只能一点点尝试替换,是苦力活。
最好的办法,是和某个资深程序员聊天,有技巧的提起这个难题,激起他的好奇心和好胜心,然后他就会接手了。
首先应该做的就是叫上几个熟悉多线程,高并发的先审查一下代码,看是否哪个地方有多线程风险之类的。
然后,可能需要把生产环境的数据弄下来测试,关键位置用日志输出关键的信息,仔细核对日志。
最后还无望,那可能要考虑使用的平台,或者框架,甚至更底层出故障的可能性。
一般两个思路:自顶向下、自底向上。
前者是说从现象和日志中查找有用线索,缩小排查范围,逐步找出可疑点。
后者是说从代码层面分析,看看运行中覆盖了哪些边界条件,写测试用例尝试复现场景。
两者也可以结合起来使用。
定位出来之后,就赶紧还上开发时欠下的债。把这个场景的测试用例补齐吧:)
1,越诡异,越感觉复杂的Bug,越是非常弱智,非常渺小的问题导致的。
2,实在难以重现,就走白盒,2分法切除逻辑,逐步确认错误代码区间。
3,还有一种难以重现的Bug,就只能看Dump,错误信息相关记录,然后一点点推业务逻辑解决了。
讲一个故事,2014年年会上直接听到的实际案例:
洛克希德马丁家有个导弹,有时候打得准,有时候打不准,研发的小伙子们压力很大(姑娘们已经翻车不干了)。
后来有个专家说:用Coverity试试?
结果找到了两个问题:
1.精度丢失,具体的问题记不得了,要么是长度转换失败,要么是执行完毕后浮点数自动模运算成了整数。
2.大堆栈使用状况下的缓冲区溢出,那玩意儿用的芯片内存极小,控制不好溢出会直接崩掉。
修复之后搞定。
当然不是给Coverity打广告,只是提出另一个点的解决方案,除了打log之外,有时候也可以考虑看看代码中是不是有某些显而易见或者很难找到的问题 :
空指针引用:有时候路径走到了崩溃,路径没走到不崩溃。
缓冲区溢出:大内存没问题,小内存出问题。
资源泄漏:内存逐渐被占满,重启后解决,长时间后重现。
多线程问题(该加锁没加锁,加了锁没释放,竞态条件….):这事儿是最难调试的,见过太多多线程问题,十个使用有9个加了锁,1个没加锁,结果问题就出现在这个上。。。
一、找bug(bug复现)
从写入dao从低向上review到业务触发代码,特别注意线程的使用,代码里用的线程池还是新开线程,该加锁的地方是不是粒度控制有问题。
找到之后,fix掉,补充用例,回归用例。
其实这时候也是有点手心冒汗的,因为你也不确定还有多少类似隐藏的问题。总不能case by case吧
二、如何更有效的保障应用质量呢?
题主也说了,线下OK,线上不OK,线上线下环境是有区别的,如果有机制能够将线上的调用记录下来,(记录入参,出参,异常等),然后在线下将这些调用执行一遍,对出入参和异常做对比,OK?改动OK。不OK?那就再看看吧。
三、多重校验都没有发现这个问题,首先要看这个多重校验是不是必要了~,也从侧面可以排除一类问题了。
,我们好像先得把逻辑讲清楚,在考虑代码问题之前先把非代码的因素给排除掉,如运行环境的问题,如果本身环境就不支持大量并发,或者数据在过负荷传输中出现误差,这些问题非人为因素应该先确认。
然后就是回来看代码,测试下无法重现bug,有的答主认为是多线程的问题,我觉得有点片面,如果代码没有自查,那么在延迟的情况下,单单解决代码也没啥解决的啊。
一般如在机器学习中,测试用例已经开始用非常大的数字了,那么在一般商业开发中,我们α测试中选取一般量测试没发现问题,β测试出现反馈bug,提高测试量应该是正常的,然后还不能够重现,那么样本可能就存在问题了,样本在程序里过的意义是模拟现实应用的情况,那么我觉得,不可重现的原因主要还是测试样本的问题。
当然这是废话,我要是知道是什么样本出的bug,我不就修复了吗。
那么,一般对于如何找出这个样本的一般方法不就是抓异常嘛,在关键位置写入catch代码放到线上继续测试咯。
例如查重,是不是可能各终端请求ID混了,是不是延迟啊,等等,错误就log呗。
排除法
一点点排除可能引起问题的关键因素,直到确定是由某个原因引起的。
此方法适用于以前工作正常,在某个时间或条件之后才出现的问题。这个问题是什么时候开始出现的?我们把这之后添加的新功能(代码)一点点拿掉看看问题是否出现,直到定位到具体某次改动。举个例子,比如你在完成一个组装电脑之后,开机没有任何反应,此时你可以考虑从正常工作的电脑上逐步替换掉问题电脑的内存硬盘CPU等零件,直到问题消失就能定位到底是具体哪个零件出了问题。
最小环境法
在一个简单且没有过多因素干扰的环境中验证问题根源和本质。
此方法适用于比较复杂的系统和环境,和适用如果使用排除法需要排除太多东西,并且我们对该问题的起因有一定的认识的情况。比如你在调试一个程序问题的时候,出问题的部分和其他模块或子系统有错综复杂的关系,为了定位问题的本质和消除其他外部因素的影响和干扰,可以考虑新建一个简单项目,在这个没有其他因素影响的环境中重现和解决问题之后再应用到原来的项目中。对于可能由一些驱动或者硬件平台引起的问题,可以考虑在一个纯净的虚拟机环境中重现从而定位你的问题。
使用日志和断点
这个是调试程序问题的惯用方法,对此不打算详细论述。主要就是在怀疑可能出问题的地方设置断点或者打印出相关变量的值,以便于确定从哪儿开始,程序已经出现了错误了,从而定位到一开始出错的地方加以修正。
还有楼上有人回答的自定而下的方法:
就是把出问题的系统或者模块划分成几个部分,把可能的解决方案列一一列出来,针对每个部分逐步验证。在尝试的时候应该把确定不能解决问题的方案以及和问题无关的部分从列表中删除,不再尝试。对于确定问题所在的部分继续逐步划分,直到定位问题的根源和找到解
一个英国的驱动高手(56岁的老头),有一次客户报bug说在一家医院里他们的一个USB设备就是没法在新一代电脑上用,而且是时好时坏,没有规律可言。客户派了硬件软件BIOS几个工程师过去解都没解出来。我请到这位高手出马,高手拿到USB设备和电脑后,花了一天的时间做初步验证,然后花了一个晚上写了个小程序模拟那个USB设备的驱动的挂起/卸载,然后花了2小时运行那个程序,发现USB设备的驱动在挂起/卸载很多次以后Windows就会有一定的几率拒绝挂载这个设备,然后他在和微软以及那个设备的厂商联系,花了一天时间确定USB设备的驱动里的一个bug,第三天,USB设备的厂商出了一个测试驱动,问题完美解决。
粗心的硬件工程师
某台式机机型,出厂后大约一年后,开始出现大批量的返厂。返厂的现象惊人一致:主板挂了无法开机,挂掉的是主板上的芯片组。 由于该机型芯片组损坏率远高于其他机型,我们一开始怀疑是客户使用不当(因为最早返回的都是某一特定大客户的在某国工厂退回来的机器),但随着时间的推移,全世界各地都有返厂的现象。于是目标转向怀疑该主板设计有问题。但是,妖异的来了,从工厂抽调数百台该机型进行压力测试(没日没夜跑高压程序,进烤箱,各种设备插拔),就是没有一台机器挂掉。前前后后折腾了一个月,大家都丧气了。最后,好不容易,从客户手中拿到一些坏掉的机器,我们把芯片组拆下,重新植球,检测后发现是芯片里有一部分电路已经损坏,多块芯片组损坏的地方完全一致。最后,我们去检查该主板的设计,发现芯片组的某个输入电源,应该使用1.5V,却使用了3.3V电压。由于芯片组本身质量不错,尽管设计需要1.5V但是在3.3V下仍旧可以忍辱负重工作很久才烧掉,所以这也就解释了为什么我们无论怎么压力测试都测不出来,要等机器在客户手里使用一年左右才会烧掉。 最后,全球召回更换主板,问题解决。
奇异的环境
某台式机墨西哥工厂生产线,操作系统有时会无法download(这边普及一下知识,生产线上的操作系统不是安装的,都是通过网络直接把image下载到硬盘)。该问题在其他国家的产线上完全无法复制。经过多个工厂严格的对比检验(工程师飞来飞去好几个星期),发现唯一的区别是墨西哥工厂生产线上的网络环境使用的是hub(有点穷),而其他工厂使用的是switch(别的富裕一点)。进一步debug发现,台式机自带的网卡的驱动,在用hub的环境下会丢包导致image download失败,而在switch环境下就不会。通过网卡厂商驱动程序工程师debug之后,更新了驱动,问题解决。
躺着都中枪
某机型,量产后稳定发售大半年,一切OK,用户反映良好,销售订单不断,大家都很开心。不料,从某个月起,返厂的数量逐渐增加,故障完全相同:主板上供电模块烧毁。由于该供电模块应用在非常多的机型中,只有这一个机型有大量返厂,于是怀疑供应商的供货质量管控不佳,某一批次产品质量有问题。供应商被challenge的狗血淋头,使用各种手段验证,得出结论是产品质量控制没问题。我们当然不信,继续challenge供应商,逼得大家鸡飞狗跳。后来,随着时间推移,返厂的机器越来越多,损坏的供电模块广泛散布于各个批次之间,这下供应商抬起头来:总不见得我每批货都有问题吧!好,只能继续debug,供应商对模块进行检测后,告知损坏的原因是长期超负荷运行导致,基本上超负荷连续运行超过3个月,模块就会损坏。我们很奇怪,电路设计各方面都符合标准,为啥模块会超负荷运行?于是,开始各方面排查,经过两个月的仔细研究,把每台返厂的机器的所有config都拉出来做对比,终于得出一个共同点:所有损坏的机器,都是从某年某月某日开始使用了某个新的显卡驱动之后才发生的。神了,显卡驱动能烧毁供电模块?这时候,工藤新一开始附体:如果排除所有可能,剩下最后一个可能,即使它是多么的不可思议,也一定就是真相! 好吧,继续研究,发现这一版的显卡驱动,在调配显卡功耗的时候,会在供电模块的输出电路上产生一个尖峰(电压高达额定电压的十倍以上),尽管这个尖峰时间很短(以微秒计),但由于驱动会不断的调整功耗,导致这个尖峰会极其频繁的出现。所以,如果电脑是24小时开机并一直在运行高压程序的话,这个尖峰的持续出现会导致供电模块的损坏。后来,修正显卡驱动,问题解决。供电模块的供应商感慨道:这真是躺着都中枪啊!
====解释的分割线======有人提到没事不要更新驱动,其实这个有点草木皆兵的味道了。该更新的时候还是要更新的,毕竟新的驱动很多时候是解了不少bug以及提升不少性能的。这个bug当时存在的原因是该显卡第一次内置动态功耗调整,所以驱动开发人员经验不足。后续的产品在驱动开发的时候都会配合硬件工程师一起做测试来保证不出现类似的bug。
不差钱的土豪也有倒霉的时候
某机型,量产后稳定发售半年有余,忽然从某个月开始,产线上不良率骤然提升,具体表现在某个出厂测试无法通过,不良品广泛散布于各条生产线各种配置上,毫无规律可言。工厂端仔细检查后确认近期没有引入任何新的物料,也没有更改任何生产步骤,产线上的工人也没有任何变动,于是将问题上报。研发人员介入后发现,似乎问题是跟着PCB板走的,因为在不良品上即使更换处理器芯片组,不良品依旧是不良品,而当你把不良品上的处理器芯片组换到良好的产品上,测试就通过了。于是,矛头指向PCB制造厂,怀疑他们品控不良。PCB厂经过两周的排查,确定他们出厂的PCB品控良好,并且他们对不良PCB做过检验,各项指标都在标准范围之内。这下研发人员抓瞎了,问题被推送到我这里。不巧的是,正好没几天就农历新年了,我只好要求工厂把不良品寄给我,我再寄给美国总部的工程师帮助debug(老美不过新年也有好处啊)。随后,工厂停产过年,等待年后复产。大家开开心心过年回来后,老美的报告出来了,经过不良品和良品的检验,发现不良品上处理器的某个控制电压超出范围,导致处理器工作不正常。研发人员进一步检查后发现,原来在那个控制电压上,研发过程中曾经为了debug的需要,串联了一颗 0 欧姆的电阻,而那个电压就是在通过了 0 欧姆的电阻后,压降超出范围导致的问题。随后,经过对不良品上的 0 欧姆电阻检测发现,该0 欧姆电阻的阻值远超spec,原来是电阻厂品控不良啊! 研发通报了该事故给采购,采购立即将该电阻厂除名,问题解决。
解释一下,开发过程中,电路上经常会预留一些0 欧姆的电阻,可以作为监测点来debug。通常来说,量产后,这些0欧姆的电阻都应该去除,改为直连,以节省成本。有趣的是,当初该客户正好处于不差钱的状态,又十分保守,决定在研发结束到量产的过程中,所有电路一条线都不能改,于是这些用于debug的0 欧姆电阻依旧被保留着,直到该问题被发现。
强势的事实垄断者
前面写了很多量产后的bug,这里写一个还未量产的bug吧,其实这个bug的核心不在于技术问题,而是钱的问题。
某机型,研发过程中的小批量生产的时候一切正常,研发进行的异常顺利。大家都很开心,准备等8月份宣布量产后开香槟庆祝。
6月下旬,按研发流程,工厂开始了量产前最后一批大规模试产,同时对产线进行最后的调试。6月底,工厂开始报问题,这批试产的机型大约会出现千分之三左右的花屏现象,现象完全随机出现,经过数次排查,无法确认花屏出现的规律。咣当!大家都懵了!没有规律的bug是最头疼的,何况现在接近产品发布日期,如果在发布之前无法解决,产品上市就会延期,大家都死啦死啦滴。得,工程师全部驻厂,没日没夜debug,硬件软件固件工程师就在产线上趴着,逮到一台花屏机就开始debug。幸运的是,在产品发布会延期的巨大压力下,工程师被逼出了无穷潜能,居然最后debug出来了。原因很简单:屏幕控制芯片的一个时序不符合规范,有个信号理论上延时不得大于30ms,实测值什么样的都有,最大的能跑到300ms,翻了10倍。大家长出一口气:行了,替死鬼找到了,有人担纲了。
万万没想到的是,当工程师找到屏幕控制芯片的厂商的时候,他们一脸轻松:我知道啊,这个时序我的确没符合规范!工程师当场跳起来了:靠我去年买了个表,你知道你怎么不早说!有毛病的片子你敢出厂!看我不告死你!厂商一脸傲娇:没办法,这个时序如果要控制在规范内,我自己没法量产,因为良率不达标!大家大怒:TMD良率不达标关我屁事,你的片子卖给我你就得达标!不达标我就告你!厂商说:你丫懂不懂,这个规范不知道那个脑子进水的人定的,你要我时序符合规范,可以,良率降4成,我的价格要翻倍!你出双倍价格我就给你达标的片子! 七嘴八舌吵了一个多小时,大家一拍桌子不欢而散。
工程师回来找到采购:TMD采购你怎么搞得,这种厂商你也放进来!采购一脸无辜:你知道么?!这是目前市面上唯一能提供你需要参数的厂商!只此一家,别无分店!你要我换厂商,可以,你把你的参数改了! 工程师说:靠难道赚钱的活没人干?这片子又不是什么高精尖造火箭!采购说:别的厂商有,但人家要年底才能出货,你能忍?
工程师没办法,回去找大老板:老大,这活没法干了!
老大了解了来龙去脉,嘿嘿一笑:能用钱解决的问题就不是问题。采购,你去把第二家厂商拖进来,让他们年底开始供货,然后去跟第一家谈,我出双倍价,让他们供货,等到了年底第二家上来了我们再去治治第一家。工程师,你现在就去跟第二家开始合作,测试他们的片子,确保年底能供上。
最后问题解决了,靠的不是技术,是砸钱,砸钱,砸钱。。。。。。。。。
这是前年的bug了,不过今天偶然想起来了,顺便更新一下。
话说现在4G越来越普及速度越来越快资费越来越低,平板电脑和笔记本电脑也有很多都自带3G 4G 网卡了(俗称WWAN)。厂商都不傻,早早的就开始了研发工作。所以,前年的一半左右时间,我都深陷WWAN bug之中无法脱身。其中有一个,印象极其深刻。
话说这个WWAN吧,测试是个麻烦事,实验室测的真的不太管用,在实验室里测试结果很漂亮的机器,拿到公共场合经常各种毛病,不同运营商,不同地点等等都会影响测试结果。当然,这是普通平板电脑和笔记本电脑首次内置WWAN,各大厂商基本都没啥经验。不比华为这类有着深厚功底的公司,3G上网卡早就做的不要不要了。这不,一个bug就找上门来了。简单点来说,就是WWAN时好时坏,在实验室里啥毛病没有,到了公众场合测试就会时不时连不上啊,飞行模式切过去再切回来WWAN就起不来了啊之类的,非常影响用户体验。更加倒霉的是,debug过程中牵扯到本公司数个部门,关键员工广泛分布于印度美国波兰德国中国台湾等各地,每次开会要凑齐所有相关人员简直痛苦的一塌糊涂。长话短说,debug了三个月之后,在大家都精疲力尽的情况下,终于矛头指向了某软的USB驱动。但因为没人有他们的代码,debug只能靠不断的猜测,各种尝试来试图逼近结果。
此话一出,相关领导全部表示不相信:开啥玩笑,某软的USB驱动存在已有接近10年历史,bug早就被扒的干干净净。。。。小伙你是想偷懒吧?
(此处插播一个传言:传说中,如果你向某软报bug,最终查出来如果真的是他们的bug,那么不收钱。否则,他们会按照技术支援人员在你这个bug上耗费的时间来收钱,貌似每人每小时要收一百多美金,如果由于bug级别提升而需要他们的人员周末加班那就会上升到数百美金每小时,如果再要他们的大牛出马的话就更没边了。由于此传言过于奇葩,据说没人敢于真正尝试随便向某软报bug,都是把bug查到100%有把握是某软的问题之后才敢上报。)
话说,众多兄弟,数十人三个月日以继夜的debug已经精疲力竭,纷纷向领导表示:你不报给某软咱就不干了。领导被逼无奈:报吧,真要花钱就花吧,这么多兄弟花这么多时间,也都是钱啊。。。。。。
bug报上后,把问题机型寄到某软总部,某软指派工程师查看一周后,回复:没错,这是个bug,我们7月份的更新已经包括了这个bug的解决办法,你们等最新的推送就好了。
大家都已经等了3个月了,实在等不起,求爷爷告奶奶,某软终于答应给一份内部测试版的更新让大家尝试。更新包打上之后,WWAN稳定的不要不要的。。。。。。。。。
事后,众人算账,就算某软某工程师一周每天8小时查看这个bug,也就40小时而已,即使被某软收钱,也就几千美金最多万把块美金的事情。我们这里前前后后各个厂家扯进去的工程师都快50人了(包括测试人员),三个月的工作量,怎么都不止几千美金。。。。。
一切的一切,都是刚开始的时候不舍得花钱,于是窟窿越来越大
那是我在前东家所参与的第一个重点的芯片设计项目。那个项目之初的设计目标就是低功耗,当时市面上竞争对手的最低休眠功耗在1.1V电压下可以做到1uA,但是他们用了较为旧的工艺,所以芯片面积大。而我们选择了更新的工艺,面积可以减小一半多,但是由于新工艺的漏电流较大,经过初期的估算,我们的目标是接近1uA。最后在前端设计完成后,用PTPX后仿真的结果就是1.5uA。老大觉得达到了预期,于是就送出去流片了。
芯片回来后bring up,在基本功能正常, 无线收发的指标都达到预期之后,组里的老板,director,甚至部门的VP就关心一个东西:休眠状态下的静态功耗。当我们满怀期待地接上电流表之后,电流表稳定在了6uA,是预期值的整整4倍,足足大了4.5uA!换了5块板子,都是这个数,换了socket 板子,再测了三四颗芯片,都没有比6uA低的。这可要了命了,我们的芯片主打就是面积小低功耗,现在这功耗简直被竞争对手吊打啊,director下了死命令,必须在下一次流片之前把问题找出来,而下一次流片就是量产流片了。
组里所有人开始行动起来开始找原因。我并不是always on模块的设计者,但是是我跑的PTPX仿真,我于是先又跑了一遍仿真,重新检查了各种设置,没有发现任何问题,仿真就是告诉我是1.5uA。同时跑其他非休眠状态的仿真,仿真功耗值和实测值可以对得上,证明设置是正确的,工艺库的值也是准确的。
接下来怀疑是板子上哪里有漏电或者是板子设计的问题,但是经过将近一周的重新排查,PCB板设计并没有问题,那么就是芯片内部的问题了。
always on模块的设计者也和大家重新review了design,各种猜想被提出,但是又各种被实测否定。比如说,猜测某个isolation cell没有插入对,于是我们和模拟团队坐在一起,把上千个信号又一起review了一遍,最后发现都是正确的。类似这样的头脑风暴几乎每天都在进行,每次大家满怀希望以为找到一个合理解释,但是最终都是失望而归。
这里要和大家讲述一下silicon debug的难度。 软件debug通常有日志,有调试器的帮助,单步运行足以找出绝大多数bug。但是小小的芯片就难了,封装好的芯片还没有小拇指甲盖大,上面的引脚比头发丝也粗不了多少,GPIO一共也就二三十个,每个IO后面能mux出来的信号也是有限的几个,要是动态的信号好歹可以利用示波器和逻辑分析仪看波形,但是这种静态功耗的debug,示波器和逻辑分析仪也只能抓瞎。我们测算过,4.5uA说大也大,说小也很小,一个transistor短路就足以引起这么大的short circuit current,可是小小芯片里面几千万上亿个transistor,找出这个无异于大海捞针。
大海捞针也得捞啊,在我们大家都想不出其他解释之后,老大批准了给芯片照热成像和FIB。给芯片照热成像是啥意思,因为芯片在运行的时候是有功耗,那么在有电流流过的地方就会有功耗,那么相应的温度就比周边稍稍高一点点,这个时候用专用的热成像仪照一个照片,只要看到哪一个地方比其他地方红一些,那么就大概可以定位出功耗大的区域,然后再在layout上找到那片区域对应的模块。
但是照回来的结果是,热成像分布非常均匀,完全看不出有哪个地方过热。想想也是,4.5uA的电流,换算成功率也就是几uW,太难了。
然后又在几个怀疑的点做了FIB,简单说就是把封装打开,在die上面重新用仪器去把metal wire断开,这个因为难度高,做一次就要好几千美金,我们前后做了好几次,每次都是没有效果,上万美金就这样打了水漂。真是心疼啊,我们几个年终奖加起来也不到这个数。
这样大概折腾了2个月,下至工程师,上至director,我们都觉得要放弃了。只能猜测是厂家工艺的原因,随着量产芯片tape out日期的临近,我们只能寄希望于下一批次回来的芯片能够功耗第一些。
直到有一天,我和另一个项目的后端工程师一起开会,review placement 和 critical timing path时,他给我们看了当前的placement, 我发现standard cell之间还有一些空位,就像下面这个图, 所有的单元应该是按行摆放,但是中间会有空位。
于是问他们这些空位要怎么处理,他们说要放decap cell 和filler cell. 我之前知道filler cell是用来填满空隙的, 可以去平衡metal density,可是decap cell是个什么东东?这玩意有功耗吗?开完会立马打开lib文件一查,我擦,decap cell这玩意居然是有leakge power的!而且x2, x4, x8 (就是不同大小)的功耗也是不一样。 我隐约觉得自己发现金矿了!
于是检查后端给我们的netlist, 没有filler cell, 没有decap cell!难怪前端功耗仿真偏小,因为这些cell压根就不存在。在后端给前端返回netlist的时候,因为decap cell和filler cell属于没有任何功能,没有任何connection的cell, 他们通常的做法是在写出netlist的时候不包括这些cell。 于是我们重新让后端写出带这些cell的netlist, 再跑功耗仿真,非常准的6uA, 问题解决!
进一步分析表明,绝大多数的decap cell被放置在了always on的区域。这里面插一句,现代SoC低功耗设计的一个常用方法是将芯片划分成不同的power domain,有的power domain可以开可以关。在关闭的时候连power都关掉,这样连漏电流就都没有了,是最省功耗的做法。但是芯片睡眠后必须有一部分电路还是工作的,准备随时唤醒整个芯片,这一部分always on的电路因为power rail不同,在版图上通常是划定一个特定的区域。我们的芯片由于always on逻辑很少,这片区域function cell比较稀疏,而后端在放置decap cell的时候是全局铺放,当tool看见这部分空位比较多的时候,将大部分的decap cell都放在了always on区域里,这样引入了多余的4.5uA功耗。
这个bug坑的地方在于,前端拿到的netlist里没有decap cell,而我们前期也压根没有往这方面想,花了大量时间去检查看function上是不是有问题。
手机版app好像没法设置引用,不过 whatever
这是Dave Baggett发表在Quora上一篇The hardest bug you’ve ever debugged,读起来让人十分惊叹。(如果是我的话,在代码中找不到可能就直接放弃了。。。)
回想起这个bug,仍然让我有些痛苦。作为一个程序员,在发现bug时,你学会了首先在自己代码中找问题,或许在测试一万次之后,你会把问题归咎于编译器。只有在这所有的都不起作用之后,你才会把问题归咎于硬件。
这是我遭遇一个硬件bug的故事。
抛开别的不说,我曾为《Crash Bandicoot》写存储卡(读写)代码。对于一个自大的游戏程序员,这就像是在公园里散步一样轻松愉快,我认为只要几天就写完了。我中止调试六个礼拜。在此期间我做一些其他的事情,但我一直回来处理这个bug——几天内每天几个小时。这个bug实在烦人。
这个bug的症状是,当你需要保存你的进度时,代码会访问存储卡,而大部分情况下没有什么问题…但是偶尔读写会超时…没有任何明显的原因。一个短小的写入经常毁掉存储卡。玩家要保存进度,我们不仅不保存,还擦除他们存储卡上的全部东西。天哪。
过了一段时间,我们在Sony的制作人Connie Booth慌了。我们显然不能带着这个bug发布游戏,而六个星期之后我对于问题出在哪一点线索都没有。通过Connie我们向其他 PS1 开发者求助:有没有人出现过像我们这样的情况?没有。绝对没有任何人在存储卡系统上出现任何问题。
在你绞尽脑汁之后,你能做的唯一一个调试方法就是分而治之:一点点去除程序中的代码,直到留下的代码很少但你仍然出问题。像木雕一样去除没有问题的代码,留下的就是你的bug所在。
在这样的背景下挑战在于,视频游戏是很难去除某一部分的。在你删除模拟重力或者显示字符的代码后,如何运行游戏?
你必须做的是用一个假装做真正的事情,但实际上只是做很简单的不会出现bug事情的东西来替换掉整个模块。你必须写新的支撑代码来让这些玩意正常工作。这是一个缓慢而痛苦的过程。
长话短说:我做完了。我移除了大片大片的代码,相当多,只留下了初始化代码——就是准备游戏运行系统,初始化底层硬件等等。当然,我不能显示加载/保存菜单,因为我截除了所有的图像代码。但是我能够假装用户使用(不可见的)加载/保存屏幕并且请求保存,然后写入卡中。
我最终以一个带有这个bug的很少量的代码结束——但问题仍然随机出现!在大多数情况下没啥问题,但是偶尔会失效。基本上所有的Crash的实际代码都被移除了,但还是这样。这实在是莫名其妙:留下来的代码基本上都没做什么事。
在那时——估计是凌晨3点——一个想法蹦了出来。读写(I/O)涉及精确定时。无论是硬盘、存储卡、蓝牙发送器——随便啥——做读写的底层代码都是根据时钟来的。
时钟让不直接连接到CPU的硬件设备和cpu运行的代码同步。时钟决定了波特率——数据从一头传到另一头的速率。如果计时有什么问题,硬件或者软件或者两者都会乱七八糟的。这真的,真的很糟糕,并且通常导致数据损坏。
如果我们的初始化代码以某种方式弄乱了计时会怎么样?我又看了一遍测试程序中和计时有关的代码,并注意到我们将PS1上的可编程计时器设置到了1kHz(1000跳每秒)。这是比较快了,当PS1启动的时候,默认状态大概是100Hz。因此,大多数游戏将他们的计时器设置为100Hz。
这个游戏的带头(和除我外的唯一)开发者Andy,将计时器设置为1kHz,使得Crash的动作计算更加准确。Andy喜欢矫枉过正,如果我们要模拟重力,我们应该尽可能的提高精度!
然而如果提高计时器频率莫名其妙的干扰了整个程序的计时,故而将这个计时器设置到存储卡的波特率上会怎样呢?
我将计时器代码注释掉。然后我就无法复原这个bug了。但是这并不表示bug被修复了,这个问题是随机发生的。万一我只是运气好呢?
几天过去了,我还是在玩我的测试程序。Bug没有再出现。我回到全部的Crash代码中,修改了加载/保存代码,在访问存储卡之前将可编程计时器重置为默认设置(100Hz),之后设置回1kHz。从此之后没有发现问题再次出现。
但是…为什么?
我重新回到测试程序上,试着检测当计时器设置为1kHz时出现的那些错误的模式。终于,我注意到这些错误出现在使用PS1手柄的人身上。因为我自己很少这样做,所以我没有注意到(为啥我要在测试加载/保存代码的时候用手柄)。但是有一天我们的美工等我去完成测试(我确定那时候我在爆粗口),而他紧张的摆弄着手柄。卡损坏了。“等下,怎么回事?喂,再来一次!”
一旦我发现了这两件事是联系着的,就很容易重现bug:开始写入存储卡,动一下手柄,存储卡损坏。在我看来完全是硬件bug。
我去找Connie告诉他我的发现。她转述给设计过PS1的硬件工程师。她被告知:“不可能,这不可能是硬件问题。”我跟她说问一下我能不能直接和他说。
那个工程师给我打电话了,他用着他的烂英语,我用着我更烂的日语,我们争论一会。我最后说:“我给你一个30行的测试程序,让你在动手柄的时候能够出现这问题。”他答应了。他向我保证,这是浪费时间,而他正在一个新项目上很忙,但因为我们是Sony很重要的开发者,他会试的。
第二天晚上(我们在洛杉矶,而他在东京,所以对于我来说是晚上而他是到了第二天),他给我打电话,不好意思的向我道歉。这是个硬件问题。
我还是没有完全搞清楚问题到底在哪,但是我的印象中,从Sony总部的反馈听到的是,如果将可编程计时器设置到足够高的时钟频率,会影响到主板上时钟晶振附近的一些东西。这些东西之一就是存储卡的波特率控制器,同时也设置手柄的波特率。我不是搞硬件的,所以对于细节我相当模糊。
但是主旨是主板上两个独立部分的串扰,以及手柄接口和存储卡接口数据发送的结合在1kHz的时钟频率下会导致丢位,从而数据丢失,以致卡损坏。
这是我全部编程生涯中,唯一一次因为量子力学debug的问题。
手机版app好像没法设置引用,不过 whatever
这是Dave Baggett发表在Quora上一篇The hardest bug you’ve ever debugged,读起来让人十分惊叹。(如果是我的话,在代码中找不到可能就直接放弃了。。。)
回想起这个bug,仍然让我有些痛苦。作为一个程序员,在发现bug时,你学会了首先在自己代码中找问题,或许在测试一万次之后,你会把问题归咎于编译器。只有在这所有的都不起作用之后,你才会把问题归咎于硬件。
这是我遭遇一个硬件bug的故事。
抛开别的不说,我曾为《Crash Bandicoot》写存储卡(读写)代码。对于一个自大的游戏程序员,这就像是在公园里散步一样轻松愉快,我认为只要几天就写完了。我中止调试六个礼拜。在此期间我做一些其他的事情,但我一直回来处理这个bug——几天内每天几个小时。这个bug实在烦人。
这个bug的症状是,当你需要保存你的进度时,代码会访问存储卡,而大部分情况下没有什么问题…但是偶尔读写会超时…没有任何明显的原因。一个短小的写入经常毁掉存储卡。玩家要保存进度,我们不仅不保存,还擦除他们存储卡上的全部东西。天哪。
过了一段时间,我们在Sony的制作人Connie Booth慌了。我们显然不能带着这个bug发布游戏,而六个星期之后我对于问题出在哪一点线索都没有。通过Connie我们向其他 PS1 开发者求助:有没有人出现过像我们这样的情况?没有。绝对没有任何人在存储卡系统上出现任何问题。
在你绞尽脑汁之后,你能做的唯一一个调试方法就是分而治之:一点点去除程序中的代码,直到留下的代码很少但你仍然出问题。像木雕一样去除没有问题的代码,留下的就是你的bug所在。
在这样的背景下挑战在于,视频游戏是很难去除某一部分的。在你删除模拟重力或者显示字符的代码后,如何运行游戏?
你必须做的是用一个假装做真正的事情,但实际上只是做很简单的不会出现bug事情的东西来替换掉整个模块。你必须写新的支撑代码来让这些玩意正常工作。这是一个缓慢而痛苦的过程。
长话短说:我做完了。我移除了大片大片的代码,相当多,只留下了初始化代码——就是准备游戏运行系统,初始化底层硬件等等。当然,我不能显示加载/保存菜单,因为我截除了所有的图像代码。但是我能够假装用户使用(不可见的)加载/保存屏幕并且请求保存,然后写入卡中。
我最终以一个带有这个bug的很少量的代码结束——但问题仍然随机出现!在大多数情况下没啥问题,但是偶尔会失效。基本上所有的Crash的实际代码都被移除了,但还是这样。这实在是莫名其妙:留下来的代码基本上都没做什么事。
在那时——估计是凌晨3点——一个想法蹦了出来。读写(I/O)涉及精确定时。无论是硬盘、存储卡、蓝牙发送器——随便啥——做读写的底层代码都是根据时钟来的。
时钟让不直接连接到CPU的硬件设备和cpu运行的代码同步。时钟决定了波特率——数据从一头传到另一头的速率。如果计时有什么问题,硬件或者软件或者两者都会乱七八糟的。这真的,真的很糟糕,并且通常导致数据损坏。
如果我们的初始化代码以某种方式弄乱了计时会怎么样?我又看了一遍测试程序中和计时有关的代码,并注意到我们将PS1上的可编程计时器设置到了1kHz(1000跳每秒)。这是比较快了,当PS1启动的时候,默认状态大概是100Hz。因此,大多数游戏将他们的计时器设置为100Hz。
这个游戏的带头(和除我外的唯一)开发者Andy,将计时器设置为1kHz,使得Crash的动作计算更加准确。Andy喜欢矫枉过正,如果我们要模拟重力,我们应该尽可能的提高精度!
然而如果提高计时器频率莫名其妙的干扰了整个程序的计时,故而将这个计时器设置到存储卡的波特率上会怎样呢?
我将计时器代码注释掉。然后我就无法复原这个bug了。但是这并不表示bug被修复了,这个问题是随机发生的。万一我只是运气好呢?
几天过去了,我还是在玩我的测试程序。Bug没有再出现。我回到全部的Crash代码中,修改了加载/保存代码,在访问存储卡之前将可编程计时器重置为默认设置(100Hz),之后设置回1kHz。从此之后没有发现问题再次出现。
但是…为什么?
我重新回到测试程序上,试着检测当计时器设置为1kHz时出现的那些错误的模式。终于,我注意到这些错误出现在使用PS1手柄的人身上。因为我自己很少这样做,所以我没有注意到(为啥我要在测试加载/保存代码的时候用手柄)。但是有一天我们的美工等我去完成测试(我确定那时候我在爆粗口),而他紧张的摆弄着手柄。卡损坏了。“等下,怎么回事?喂,再来一次!”
一旦我发现了这两件事是联系着的,就很容易重现bug:开始写入存储卡,动一下手柄,存储卡损坏。在我看来完全是硬件bug。
我去找Connie告诉他我的发现。她转述给设计过PS1的硬件工程师。她被告知:“不可能,这不可能是硬件问题。”我跟她说问一下我能不能直接和他说。
那个工程师给我打电话了,他用着他的烂英语,我用着我更烂的日语,我们争论一会。我最后说:“我给你一个30行的测试程序,让你在动手柄的时候能够出现这问题。”他答应了。他向我保证,这是浪费时间,而他正在一个新项目上很忙,但因为我们是Sony很重要的开发者,他会试的。
第二天晚上(我们在洛杉矶,而他在东京,所以对于我来说是晚上而他是到了第二天),他给我打电话,不好意思的向我道歉。这是个硬件问题。
我还是没有完全搞清楚问题到底在哪,但是我的印象中,从Sony总部的反馈听到的是,如果将可编程计时器设置到足够高的时钟频率,会影响到主板上时钟晶振附近的一些东西。这些东西之一就是存储卡的波特率控制器,同时也设置手柄的波特率。我不是搞硬件的,所以对于细节我相当模糊。
但是主旨是主板上两个独立部分的串扰,以及手柄接口和存储卡接口数据发送的结合在1kHz的时钟频率下会导致丢位,从而数据丢失,以致卡损坏。
这是我全部编程生涯中,唯一一次因为量子力学debug的问题。
一批试制PCB,收到后开始手焊。
第一块焊好,烧程序,一切正常。
第二块焊好,烧程序,不正常,后确认是电源问题,飞线、换件……一时无法解决。
狠心(狠心的原因是器件不便宜,焊上去不能用再吹下来也不保险了)焊好第三块,烧程序,不正常,与第二块差不多。
软件组不耽误事,反复调第一块板子就好了,但硬件组很蛋疼。
某日,终于决定一根线一根线地量,翻出发给厂家的PCB图,一打开就发现一个特不自然的地方:电源浮地。马上测量,证实了这一点。
板子需要重做是没啥问题的了,程序倒也已经调了个差不多,基本上没耽误。所以在等待新板子的过程中,我们思考了两件事:
第一,为啥电源还浮地,就把这图发出去了——以那时的认知来看,只有这一处问题。
第二,为啥第一块板子能用。
所以,我认为最难调试修复的bug是:一次成功,但正常运行的是因为有bug,不正常的才是正常的。
大一时候参加ABU Robocon机器人大赛,我不懂机械也不懂电子,只会写写C++,于是就负责编写机器视觉的闭环控制程序,大部分时间做图像处理,小部分时间调用串口驱动电机。
结果有一天机器人突然一动不动了。赶紧改代码,改了一会儿。
正当我怀疑是不是把板子烧了的时候,F5一跑发现,机器人居然又能动了!以为修复了bug,赶紧保存。
继续写程序不到半个小时,机器人再次罢工。我又以为自己把刚才修好的bug又触发了。再赶紧回头改。
如此,在“我操(四声)”和“我操(二声)”中不断往复。调bug大家都懂的,吃饭睡觉都得尽量避免,必须得通宵才有感觉。
当折腾到第三天我人都快熬死了的时候,做硬件的师兄过来发现,有块电容他忘了接地了。。。于是电容充满电,机器人就罢工。断电呆半个小时,等这块儿质量不怎么好的电容放干净电,就可以继续正常调试了。。。
当年在IBM 的CellBE上 (就是PS3上那块)做了不少图像处理、视频编解码的工作。这个奇葩的异构体系结构有如下几个特性:
虽然号称一个芯片上有9个核,但是只有一个是通用核,普通程序只能在这个核上跑,没法和x86一样直接多线程!
另外的8个运算核只有一种模式那就是128位的向量运算,只能跑一些及其精简的指令,需要在C程序中嵌入伪汇编来编程。啊?加减乘除运算符?在运算核上放多了那个会被开除吧!
通用核与运算核并不共享内存空间, 他们之间所有的数据共享和同步都必须完全手工管理,想要传数据得自己调用硬件DMA,然后自己同步!
因为代码全部展开能最大增加优化潜力,基本一切函数都被变成了宏!
要想发挥性能一定把程序并行、向量化。用trick,gotcha一堆!人能看懂的程序都不是好程序!
8个运算核上跑的程序不能用gdb debug,唯一靠谱的调试手段就是printf,但是因为运算核上printf也是通过DMA跑去通用核打印到终端,所以不同核之间的printf并不能保证同步!
只有terminal access都不算问题了,至少还有vim!
这一年我需要在上面从头写一个MJPEG编解码器。视频编码过的二进制流你们见过的!!!大年三十在家加班debug看dump出来的二进制流再调这种坑爹的程序你们脑补下!!!
不行了我一定要贴段代码恶心下你们,哼!!
这段566行气势恢宏的代码有且只有一个功能,那就是把rgba转成了yuv!!!!
一批试制PCB,收到后开始手焊。
第一块焊好,烧程序,一切正常。
第二块焊好,烧程序,不正常,后确认是电源问题,飞线、换件……一时无法解决。
狠心(狠心的原因是器件不便宜,焊上去不能用再吹下来也不保险了)焊好第三块,烧程序,不正常,与第二块差不多。
软件组不耽误事,反复调第一块板子就好了,但硬件组很蛋疼。
某日,终于决定一根线一根线地量,翻出发给厂家的PCB图,一打开就发现一个特不自然的地方:电源浮地。马上测量,证实了这一点。
板子需要重做是没啥问题的了,程序倒也已经调了个差不多,基本上没耽误。所以在等待新板子的过程中,我们思考了两件事:
第一,为啥电源还浮地,就把这图发出去了——以那时的认知来看,只有这一处问题。
第二,为啥第一块板子能用。
所以,我认为最难调试修复的bug是:一次成功,但正常运行的是因为有bug,不正常的才是正常的。
大一时候参加ABU Robocon机器人大赛,我不懂机械也不懂电子,只会写写C++,于是就负责编写机器视觉的闭环控制程序,大部分时间做图像处理,小部分时间调用串口驱动电机。
结果有一天机器人突然一动不动了。赶紧改代码,改了一会儿。
正当我怀疑是不是把板子烧了的时候,F5一跑发现,机器人居然又能动了!以为修复了bug,赶紧保存。
继续写程序不到半个小时,机器人再次罢工。我又以为自己把刚才修好的bug又触发了。再赶紧回头改。
如此,在“我操(四声)”和“我操(二声)”中不断往复。调bug大家都懂的,吃饭睡觉都得尽量避免,必须得通宵才有感觉。
当折腾到第三天我人都快熬死了的时候,做硬件的师兄过来发现,有块电容他忘了接地了。。。于是电容充满电,机器人就罢工。断电呆半个小时,等这块儿质量不怎么好的电容放干净电,就可以继续正常调试了。。。
当年在IBM 的CellBE上 (就是PS3上那块)做了不少图像处理、视频编解码的工作。这个奇葩的异构体系结构有如下几个特性:
虽然号称一个芯片上有9个核,但是只有一个是通用核,普通程序只能在这个核上跑,没法和x86一样直接多线程!
另外的8个运算核只有一种模式那就是128位的向量运算,只能跑一些及其精简的指令,需要在C程序中嵌入伪汇编来编程。啊?加减乘除运算符?在运算核上放多了那个会被开除吧!
通用核与运算核并不共享内存空间, 他们之间所有的数据共享和同步都必须完全手工管理,想要传数据得自己调用硬件DMA,然后自己同步!
因为代码全部展开能最大增加优化潜力,基本一切函数都被变成了宏!
要想发挥性能一定把程序并行、向量化。用trick,gotcha一堆!人能看懂的程序都不是好程序!
8个运算核上跑的程序不能用gdb debug,唯一靠谱的调试手段就是printf,但是因为运算核上printf也是通过DMA跑去通用核打印到终端,所以不同核之间的printf并不能保证同步!
只有terminal access都不算问题了,至少还有vim!
这一年我需要在上面从头写一个MJPEG编解码器。视频编码过的二进制流你们见过的!!!大年三十在家加班debug看dump出来的二进制流再调这种坑爹的程序你们脑补下!!!
不行了我一定要贴段代码恶心下你们,哼!!
这段566行气势恢宏的代码有且只有一个功能,那就是把rgba转成了yuv!!!!
最难调的 bug 是难以重现的 bug。如果找到重现方法,就好办了。
有时候,难调是因为没有源码……
以前基于 Adobe Flash Media Server 写流媒体程序的时候,碰到过一个纠结的 BUG,查了3天才再现出来,发现是 Adobe Flash Player 的 bug,提给 Adobe 后被解决了。这个 BUG 是这样的(完整版:FlashPlayer在执行NetStream.play的时候崩溃的解决办法) :
BUG表现
在使用NetStream连接FMS发布的流,并执行NetStream.play(‘streamName’)方法时,FlashPlayer会崩溃。独立版、调试版以及基于浏览器的插件版均如此。
但是,这还不是全部。必须满足以下几点,该BUG才会出现。
使用Windows 7操作系统。也就是说,Windows XP不会出现这个问题;
播放的必须是RTMP流,RTMP流可以由Flash Media Server或者Red5来发布。也就是说,使用NetStream播放本地的flv/f4v/mp4视频不会出现这个问题;
播放的流包含音频。也就是说,如果该流只包含视频,不会出现这个问题;
播放的流中包含的音频声音较大。也就是说,即使该流包含音频,但如果发布方没有发出声音,或者发出的声音很小,该问题不会出现;当然,不需要很大的声音就能让播放端立即崩溃;
使用了Frame标签来做预加载。不了解Frame标签预加载的,看这篇文章:Preloaders in AS3;
在预加载完毕之后,使用removeChild移除了预加载类的实例(BUG就在这里)。
开发和测试平台(出现BUG的平台)
Flex SDK 4.5.1
Flash Media Server 4.0
Flash Player 10.3独立版/调试版/插件版
Windows 7 旗舰版
Chrome12/Opera11.5/Firefox5/IE9
BUG再现
我写了两个简单的Demo(一个发布端,一个接收端)来重现这个BUG。Demo需要FMS的支持。
错误的重点在于预加载类(PreloaderNSPlay.as)。由于预加载类在完成加载后就不再需要,一般的处理方法是将其从Stage中移 除。只要将移除,就会出现这个BUG(并非移除后立即出现,而是在接收音频流的时候出现)。而如果使用visible将预加载类隐藏,就不会出现这个问 题。
Demo的使用方法(服务端以FMS为例):
安装FMS,在安装目录下建立 /applications/testspeed/ 文件夹;
编译NSPulish.as和NSPlay.as,或者在这里直接下载swf文件;
确认本机安装了摄像头和麦克风,运行NSPublish.swf,单击“连接”按钮,查看log信息确定连接正常,见下图:
运行NSPlay.swf,单击“连接”按钮,查看log信息确认连接正常。此时会看到发布端的摄像头视频。如果FlashPlaye没有崩溃的话,就向着麦克风吹口气……呼……整个世界清静了……
使用 Adobe AIR 的时候碰到了一个 BUG 也是比较难找的,当然,这是因为我的功力不够:BUG?AIR打包的iOS程序在整数比较上的问题
在使用 Adobe 的开发系列的时候,碰到过许多令人纠结的 BUG,当然最后我都归结为自己的功力不够,怪不得别人。所以我把它们集合在一起,时刻提醒自己:AdobeBug | zrong’s Blog。
最难调的 bug 是难以重现的 bug。如果找到重现方法,就好办了。
有时候,难调是因为没有源码……
以前基于 Adobe Flash Media Server 写流媒体程序的时候,碰到过一个纠结的 BUG,查了3天才再现出来,发现是 Adobe Flash Player 的 bug,提给 Adobe 后被解决了。这个 BUG 是这样的(完整版:FlashPlayer在执行NetStream.play的时候崩溃的解决办法) :
BUG表现
在使用NetStream连接FMS发布的流,并执行NetStream.play(‘streamName’)方法时,FlashPlayer会崩溃。独立版、调试版以及基于浏览器的插件版均如此。
但是,这还不是全部。必须满足以下几点,该BUG才会出现。
使用Windows 7操作系统。也就是说,Windows XP不会出现这个问题;
播放的必须是RTMP流,RTMP流可以由Flash Media Server或者Red5来发布。也就是说,使用NetStream播放本地的flv/f4v/mp4视频不会出现这个问题;
播放的流包含音频。也就是说,如果该流只包含视频,不会出现这个问题;
播放的流中包含的音频声音较大。也就是说,即使该流包含音频,但如果发布方没有发出声音,或者发出的声音很小,该问题不会出现;当然,不需要很大的声音就能让播放端立即崩溃;
使用了Frame标签来做预加载。不了解Frame标签预加载的,看这篇文章:Preloaders in AS3;
在预加载完毕之后,使用removeChild移除了预加载类的实例(BUG就在这里)。
开发和测试平台(出现BUG的平台)
Flex SDK 4.5.1
Flash Media Server 4.0
Flash Player 10.3独立版/调试版/插件版
Windows 7 旗舰版
Chrome12/Opera11.5/Firefox5/IE9
BUG再现
我写了两个简单的Demo(一个发布端,一个接收端)来重现这个BUG。Demo需要FMS的支持。
错误的重点在于预加载类(PreloaderNSPlay.as)。由于预加载类在完成加载后就不再需要,一般的处理方法是将其从Stage中移 除。只要将移除,就会出现这个BUG(并非移除后立即出现,而是在接收音频流的时候出现)。而如果使用visible将预加载类隐藏,就不会出现这个问 题。
Demo的使用方法(服务端以FMS为例):
安装FMS,在安装目录下建立 /applications/testspeed/ 文件夹;
编译NSPulish.as和NSPlay.as,或者在这里直接下载swf文件;
确认本机安装了摄像头和麦克风,运行NSPublish.swf,单击“连接”按钮,查看log信息确定连接正常,见下图:
运行NSPlay.swf,单击“连接”按钮,查看log信息确认连接正常。此时会看到发布端的摄像头视频。如果FlashPlaye没有崩溃的话,就向着麦克风吹口气……呼……整个世界清静了……
使用 Adobe AIR 的时候碰到了一个 BUG 也是比较难找的,当然,这是因为我的功力不够:BUG?AIR打包的iOS程序在整数比较上的问题
在使用 Adobe 的开发系列的时候,碰到过许多令人纠结的 BUG,当然最后我都归结为自己的功力不够,怪不得别人。所以我把它们集合在一起,时刻提醒自己:AdobeBug | zrong’s Blog。
曾经把几台激光机连接在一起使用,所有信号保持一致。你可以简单的理解成让激光机整齐的动作。
测试也很简单,就是在铁板上画圆。在护目镜的后面,看到整齐的一排圆形火花也是很有成就感的。然后我们就开着机出去抽了根烟,回来就发现有几个圆的外围出现了曲线。由于是激光刻的,所以很清晰且无法消除。激光机从电源模块 激光发生器 振镜系统和控制系统都是我们亲手装亲手调的。于是我们乐观的拆了所有控制模块,做替换实验。观察了半个小时,又发生了,但图案与之前的完全不一样!
基本判定这是一个干扰,我们排查了一整天天,关闭了所有的用电设备,包括使用电池的笔记本电脑,这个问题还是偶尔发生。项目已经超期了,每天的损失上万,我们的压力可想而知。
这是一个偶发问题,我们连它的发生规律都掌握不了!第二天继续排查,我无意中发现最稳定的时候出现在午休时分。于是我觉得会不会是其他车间造成的干扰?
长话短说,最后查明是距我们百米开外的一个焊接车间电焊机每次启动时造成的干扰。我们共用一个地线,接地电阻出了问题导致焊机的干扰窜了过来!
这个车间是一个独立的小房间,我们既看不到光也听不到声音,就这么默默的被折磨了一天!
最后是我们自己打了一根地线解决了问题。
Used under license from Laughing Squid. 原始图片可以在 这里找到。)
他们技术改造了磁带驱动器,使得你可以只有一个中心驱动器 —— “A”盘 —— 由它连接着数个“B”盘,在跟A盘连接的内存里驻留这一个小型的操作系统,负责代理所有B盘的数据的读写操作。
每次当你启动A驱动器,你需要在外围驱动器里插入一张软盘,操作系统会把A盘加载到内存里。这个操作系统简单的出奇 —— 它的处理能力全部从一个8字节的微型控制器产生。
这种设备的目标用户是拥有大量数据的企业 —— 银行,杂志等等 —— 他们需要打印大量的地址簿或银行帐目。
有个客户出现了一个问题。在打印的过程中,有个别的驱动器会停止工作,导致整个打印过程终止。为了重载驱动器,值班人员必须重启所有驱动 —— 如果这种事情发生在一个6小时的打印任务中,大量宝贵的计算机使用时间都会浪费,整个任务将不能按时间完成。
公司派出了技术人员。技术人员尽了他最大的努力也不能在测试环境复制出这个问题:这个问题似乎只会出现在打印大量任务的过程中。尽管问题出在硬件上可能性微乎其微,他还是更换了所有的设备 —— 内存,微处理器,磁盘驱动,所有跟磁带机相关的部件 —— 但问题仍然出现。
于是技术人员打电话给总部叫来了一位专家。
专家要了一把椅子和一杯咖啡,坐在了计算机房 —— 那个时候他们已经专门为计算机提供了机房 —— 值班人员准备了一大堆的打印任务,他就在旁边看着。他等着,一直到机器崩溃。机器果真崩溃了,所有人都看着专家 —— 专家没有发现任何的线索。他命令把打印任务重新执行一次,所有的值班人员和技术人员都回各自岗位工作。
专家又在椅子上做下来,等着机器崩溃。这一等就是六小时,但真的又发生了。专家仍然没有弄清是什么导致了崩溃 —— 除了有一点他注意到,崩溃总是发生在屋内人比较多的时候。他命令再打印一次,重新坐下,等着。
当第三次崩溃时,他发现了一件事情。崩溃总是在值班人员更换其他没有关联的启动盘时发生的。进一步研究,他意识到当一个值班人员走过某块地板时崩溃就会发生。
地板是由铝制的板块拼成,下面有6 到 8 英寸高的隔空层,计算机所使用的大量的电缆都走地板下,这样可以避免值班人员无意间踢到它们。地板块间拼合的很紧密,这是为了保证垃圾不掉进电缆通过的空间。
专家说有一块地板变形了。当值班人员踩着这块变形的地板的一角时,地板块的边缘相互摩擦,这就会跟连接各地板的塑料之间产生静电,进而造成电磁干扰。
如今所有的RAM都有防电磁干扰功能。但当时并没有这种技术。专家指出,电磁干扰破坏的RAM的工作,操作系统也就崩溃了。
专家打电话给维护部门,拿来了一块新地板,他自己把它装上,问题就这样解决了。
在做 Patrisika 的时候遇到的 bug,它会把如下的 AST
[[. obj method] [yield x]]
错误地转换成
[set t1 [. obj method]]
[set t3 x]
[set generator.next [lambda [t2] [begin
[t1 t2]
…… ]]]
[return [object [value t3] [done false]]]
[t1 t2] 那里丢失了隐式传入的 this。
当时我正在写一个大量使用异步的命令行工具,在调试的时候发现变量干扰,有 node-inspector 下断点调试才知道是 this 没传进来。不过可以说是定位到 Patrisika 里来了。
于是打开 Patrisika,在那个做语义展开的 re 里下断点,结果发现那个方法调用居然走的是普通函数调用的模式,结果发现原因是模式匹配的时候忘记处理一种加 [.trivial] 修饰(这个修饰用于优化,re 基本不会展开如此的子节点)的子模式。把它补充上去之后,bug 消失。
OpenCV内部有如下这样一个表:
static const char icvPower2ShiftTab[] =
{
0, 1, -1, 2, -1, -1, -1, 3, -1, -1, -1, -1, -1, -1, -1, 4,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 5
};
有一个通过sizeof(XXType)算出来的element_size,这个值在32位下为16,在64位下为24。
有一个判断是 if(icvPower2ShiftTab[elem_size – 1] > 0)。
表现是在windows 32位下正常,移植到android下32位正常,64位异常。
于是判断是不是数据类型的问题,编译了一个64位windows下的版本测试(android下调试太麻烦了),结果也正常!怕操作系统问题,移植到linux下,64位也正常!
这就奇了怪了,windows 64位和android 64位没啥区别啊,跨平台代码,应该没错啊。
于是二分法、printf大法,调到最后定位出是这个if(icvPower2ShiftTab[elem_size – 1] > 0)判断出错,结果print("%dn",(int)icvPower2ShiftTab[elem_size – 1])发现竟然打印出来255,总算是找到出错的地方了。
看起来是-1变成了255,就像是signed char被强制认为是unsigned char一样。于是搜索“gcc char signed or unsigned”,c – Is char signed or unsigned by default? 看完这个问题的第2个答案才发现gcc在其他平台下默认char是signed char,在android下默认是unsigned char,简直哭晕。gcc和msvc默认char都是signed,但是android gcc默认char是unsigned。
最奇葩的是在Android 32位碰巧正常,因为第16个数为4,确实大于0,属于瞎猫碰上死耗子。
这个bug调了一整天,伤心伤身啊。。。
对了,附上解决方法:编译选项中加入 -fsigned-char,强制指定char为signed char。