Teaser Image

mindwind

十日画一水,五日画一石




系统出了故障,我们又一次掉进了坑里,好不容易爬出了坑,拍拍尘土继续前行,却忘了回头看看这个坑当初为啥我们就没看见。每一次入坑出坑的过程都有或大或小的代价,值得我们回头看看。

系统故障

每一次系统故障多是因为程序运行失败或错误,偶尔也会有因为环境问题,比如:机器掉电、硬件故障、虚拟机错误等。但即便是环境原因引发的系统故障,也是因为程序编写考虑不足导致的。曾经就碰到因为硬盘故障导致服务假死(挂起)引发的系统故障,这就是程序的编写并未考虑硬盘 I/O 阻塞导致的挂起问题。

实际上,现今程序运行环境的可靠性并不如我们想象的高,而程序员很容易忽视这一点。之所以容易忽视是在于平时的开发、调试中我们很难遇到这些环境故障,而在一个部署规模稍大的分布式系统中这样的环境故障就比较常见了。

互联网企业多采用普通的 PC Server 作为服务器,这类服务器的可靠性大约在三个 9,换言之就是出故障的概率在千分之一。而实际在服务器上,出问题概率最高的可能是机械硬盘。

Backblaze 2014 年发布的硬盘统计报告指出,根据对其数据中心 38000 块硬盘(共存储 100PB 数据)的统计,消费级硬盘头三年出故障的几率是 15%。

而在 Google 这种百万级服务器规模的部署级别上,几乎每时每刻都有硬盘故障发生。因而在开发大规模的分布式程序系统时,面向失败设计(Design For Failure)是一个最基础的出发点。

程序失败

而程序失败除了其依存环境的故障外,更大一部分来源于程序自身的缺陷(Bug),这些缺陷都是因为程序员自身的编写水平局限所致。

粗心大意

人人都有局限性,所以任何程序员写出来的程序都可能存在缺陷。而对于所有人都存在的一个普遍缺陷就是粗心大意,这就导致了程序里一定可能存在一些非常低级的因为粗心大意导致的缺陷。

这就好比写文章或写书都会有错别字,即使通过正规出版的书籍,经过了三审六校,都无法完全的避免错别字的存在。而实际上我们「国家新闻出版署和语言文字工作委员会」联合颁布的《国家图书编校质量差错认定细则》规定,图书的错字率不超过万分之一就算合格。

而程序中的这类「错别字式」低级错误,比如:条件 if 后面没有大括号导致的语义变化,====== 的数量差别,++--的位置,甚至;的有无在某些语言中带来语义差别。其实通过反复检查也可能有遗漏,而自己检查自己的代码会更难发现这些缺陷,这和自己看不到自己的错别字是一个原理:

心理学家汤姆·斯塔夫(Tom Stafford)在英国谢菲尔德大学研究拼写错误。他解释说,错别字之所以能躲过我们的「法眼」,并不是因为我们太笨了或者太粗心,事实上,恰恰是因为我们的做法非常聪明。

“当你在书写的时候,你试图传达想法。这是非常高级的任务。”他说。而在做高级任务时,你的大脑将简单、零碎的部分(比如拼词和造句)概化,这样就可以更专注于更复杂的任务(比如将句子变成复杂的观点)。

“我们不会抓住每个细节,我们可不像电脑或国安局数据库,” 斯塔夫说,“相反,我们吸收感官信息,将感觉和期望融合,并且从中提炼意思。”当我们阅读他人作品时,这样做能帮助我们用更少的脑力更快地理解含义。但当我们验证自己的文章,我们知道想表达的东西是什么。因为我们预期这些含义都存在,所以很容易忽略掉某些感官表达上的缺失。我们在屏幕里看到的,在与我们脑子里的印象交战——这,便是我们对自己的错误视而不见的原因。

程序相对文字而言可以进一步的通过补充单元测试在运行时做一个正确性后验,反过来去发现这类我们视而不见的低级错误。

认知偏差

另一类缺陷可能是由于程序员对某些框架或代码库 API 的认知偏差导致的。比如,我曾经就在关于 Java 线程池的应用上踩过坑。Java 自带线程池有三个重要参数:核心线程数(core)、最大线程数(max)和队列长度(queues)。

我曾想当然的以为当核心线程数(core)不够了,就会继续创建线程达到最大线程数(max),此时如果还有任务需要处理但已经没有线程了就会放进队列等待。但实际却不是这样工作的,实际是核心线程(core)满了就会进队列(queues)等待直到队列也满了再创建新线程直到达到最大线程数(max)的限制。

