闲谈丨像福尔摩斯一样去解Bug

621 阅读8分钟

十二月的风,将枝头的最后一片叶子吹落。在这样凛冽的日子里,人总是习惯性地回顾。但今天这篇不是年终总结,而是工作心法。毕业至今已七年有余,干的一直是稳定性的工作,平日里负责解各种各样的Bug。俗话说“熟读唐诗三百首,不会作诗也会吟”,工作上也是一样,长期的浸淫都会生出经验,或者说是心法。这些心法并非金科玉律,但它凝结了体验和思考,因此值得一说。

小时候我特别喜欢看一部动画片,名字叫《鸭子侦探》,那时我经常带入角色,把自己当成侦探来进行案件推理。殊不知二十年后,自己竟然做起了同类的工作,因为解Bug和案件推理实在太相似了。下面我就借助推理界的明星人物福尔摩斯的名言,来阐述解Bug的诸多心法。

"Data! Data! Data! I can't make bricks without clay."

“数据!数据!数据!没有黏土,我造不出砖块。”

调查者对于案件关联的信息是贪婪的,总是希望越多越好,因为你不知道哪一个会起到关键性的作用。譬如普通问题通常只保留调用栈,但是对复杂问题而言,寄存器、变量值以及内存信息也同样重要,这时Coredump、Ramdump等内存镜像就派上了用场,因为它们可以完整地保留案发现场。

除了信息以外,调查者对于工具也是贪婪的。这就好比一个修理工会准备各种型号的扳手,调查者同样有自己的工具库。内存崩溃用什么工具、fd泄露用什么工具、进程卡死用什么工具,这些调查者心里应该门儿清,同时针对一种问题可能要准备多个工具去适应不同的场景,譬如32位还是64位,Kernel是5.x还是6.x,等等。

"You see, but you do not observe. The distinction is clear."

“你看到了,但你没有观察到。这两者有很大的区别。”

“看到”和“观察到”的区别,其实就是表象和本质的区别。很多程序员写了多年的代码,其实一直生活在API建构的虚拟世界中。那些常见的机制对他而言只是一个黑盒,按一下按钮,掉下来一颗糖果,至于黑盒内部的原理则一概不知。举两个例子,分析问题用到调用栈的人很多,但是知道调用栈回溯原理的人并不多,因此当碰到调用栈异常的情况时,前者只能抓瞎,而后者可以分析出原因并寻找对策;代码中使用std::string、std::vector、std::map的人很多,但是了解这些数据结构内部布局的人并不多,因此碰到内存中的裸数据时,前者只能抓瞎,而后者可以取出具体的数据。如此种种,不一而足。

除了对常见机制的深入理解外,工具的原理也值得去了解。因为没有任何一个工具是万能的,了解原理才能够明白它的优缺点和适用范围,并且在某些特殊情况下改造工具以满足需求。

"Breadth of view is one of the essentials of our profession."

“广博的视野是我们这行的基本要素之一。”

软件行业的一个特点就是层级分明,上层应用通常建构在庞大的操作系统和基础库之上。没有问题时,大家各司其职,通过暴露的API友好协作。但一旦遇到复杂问题,尤其是问题贯穿多个层级且无清晰边界时,“各家自扫门前雪”的状况就会阻碍问题的解决,甚至可能演变为“公说公有理,婆说婆有理”的情况。因此,一个优秀的调查者应该拥有更加广博的视野,对于各个层级的一些基础概念都有了解,这样才能对复杂问题划清界限、追本溯源。

"We must look for consistency. Where there is a want of it we must suspect deception."

“我们必须寻找一致性,哪里缺乏一致性,哪里就可能有欺骗。”

早年间我在分析问题时犯过一些愚蠢的错误,譬如仅仅根据部分log就草率地下了结论,结果发现根本不是那么一回事儿。所以从那以后再碰到同类情况时,我就会提醒自己:当找到的根因只能解释部分现象,而无法解释全部时,那么就意味着根因寻找的并不准确。巧合的是,这一判断在日后从未出错。

"When you have eliminated all which is impossible, then whatever remains, however improbable, must be the truth."

“一旦你排除了所有不可能的情况,剩下的,无论多么不可思议,都必定是真相。”

“世界是个巨大的草台班子”,我不认同这句话的戏谑态度,但认同它的批判精神,它让我们平等地审视那些看起来“高大上”的事物,譬如操作系统、内核、CPU等等。它们是软件运行的基石,但并不表示它们精准无误。曾经我遇到过一个用户层的问题,所有的逻辑链条都能成立,唯独一个寄存器的值无法解释。我以为推理出了问题,因此反复尝试不同的角度,可是结果都一样。但当我假设底层的运行机制出错时,逻辑链条竟然奇迹般地闭合了,而事后的结论也证明了我的推断。因此,当你排除了所有不可能的情况,剩下的,无论多么不可思议,都必定是真相。

"If you have all the details of a thousand at your finger ends, it is odd if you can't unravel the thousand and first."

“如果你已经对一千个案件的细节了如指掌,却不能破解第一千零一个案件,那就奇怪了。”

解Bug的人最喜欢听到的一句话是:问题修复了。很多人一听到这句话就兴奋地将问题结项存档,其实自己还是一知半解。一知半解的理解只是一团浆糊,浆糊是没法砌起高楼的。因此我们必须对解决过的问题了如指掌,要抓住那些可以学习的机会。

微信上经常有朋友来跟我探讨问题,有些在问题解决后说要转个红包请我喝杯咖啡。这种我都谢绝了,反而我要感谢他们,感谢他们让我见识到这些稀奇古怪的问题,并配合我抓了充分的日志和调试信息,是他们给我提供了一次很好的学习机会。正是这些独特的学习经历,才造就了一个人的技术优势和壁垒。

"Once or twice in my career I feel that I have done more real harm by my discovery of criminal than ever he had done by his crime. I have learned caution now, and I had rather play tricks with the lay of England than with my own conscience."

“在我的职业生涯中,曾有一两次我深悟到,我抓到罪犯所造成的实际伤害,比他们的罪行本身还要严重。如今我已经学会了慎重,我宁愿对英国的法律耍些手段,也不愿违背自己的良心。”

Bug解的多了,难免将自己降格为匠人。但请千万记住,只有跳出问题,才能真正的认识问题。因此我们要学会更宏观地思考。譬如,不要纠结于Bug的难易,而要根据Bug的复现概率和影响程度去决定优先级;要学会用不完美去逼近完美,有些时候我们无法找到Bug的根因,但可以通过workaround去绕过它;学会用非技术的手段去定位问题,通过版本的二分比对去快速逼近问题的根因,等等。

此外,还有两个更深入灵魂的拷问,即:这是不是一个Bug?以及,这个Bug真的要修吗?

第一个问题在我们面对资源型的Bug时尤为明显,譬如超过阈值。可是阈值是谁定的呢?系统想要人人平等,可是我作为应用可不可以开个后门多用一点?这些问题深入下去已经超越了技术的范畴,更像是社会学讨论。

第二个问题在面对一些即将弃用的架构时会碰到。有些Bug的修复需要伤筋动骨,而恰巧这块代码正在剧烈的调整过程中。因此这类Bug通常采用绕过的方式,然后等待新架构的到来。

你看,等待有时也不是一种消极。