这类认知偏差有时会带来严重的后果,然后你还找不到原因,因为这进入了你的认知盲区,你以为的和真正的现象之间的差异让你困惑不解。

另外一个来自生活中用语的例子。现在互联网上、朋友圈中小道消息满天飞,与此类现象有关的一个成语叫「空穴来风」,现在很多媒体文章有像下面这样用的:

他俩要离婚了?看来空穴来风,事出有因啊!
物价上涨的传闻恐怕不是空穴来风。

第一句是用的成语愿意:指有根据、有来由,空发三声读 kǒng 意同「孔」。第二句是表达:没有根据和由来,空发一声读 kōnɡ。第二种的新意很多名作者和普通大众沿用已久,约定俗成,所以又有辞书增加了这个新的义项,所以允许两种意思并存,自然发展,与时俱进。关于日常运用的问题,对于「空穴来风」这个词竟然有两种完全相反的解释并存,我不敢说在古今中外的语义学史上空前绝后,但的确是极其罕见的。所以现在我是从来不用这个成语的了,因为大部分人只知其一不知其二很容易引发误解。

而关于程序上有些 API 的定义也犯过「空穴来风」的错误,一个 API 可以表达两种完全相反的含义和行为。这样的 API 是极容易引发认知偏差导致的缺陷,在设计 API 时我们也要吸取「空穴来风」的教训。

熵增问题

程序规模变大,复杂度变高之后再去修改程序或添加功能就极容易引发未知的缺陷,这里软件界常借用物理学概念「熵」来表达软件系统的混乱和复杂程度概念。

曾经腾讯分享过 QQ 的架构演进变化中,到了 3.5 版本 QQ 的用户在线规模进入亿时代,此时在原有架构下去新增一些功能,比如:

「昵称」长度增加一半,需要两个月; 增加「故乡」字段,需要两个月;
最大好友数从 500 变成 1000,需要三个月

后端系统的高度复杂性和耦合作用导致即使增加一些小功能特征也带来巨大的牵连影响,所以一个小功能才需要数月时间。而现今流行的微服务架构其本质就是在控制大规模的系统的熵增问题。微服务架构通过增加开发协作、部署测试和运维上的复杂度来换取系统开发的敏捷性,因为前面付出的代价都可以通过提升自动化水平来降低成本,而只有编程这一项活动是没法自动化的,全依赖一定会犯错还有缺陷的人类程序员来完成。

所以,微服务本质上就是将一个大系统的熵增问题,局部化在一个又一个的小服务中,而每个微服务都有一个熵增的极限值。如果超过了这个极限值,所谓的「微」服务可能就不再是一个微服务了,而是一个「巨」服务了。

而对于一个熵增接近极限附近的微服务,服务负责人就需要及时重构优化降低熵的水平,高水平和低水平程序员负责的服务本质差别在于熵的大小。

错误修正

实际在非性命攸关的普通商业软件系统中,即使生产环境运行的程序我们都预知一些已存在的缺陷,它们可能在一些小概率的和非主要场景下的潜在故障。这类预期内故障通常通过排期优化方式来逐步迭代解决,而只有预期外的系统故障才会进行立即的错误修正。

引发故障的原因来自程序失败,程序失败来自前面我们分析的多种因素。若是要规避粗心的低级错误,普遍做法是通过开发规范、代码风格、流程约束,结对编程和同行评审等都是手段之一。

而认知偏差除了每次掉坑爬出来后的经验教训总结和团队内部分享,还可以通过一些技术手段来规避。比如常用的静态代码扫描来防止误用一些不安全的框架(Struts 漏洞扫描),不规范的写法(Findbugs 静态代码分析)。

而熵增问题若不及时重构优化,最后完整的重新设计实现必然带来巨大的代价。历史上曾经丰田陷入「刹车门」事件,就是因为其汽车动力控制系统软件存在缺陷。而为追查其原因,在十八个月中,有 12 位嵌入式系统专家受原告诉讼团所托,被关在马里兰州一间高度保安的房间内对丰田动力控制系统软件(主要是 2005 年的凯美瑞)源代码进行深度审查。

最后得到的结论把丰田的软件缺陷分为三类:

  • 非常业余的结构设计:架构师的问题
  • 不符合软件开发规范:技术经理的问题
  • 对关键变量缺乏保护:程序员的实现问题

在我看来,第一类属于架构师的问题,导致熵增问题随着系统规模变大而失控。第二类属于技术经理的问题,认知和控制不严格。第三类才属于程序员的粗心或实现水平问题。

而为了修正真正的错误,而不是头痛医头、脚痛医脚,我们需要更深刻的认识问题的本质,再来开出「处方单」。在 Amazon 严重的故障,需要写一个 COE(Correction of Errors)的文档,这是一种帮助去总结经验教训、加深印象避免再犯的形式。其目的也是为了帮助认识问题的本质,修正真正的错误,但一旦这个东西和 KPI 之类的挂上钩,负面作用是 COE 的数量会变少,但真正的问题并没有减少,只是被隐藏了。而其正面的效应像总结经验、吸取教训、找出真正问题,就会被大大削弱。

关于如何构造一个鼓励修正错误的环境,我们可以看看来自大韩航空的例子。大韩航空曾一度困扰于它的飞机损失率:

美国联合航空 1988 年到 1998 年的飞机损失率为百万分之 0.27,也就是说联合航空每飞行 400 万次,会在一次事故中损失一架飞机;而大韩航空同期的飞机损失率为百万分之 4.79,是前者的 17 倍之多。

事实上大韩航空的飞机也是买自美国,和联合航空并无多大差别。它的飞行员们的飞行时长、经验和训练水平从统计数据看也差别不大,为什么飞机损失率会如此地高于其他航空公司的平均水平?在《异类》这本书中作者以此为案例做了详细分析,我这里直接引用结论,有兴趣的读者可以去看书。

现代商业客机——就目前发展水平而言——跟家用烤面包机一样可靠。空难很多时候是一系列人为的小失误、机械的小故障累加的结果,一个典型空难通常包括 7 个人为的错误。

一个飞机上有正副两个机长,副机长的作用是帮助发现、提醒和纠正机长在飞行过程中可能发生的一些人为小错误。大韩航空的问题正在于副机长是否敢于以及如何提醒纠正机长的错误。背后的理论依据源自荷兰心理学家吉尔特·霍夫斯泰德(Geert Hofstede)对不同族裔之间的文化差异的研究,就是今天被社会广泛接受的跨文化心理学经典理论框架:「霍夫斯泰德文化纬度(Hofstede’s Dimensions)」。

在霍夫斯泰德的几个文化维度中,最引入注目的大概就是「权力距离指数(Power Distance Index)」。权力距离是指人们对待比自己更高等级阶层的态度,特别是指对权威的重视和尊重程度。

而霍夫斯泰德的研究也提出了一个航空界专家从未想到过的问题:让副机长在机长面前维护自己的意见,必须帮助他们克服所处文化的权力距离。

想想我们看过的韩国电影或电视剧中,职场上后辈对前辈,下级对上级的态度,就能感知到韩国文化相比美国所崇尚的自由精神所表现出来的权力距离是特别远的。因而造成了大韩航空未被纠正的人为小错误比例更高,最终的影响是空难率也更高,而空难就是航空界的终极系统故障,而且结果不可挽回。

那么,吸取大韩航空的教训应用到软件系统开发和维护上,就是需要 建立和维护有利于程序员及时暴露并修正错误、挑战权威和主动改善系统的低权力距离文化氛围,这其实就是推崇扁平化管理和「工程师文化」的关键所在。

而一旦系统出了故障非技术背景的管理者通常喜欢用流程、制度甚至价值观来应对问题,而技术背景的管理者则喜欢从技术本身的角度去解决当下的问题。我觉着两者需要结合,站在更高的技术维度去考虑问题,一个实例就是一个程序的问题大部分还得靠另外一个程序来解决。两个程序可能同时都有缺陷存在,但发生故障需的前提需要两个程序同时失败,这样就降低了故障的概率。另外一点,规则、流程或评价体系的制定所造成的文化氛围对于错误是否以及何时被暴露、如何被修正有着决定性的影响。

系统必然会故障,程序必然会失败,错误如何修正成了根基与关键。不要提出一些不切实际的目标,比如:永不失败的程序、绝不故障的系统。一切只是概率而已,所以理性让我不必害怕坐飞机,然而感性上我还是会害怕,人性的固有缺陷,人就是一个满身缺陷的系统。


写点文字,画点画儿。 微信公众号「瞬息之间」,遇见了不妨关注看看。