Kill-it-with-Fire-中文版-全-
Kill it with Fire 中文版(全)
原文:
zh.annas-archive.org/md5/1999c1b8e7dd9da6b84815af9da50b8d译者:飞龙
我们建造计算机系统的方式就像我们建造城市的方式:随着时间推移,没有规划,建立在废墟之上。
——艾伦·厄尔曼
前言
1975 年,著名物理学家大卫·L·古德斯坦(David L. Goodstein)出版了他的著作《物质的状态》(States of Matter),并在书中写道:
路德维希·玻尔兹曼(Ludwig Boltzmann),他一生大部分时间都在研究统计力学,于 1906 年自杀。保罗·厄恩费斯特(Paul Ehrenfest),继续这项工作的人,于 1933 年也以类似的方式去世。现在轮到我们来研究统计力学了。
这是一本关于如何运行遗留系统现代化的书,许多软件工程师认为这是一个缓慢的职业自杀,甚至是字面意义上的开篇。此书适用于在大型组织中工作、处理老化技术的人,但它同样适用于那些在仍在构建自己技术的小型创业公司工作的人。恢复遗留系统的运营卓越,归根结底是为了恢复一个迭代开发过程,使得这些系统随着时间的推移能够得到维护和演进。
这本书中的大多数建议同样适用于构建新技术,但遗留系统在我心中占有特殊的地位。我职业生涯的最初十年,我环游世界寻找应用人类学的工作,同时也在空闲时间编程。我会编程,因为我的父亲是一名计算机程序员,我在一个充满计算机的家庭中长大,那时这还很罕见。
我从未成为自己想象中的那种勇敢的国际援助工作者,但我最终找到了将应用人类学运用于遗留系统现代化的工作。像陶器碎片一样,旧计算机程序是人类思想的遗物。通过查看代码,你能了解一个组织过去的很多信息。
要理解遗留系统,你必须能够定义原始需求是如何确定的。你需要挖掘整个思维过程,并弄清楚当选项发生变化时,权衡取舍的情况是什么样的。
仅仅因为老旧并不足以让某个事物成为遗留系统。遗留技术这一词背后的潜台词是它也是糟糕的,可能几乎不能运行,但遗留技术之所以存在,仅仅因为它是成功的。这些旧程序或许不如以前高效,但没有被使用的技术是不可能存活几十年的。
我们已经过了所有技术对话和知识分享仅仅围绕构建新事物的阶段。我们有太多的旧事物了。我父辈那一代写了很多程序,每年他们都会对自己的工作有多少仍然存在、仍在某个生产系统中运行感到震惊。我们这一代编写的程序 exponentially 更多,感染了生活的方方面面,嵌入了计算机芯片和一些运行指令。我们将会感到同样的震惊,当那些系统在 30 年、40 年或 50 年后仍然存在时。
因为我们没有讨论如何现代化旧技术,组织一次又一次地陷入相同的陷阱。失败是可预测的,因为许多软件工程师认为关于现代化遗留技术的讨论与他们的职业无关。有些人甚至会惊讶地发现,COBOL 仍然支撑着金融行业的运作,大部分网站仍然是用 PHP 编写的,或者人们仍然在寻找具备 ActionScript、Flash 和 Visual Basic 技能的软件工程师。
失败是如此可预测,以至于在做这项工作一两年后,我发现只要了解一些关于所部署技术的基本信息,我就能预测出组织面临的一系列问题,以及这些问题的解决方案如何失败。偶尔,我会为了逗乐其他工程师并推动我的职业发展,表演这个“魔术”,其中一次是在纽约时报的面试中。
当我离开政府部门回到私营部门时,我发现那些曾经对旧系统有效的技术,实际上对相对较新的系统也非常有效。我加入了一家成立六年的公司,做遗留技术现代化工作。然后,我又加入了一家成立仅六个月的公司,依旧做遗留技术现代化工作。有一次,我不禁抱怨对我的老板说:“为什么我在一个三个月大的系统上还要做遗留技术现代化?”他反驳道:“活该,三个月前你没来。”
尽管如此,将所有系统都视作遗留系统来维护几乎没有什么坏处。建立新系统很容易,但一旦系统建立,重新思考它们就变得困难。遗留技术现代化之所以困难,并不是因为技术上难——问题和解决方案通常是可以理解的——而是现代化过程中涉及到的人际因素让人感到棘手。要获得足够的时间和资源来实施变更,培养对变革的需求并保持这一势头,管理跨部门的沟通,尤其是在有许多其他系统与之连接或依赖的情况下——这些都很难。
然而,作为一个行业,我们并没有讨论这些挑战。我们假设太阳、月亮、星星和董事会会围绕正确的技术答案自动重新配置,仅仅因为它是正确的技术答案。我们震惊地发现,大多数人其实并不关心一项技术有多健康,只要它能够以合理的精度在不让他们失去耐心的时间内完成他们需要的功能。在技术领域,“足够好”是最重要的。
在尝试解释如何接近遗留系统现代化时,我首先回顾并探讨了技术如何随着时间变化。第一章和第二章都探讨了计算趋势和最佳实践的机制:我们是如何决定今天被视为遗留的技术的,以及我们能从这一过程中学到什么,以指导我们的现代化计划?
在第三章中,我讨论了让组织觉得需要现代化的三大问题:技术债务、性能问题和系统稳定性。我举了一个真实(虽然匿名)的系统的每种问题的例子,并说明了如何将现代化计划组织起来。
第四章讨论了为什么遗留系统现代化如此困难以及为何如此频繁失败。我分析了跨平台兼容性的大误区,以及抽象在处理我们认为容易或困难的事物时的作用。
第五章谈到了任何遗留系统现代化中最关键的特性:动力。你如何获得动力,并且如何保持它?我概述了一系列条件——有些是动力杀手,有些是动力促进者。
第六章讨论了如何进入一个已经开始的现代化项目,并解决可能阻碍其进展的最常见问题。
第七章尽可能全面地概述了设计思维,以及我们如何利用设计来引导并最终改善技术对话的结果。
第八章完全讲述了打破现状以及不惧失败的价值。我探讨了混乱测试如何补充遗留系统现代化,并探讨如何与一个认为你应该故意打破事物的建议可能有些过于激进的组织合作。
第九章讨论了为什么成功不像你想象的那样显而易见或自明,以及如何定义标准来判断一个项目何时完成。
最后,第十章提出了避免再次对同一个系统进行现代化的策略。你如何知道你的软件是否可维护?如果不可维护,你能做些什么?
本书的语言是经过深思熟虑的。我使用了组织而不是公司或企业这个词。我在这个领域的大部分工作经验都来自政府和非营利组织,但遗留问题无处不在。需要构建和维护良好技术的组织不仅限于私营部门。例如,美国联邦政府是世界上最大的技术生产者之一。关于遗留系统的讨论跨越了商业、政府、医院和非营利组织。因此,当我提到“业务”方面的组织时,我是指工程团队为支持其使命而构建技术的部分。一个组织不一定要盈利,才会有业务方面。
在整本书中,我使用系统一词来指代一组为完成共同任务而协同工作的技术。系统是一个在技术讨论中颇具争议的词,因为似乎永远找不到一群工程师能达成一致来定义它的边界。不过,就我而言,这种模糊性是有益的。它使我可以更广泛地讨论传统系统的现代化问题。
为了描述系统的各个部分,我经常使用组件或偶尔使用服务这个词。虽然本书中的许多技术适用于任何类型的技术,但示例和讨论特别倾向于软件工程和基于网络的开发。我不可能写一本关于传统系统的书而不提到主机、数据中心和旧操作系统,但我的大部分经验是将这些系统升级为更适合互联网的选项,这本书也反映了这一点。我期待着拥有其他背景的技术专家通过文章补充本书的内容,讨论我的建议是否同样适用于他们。
我真诚的希望,在你阅读本书时,无论你的技术多么陈旧,都能找到对自己技术项目的启发。我尽力将尽可能多的资源、练习和框架融入这本书,力求做到尽可能详细,并尽可能通过真实的故事来支撑我的论断。
我们正迎来传统系统的临界点。构建这些系统的最早一代人正在逐渐离世,而我们在这些陈旧、基本没有文档记录并且无人维护的计算机程序上不断叠加更多社会层面。虽然我不认为社会会因此崩溃,但对于愿意加入其中的人来说,仍有很多有趣且有意义的工作。
第一章:时间是一个平坦的圆圈
2016 年夏天,我坐在了作为软件工程师所遇到的最奇怪的系统面前。那是一个用 Java 编写的相当普通的 Web 应用程序,它连接到我最终会弄明白的是一台大型机。大型机本身并不奇怪。当你进入遗留系统现代化的领域时,你很快会意识到大型机仍然无处不在——在银行、政府机构中,深藏于民间社会的基础设施中。让一个 Web 应用程序向大型机发送请求并不奇怪。我曾难以接受一项为大宗交易设计的技术能够足够快速地响应,以满足一个网站在合理规模下的需求,但尽管我有担忧,它似乎还是能做得不错。
不,不奇怪的是,这台大型机是 1960 年代的产物,并且将数据存储在磁带上。那台大型机根本不可能响应得足够快,因此当我看到架构图时,我注意到中间有一组神秘的机器;一边是现代的 Web 应用程序,另一边是古老的主机。
我对这组机器唯一了解的信息是组织用来表示它的缩写。与我合作的工程师团队中没有人似乎知道这些机器是做什么的。在深入研究了几十年的文档后,我才搞清楚它们的用途:Unisys ClearPath Dorados。换句话说,它们是更现代的大型机,实际上像缓存一样配置在老旧大型机前面。这就是 60 年历史的代码为何能快速响应现代互联网的请求。该组织有一台新机器位于其中,存储了相关数据的临时副本。大约每周一次,新的大型机会从旧的主机请求更新。
当我问一位参与该系统开发的工程师,关于他对这种安排的看法时,他说了一句话,至今让我记忆犹新,并最终改变了我对现代化遗留计算机系统的理解:“那么,云计算和老旧的大型机时分共享系统有什么不同?”
答案是,这两者实际上并没有什么不同。这两种方法都为你在由更大的机构维护的共享资源上花费的时间收费。你通过相同的通信线路连接,有时使用相同的协议。客户端/服务器模型基本相同;唯一不同的是接口和编程语言。工程师还补充了另一个有趣的观察:“我们从瘦客户端的大型机绿色屏幕终端应用程序开始,然后他们要求我们迁移到 PC 上的胖客户端,现在他们又想要使用瘦客户端的 API。”
软件工程师在进行遗留系统现代化时,常犯的第一个错误就是假设技术进步是线性的。在这种思维框架下,任何采用旧设计模式或旧架构哲学构建的东西,都比不上更新的选择。提升旧系统性能的唯一方法,就是将其重新排列成一种新的模式。
我处理像所描述的“弗兰肯斯坦系统”的经验让我明白,技术进步不是线性的,而是循环性的。我们在前进,但进展缓慢,且是在切线方向上推进。我们放弃了某些模式,却又重新发明它们,并以全新的方式出售。
技术的进步不是在前人基础上建立的,而是通过从中转变而来的。我们从现有的核心概念出发,对其进行修改,以填补市场的空白;然后我们围绕这个空白进行优化,直到这种优化将所有未被新技术覆盖的人群和使用案例聚集在一起,形成了一个独立的市场,而另一个“进步”将会抓住这个市场。
换句话说,围绕数据中心的军备竞赛让小型组织被甩在了后面,并催生了商业云的需求。为了定制化和控制性优化云平台,创造了托管平台的市场,最终催生了无服务器计算。无服务器模型将不断为其用户提供更多基于其最具吸引力特性的开发功能,直到无服务器方法不完全适用的边缘案例开始在彼此之间找到共同点。然后,一种新的产品将应运而生,解决这些需求。
利用现有的技术
大多数人意识到,技术可能在发明时并不流行,直到很久之后才变得流行,但他们通常将这种现象归因于发明者的缺乏远见,或者是营销部门技能的不足,或是技术本身的成熟度问题。
经济学家对新技术采用率有不同的解释。他们通常将其描述为可对齐差异和不可对齐差异之间的对比。可对齐差异是指消费者已有参照点的差异。例如,这辆车比那辆车更快,或者这部手机的摄像头比那部手机更好。
不可对齐的差异是那些完全独特且具有创新性的特征;没有可以进行对比的参考点。你可能会认为不可对齐的差异更能吸引潜在消费者。毕竟,没有竞争!你正在以不同的方式做事。但当需要做出购买决定时,如果没有对比,就没有明确的价值感。如何评判一件没有等同物的商品的价值——进而评估以某个价格购买它的权衡?为了让不可对齐的差异产生影响,它所带来的估计价值必须大于所有可对齐的差异以及所有其他不可对齐的差异加起来的总和。^(1)
消费者对于必须进行这样的猜测并不自信。^(2) 这增加了买家后悔的风险,且关于需求和效用的推理让消费者感到不舒服。因此,各种产品通过寻找能够标榜与现有解决方案不同的特定特征来在市场上实现差异化。这推动了技术的循环发展。人们从相同的产品中获得的体验并不完全相同。当一家公司在不断迭代并改善产品的某个特征时,最终却使得该产品对现有客户群体的吸引力下降。公司这样做是希望通过吸引更多的新客户来使这一损失变得无关紧要。
大多数时候,这种渐进式优化只会产生一些烦恼,这些烦恼在社交媒体上上演,最终会平息。偶尔,也会有足够多的人因为优化而经历了效用的下降,进而成为一个潜在的市场,值得被捕捉。包括那些最初从未购买过该产品,但如果它以某种其他方式优化过,他们会考虑购买的消费者。利用可对齐的差异实际上是在将产品推得更远离这些消费者的购买需求,但却为另一家公司提供了机会来解决这个问题。
请考虑以下问题:是拥有一部小型手机更好,还是拥有一部大型手机更好?
世界上第一款商业化的手机是摩托罗拉的 DynaTAC 800。那是一款巨大的砖块手机,现在常用来作为讽刺作品中 1980 年代的象征。它超过 10 英寸高,根本不是那种可以轻松放进口袋的东西。显然,市场需求促使手机变得越来越小。到 1994 年,IBM 的 Simon 将手机缩小到 8 英寸高,并增加了行业首次尝试智能功能,如发送传真和电子邮件、维护日历、记笔记、阅读新闻和查看股价。尽管这些进步令人印象深刻,但 Simon 很快被翻盖手机所取代,翻盖手机将 8 英寸的手机尺寸折叠成一半,变得和普通裤子口袋的宽度和深度差不多。^(3) 各大公司开始从销售数万部设备到数百万部设备。
我那时还在高中,尽管现在手机无处不在,但在 1990 年代,手机对我们这些孩子来说并不是一个值得购买的物品。我只有一个朋友拥有手机,它主要有两个功能:一是当他的车在往返 Taco Bell 兼职工作时出现故障时使用,二是课上玩一个灰度版的Snake。对于我和我的同龄人来说,我们更可能在课间的走廊里碰到我们可能想要打电话给的人,或者通过父母支付的其他方式与他们沟通。我们并没有把短信作为手机的主要功能。寻呼机同样能起到作用。
事实上,一项皮尤研究发现,座机在 2009 年仍是美国青少年首选的通信方式,距今已有近十年。^(4) 超过一半的青少年甚至从未发送过短信。到那时,iPhone 已经上市两年,并且是其第三代产品。又过了一年,皮尤的跟踪报告呈现出截然不同的画面:美国青少年中的手机使用迅速增长,超越了所有其他通信方式。^(5)
发生了什么变化?
每隔一段时间就会有一个网络迷因,指出手机尺寸的转折点大约发生在 2005 年,并附上“在这里我们意识到可以在手机上看色情”的话。实际上,直到 2010 年,屏幕尺寸仍然在变化,市场上有很多选择,从全面触摸屏外观到更为简朴的界面,后者配有物理键盘,专为商务用途优化。青少年群体在此之前一直是一个未得到充分满足的市场,直到屏幕尺寸开始增大,摄像头成为手机的核心功能之一。青少年们使用手机的主要目的就是相互分享图片和视频。^(6) 一旦清楚地意识到通过将手机作为娱乐设备来销售可以捕捉到一个市场,手机的缩小趋势就突然停止,开始变大。关于分辨率、显示效果和相机质量的创新加速了这一进程。
看起来我们可以简单地认为这些趋势意味着技术已经成熟到能够找到并抓住市场的地步。但数据实际上并不支持这一点。诺基亚的 N95 在 2007 年就提供了 500 万像素(MP)的相机。随之而来的是第一代 iPhone 发布时配备了 200 万像素相机,而 HTC Dream 则于 2008 年推出,配备了 315 万像素相机。2010 年,iPhone 和 HTC 分别推出了前置摄像头,分辨率分别为 30 万像素和 130 万像素。市场上的技术并没有变得更好;它一度变得更差了。^(7)
单纯生产面向 1990 年代或 2000 年代初期青少年的手机,并不会导致市场的爆发性增长。青少年对手机没有强烈的参照点。从他们的角度来看,手机并没有足够吸引人的特点来证明其价值。只有当他们父母的设备在文化参照和日常生活中变得普及时,青少年市场的潜力才得以释放,并且吸引他们兴趣所需的大屏幕也得以出现。
每个在年轻美国用户中受欢迎的功能,在 2010 年之前就已经存在了。自 2000 年以来,手机上就有摄像头,并且销售情况不错。我在日本生活时,2004 年就已有能直播的手机。并不是某种令人印象深刻的技术进步改变了市场。手机在日常生活中的日益普及为一个新的、更有利可图的市场铺平了道路,迫使手机设计发生了彻底的改变。
技术历史充满了这种转变。一种特定的方式或技术变得流行,但并不适合每个人的使用需求。公司开始进行实验,并将这种新兴的热技术应用到越来越多的领域,直到这种方法无法适应或不理想的情况增加,成为改变势头的力量。行业重新发现了一种不同的做事方式,并朝着那个方向反弹。
工程师们称 Kafka 的发布/订阅模型优于企业服务总线(ESB)的中心与辐射模型。ESB 是一个单点故障,是面向服务架构的反模式。然后 Kafka 增加了其 Connect 框架(0.9 版本)和 Streams API(0.10 版本),重新引入了许多 ESB 的核心概念。谷歌开发了加速移动页面,以通过 JavaScript 推进异步加载,然后将服务器端渲染加入其中——突破了自己的规范,回到了 HTML 已建立的模式。
市场变化是复杂的事件。我们可以看到技术在同样的方式和结构中反复循环,但这些变化更多地不是关于某个特征的优越性,而是关于潜在消费者如何组织自己。
用户作为市场共同创造者
总体而言,这类复杂的复合性变化被称为服务主导逻辑**(S-D Logic)。S-D 逻辑认为,消费者的价值不是由公司生产产品所创造,而是通过多个参与者之间的积极合作。根据 S-D 逻辑,消费者不是被行业设计出需求和欲望的被动、无思考的“羊群”。相反,消费者积极参与创造市场,这些市场被用来向他们销售商品。
消费者和公司主要通过相互作用创造价值。任何曾经尝试创办自己企业的人都会告诉你,问题的存在并不意味着有市场可以解决它。2004 年,从手持设备轻松流式传输电视和电影并不是消费者有兴趣解决的问题。那时已经有相关技术,但没有人愿意把辛苦赚来的钱投入到解决每个人都面临的问题上。一旦手机解决了在移动中保持与办公室联系的实际问题,它们便出现在了大量其他消费者的视野中。这促使其他需求开始整合为可销售的问题。你可能不会太在意在横跨国家飞行时无法观看自己喜欢的电视节目的最新一集这个问题。但如果你知道其他人有这样的选择,而你却错过了,这个解决方案突然间变得更具市场潜力。
大型主机与云计算
然而,有时,推动进步的离心力要更加基础和根本。让我们回到本章开始时的故事:为什么我们从大型机的时间共享迁移到个人计算机上的庞大应用程序,再到商业云中的时间共享?例如,我们本可以继续开发大型机,直到它们变成云计算。那为什么我们没这么做呢?为什么我们花费数百万美元将成千上万的应用程序迁移到新范式,最终又不得不在一两十年后将它们迁移回更轻量的客户端?
技术是任何组织运营模式中,且可能永远是一个昂贵的元素。技术的许多进步和市场共创可以理解为硬件成本和网络成本之间的相互作用。计算机是数据处理器。它们处理数据并将其重新排列成不同的格式和显示方式。无论我们用它们来玩电子游戏还是处理电子表格,它们的作用基本上就是这些。
所有与数据处理器相关的进展归结为两件事:要么让机器更快,要么让传输数据到机器的管道更快。这两者的力量不能独立增长。如果传输数据的管道远远超前于处理数据的芯片,机器就会崩溃。如果机器远远超前于网络,用户就无法从速度提升中获得实际的附加值。
当通过解锁网络速度或硬件速度方面的可用改进,能够创造可对齐的差异时,整个行业往往会改变范式以优化这种改进。这样做会通过将一些潜在客户和使用案例抛在后面,从而为下一次变化创造市场。
在黄金时代,主机存在于一个处理器能力有限的世界里。为了将计算卸载到机器上,足够的处理能力需要投资一个充满设备和专业操作员的房间,这些都非常昂贵。市场在加速硬件提升上投入了大量资金。在这一过程中,最简单的办法其实是让芯片变得更小,这样你就可以在同一台机器上塞入更多的芯片。这样做并没有立即导致主机变得更小,而是将市场扩展到不同的价格区间,让大型组织仍然愿意支付数百万美元,而小型组织则能够被说服支付几万美元来拥有属于自己的主机。这让计算机接触到了更广泛的受众,并刺激了最终变成个人电脑的市场。即便如此,仍然没有必要提升网络速度,因为计算机很慢,存储能力也有限。举个例子,1985 年的超级计算机的处理能力大致相当于早期的 iPhone。一台那个时代的典型计算机可能只有几百 KB 的 RAM 和存储空间。国家科学基金会网络,后来成为早期互联网的骨干,在 1980 年代提供 56kbps 的速度。以这个速度,传输一台计算机的全部数据大约只需要一个小时。
最终,几十年的工程工作改变了这种关系。现在,更快、更强大的计算机正在等待通过网络传输数据。越来越多的这些机器变得更小、更便宜,且存储容量不断增长。随着机器数量的增加,网络负载也随之增加。连接到同一网络的计算机越多,网络速度就越慢。更多速度需求要花时间才能引起市场反应,因此行业通过转向那些将更多数据本地存储在机器上的应用程序进行了优化。我们放弃了那些运行在集中式计算机上的应用程序,而这些计算机是通过网络与我们连接的。如果我们不需要通过网络传输数据,那么网络速度就不会成为我们的瓶颈。
固定费用互联网
在这个时候,我们没有周期;我们有的是过渡。行业的偏好从在大型集中式计算机上处理转向了更小、更便宜的本地工作站。说互联网变得更快更便宜之后回到之前的状态并不算剧透,但这真的是不可避免的吗?
一旦个人计算市场解锁,私营部门不太可能自己建设互联网。当时,计算机制造商从他们的专有标准中获得的财务收益要大得多。互联网的核心创新是将许多不同类型的网络连接成一个互联网络(因此得名互联网),这需要对所有制造商开放的共同标准。构建一个可以跨越单一国家的网络本身就是一个重大的工程挑战。事实上,许多国家级计算机网络项目在互联网诞生的同时也在尝试中。英国有一个;法国有两个;苏联有三个失败的尝试。美国最终成功,因为它并不是在试图建立一个国家级网络,而只是试图解决计算机制造商推行的所有专有标准所导致的兼容性问题。美国军方资助了大量昂贵的计算机,并希望让彼此相距数百英里的研究机构能够共享这些资源。如果由计算机制造商来决定,他们显然会更希望所有的研究机构都购买自己的机器。
然而,互联网最终还是建成了。最初它既慢又笨重,且没有明确的商业实现,充斥着学者、爱好者、未来学家和怪人。与商业市场的饱和激发了消费者将手机作为个人娱乐设备的需求,主机计算机市场的饱和促使资源较少的小型组织寻求更小的机器一样,互联网是通过一个略带无政府主义色彩的创意社区渗透到商业市场中的。到 2000 年,76%的在线用户是从家庭连接的,而 41%则是从企业连接的。到 2014 年,家庭互联网使用激增至 90%,而工作互联网使用则停滞在 44%。^(8)
关于互联网,令人感兴趣的是它是唯一一种历史上实行统一费率定价的现代通信媒介。^(9) 互联网中的所有数据包几乎都是以相同的方式计费的,无论它们是什么,或者它们要去哪里。^(10) 相比之下,长途电话比本地电话贵,或者在国外连接到移动网络时比在国内使用更贵。在互联网中,消费者为更快的速度支付更多费用。这给电信公司带来了压力,迫使它们通过提高连接速度来竞争。互联网变得越快,越多的人涌入互联网。互联网上的内容越多,消费者登录的次数就越多。试图访问互联网上某一资源的人越多,自己机器上托管这些资源的成本就越高。最终,这反转了计算机行业的价值主张,使得“云端”处理数据比本地处理数据更便宜。我们重新回到了租用别人拥有的昂贵计算机的时间,而不是自己承担购买、维护和升级这些昂贵计算机的成本这一概念。
人们可以通过处理能力和存储容量是否比网络速度增长得更快来大致跟踪架构范式的兴衰变化;然而,更快的处理器往往是电信公司用来提高网络速度的一个组成部分。这种相互依赖性在几乎任何市场中都是成立的。产品开发改变消费者行为,而消费者行为又改变了产品开发。技术进步并非沿着直线发展,因为直线实际上并不高效。经济并不是一个平坦的平面,而是一个具有山脊和山谷的丰富地形,需要绕过。
影响变动的因素本质上也是分形的、跨学科的。美国互联网服务提供商(ISP)早期选择固定费用定价结构的原因在于,市场环境提供了两种选择来建立通信线路网络,这对于他们开展业务至关重要:要么自己建设线路,要么租用他人的线路。在后者这一类别中,存在多种选择,不仅包括像 AT&T 这样的为电话服务建立的现有电信网络,还包括大型机构为连接其数据中心而维护的专线。像 90 年代的 AOL 这样的公司,既与电信公司竞争出售互联网接入服务,又是这些电信公司的客户。这使得 ISP 对客户反馈更加敏感,而简单、固定定价的心理吸引力变得必不可少^(11)。其中,按使用量收费意味着服务在变差时变得更加昂贵。更多用户的活跃最终会导致拥塞,进而影响网络性能。用户难以准确估算自己的使用量,而大多数用户在固定价格下支付的费用更高,这进一步刺激了该行业。如果你想快速验证这一点,可以查看你实际的手机数据使用量,并与每月支付的流量限制进行对比。大多数用户从未接近耗尽流量。
在欧洲,电信通常是由政府运营的,这意味着竞争较少,从而推动了更简单的定价模式。欧洲联盟最终在 90 年代末期放开了互联网市场,而美国则允许宽带行业进行整合。结果,欧洲的更多竞争推动了网络速度的提升和价格的下降。今天,许多地方的互联网速度比美国更快。这对未来技术行业的变革意味着什么尚不明确。任何因素都有可能改变被推崇为最佳实践的范式。接触新技术可以创造一个新市场,这个市场可能与主机和个人电脑并行运行,或者它可能完全超越另一个市场,就像娱乐优化的手机淘汰了黑莓和其他商务手机一样。价格可能会下降,资源可能变得稀缺。这些变化很少,甚至从未单纯由技术优势推动。
为了价值而迁移,而非追逐趋势
这些与遗留系统现代化有什么关系呢?当人们认为技术以线性方式进步时,他们通常也认为任何新技术都比当前使用的技术更先进、更好。采纳新实践并不一定使技术变得更好,但几乎总是使技术变得更复杂,而更复杂的技术不仅难以维护,最终也更容易失败。
然而,信息技术如果永远不变,就注定失败。理解我们是以周期性方式进步的非常重要,因为这是我们学会如何避免不必要的重写和部分迁移的唯一途径。技术变革应该关注真正的价值和权衡,而不是错误的假设——即更新的技术默认更先进。
有时候,将你的使用案例与其他看似相似的组织的使用案例进行比较是很困难的。在这方面,最大的误导来自商业云,正因为它为如此广泛的使用案例增加了价值。人们往往认为这意味着它是适用于所有使用案例的更先进技术,然而事实并非如此。我有个朋友,她运营着一个 Hadoop 集群,用于处理财政部的金融数据。她的首席信息官(CIO)坚持认为,他们需要关闭他们维护的服务器,把这个处理过程迁移到云端。CIO 没有意识到的是,尽管迁移数据比 1980 年代便宜且更容易,但依然是昂贵的。毫无疑问,如果你在存储数据的地方处理数据,速度和性能会更好——在这种情况下,就是在现场。大数据即服务是否能为你节省开支,取决于你的大数据实际有多大,数据集中在哪里,以及它最初是多长时间才变得如此庞大。五年收集的 PB 级数据与几小时内产生的 PB 级数据是完全不同的情况。
价值主张常常是一个复杂的问题,正因为如此。对于纯粹的技术型组织来说,正确把握这一点已经足够困难;对于那些只有供应商拥有足够知识来提供建议的组织来说,这更加困难。
第二章:食人代码
如果技术是以周期形式发展的,你可能会认为最好的遗产现代化策略是等待十年或二十年,直到范式发生变化,再跳跃过去。要是能这样就好了!尽管大型主机和云计算在总体上可能有许多共同点,但它们在实施上有许多显著的差异,阻碍了它们之间的轻松过渡。尽管时间共享的架构理念已经重新流行,但技术的其他组成部分却在不同的节奏上进步。你可以将任何单一产品分解为无限多个元素:硬件、软件、接口、协议等。然后你可以在这些类别中添加特定的技术。不所有的周期都是同步的。现代技术与旧技术完美契合的几率,就像是找到两天每颗星星的确切位置完全相同一样。
因此,理解技术是以周期形式发展的要点不是升级等得越久就越容易,而是你应该避免仅仅因为技术是新的就去升级。
可对齐差异与用户界面
没有可对齐的差异,消费者无法判断他们被要求投资的技术的价值。完全创新的技术不是一个可行的解决方案,因为它没有参考点来帮助它找到市场。我们常常认为技术是简洁高效的,没有任何没有明确目的的冗余部分,但事实上,许多你依赖的技术形式都包含了遗留下来的特征,这些特征要么是从其他旧技术继承而来,要么是后来引入的,以制造功能相等的假象。
例如,大多数软件工程团队保持 80 列的代码行宽。短行代码比长行代码更容易阅读,这一点是事实。但为什么是 80 列呢?为什么不是 100 列呢?
奇妙的是,80 列宽度是旧时大型主机打孔卡的尺寸,这些卡片用于输入数据和程序到 20 世纪 50 年代和 60 年代建造的巨型计算机中。因此,现在,已经是 21 世纪,程序员们还在强制执行一个为他们大多数甚至未曾见过、何况是编程的机器而制定的标准。
那么,为什么主机打孔卡是 80 列宽的呢?最早计算机公司使用的打孔卡——当时它们是主要用于像人口普查这样的工作,还是机械“制表机”——是临时设计的,并且极其低效。它们被设计用来进行计数,而不是计算,因此它们的设计类似于铁路乘务员用来售票的卡片,而不是用于存储数据的卡片。^(1) 这些卡片需要批量输入机器,然后再进行排序和存储。为了避免重新发明一切,这些卡片本身的设计与当时美国纸币的尺寸大致相同:3¼英寸乘 7⅜英寸。这意味着公司可以重新利用现有的抽屉、箱子和盒子来获取所需的配件。
到了 1920 年代,客户开始要求 IBM 在一张卡片上获取更多的数据存储空间。IBM 的创新是改变了孔的形状,使其更接近矩形,这样它们可以更紧密地排列在卡片上。^(2) 这意味着可以在卡片上放置 80 列孔。
现在,我们更深入地探讨一下。那打孔卡本身呢?为什么第一代计算机设计要从带孔的硬卡片输入数据?键盘的出现几乎和打字机一样久远,第一台现代打字机由克里斯托弗·拉瑟姆·肖尔斯、卡洛斯·格里登和塞缪尔·W·索尔于 1868 年获得专利,几乎比一些主机的开发早了一个世纪。电报机甚至早于此就开始实验不同类型的键盘。那么,为什么人们宁愿在一张厚纸上打孔,而不是直接在键盘上输入信息呢?
键盘或类似的输入设备的问题在于,人类操作员很容易输入错误,尤其是当操作员没有任何视觉确认,无法确认他们认为输入的内容是否就是机器接收到的内容时。想象一下在一个隐藏输入内容的网页字段中输入密码。这样的密码隐藏字段的一个缺点是,如果你按错了键,可能直到系统拒绝你的输入时才会注意到。你有多少次输入错误密码?现在想象一下在没有看到自己输入内容的情况下输入一条完整的信息。操作员错误是电报系统中的一个大问题,尤其是当电报开始在全球传递重要信息时。
解决方案是使用键盘,但键盘并不是直接与电报接口,而是会生成一个可以在机器尝试发送信息之前检查错误的记录。基于这个概念,开发了许多不同的变体,最终被采用的是在纸带上打孔。
有趣的是,19 世纪末的打卡机时代和 20 世纪初期的早期计算机时代,它们以不同的方式达到了相同的解决方案。打卡机的穿孔卡片起源于铁路票,而电报的穿孔卡片则源于纺织行业。
一百多年前,法国的织布工通过在卡片上打印出一系列穿孔图案,并将这些卡片输入织布机,自动化复杂地毯的图案设计。这使得织布工能够更快速地生产高质量的产品,具有更高的艺术性和更大的准确性。
电报进一步改进了这一系统,引入了编码的概念。当目标是操作巨大的织布机中的线程,一行一行地创建复杂的图案时,过于复杂的设计就显得没有意义。每根提升的线对应一个孔,这就足够有效。
然而,当目标是发送远距离信息时,这种字面上的方式就显得低效。电报操作员已经习惯于使用代码来表示不同的字母,但这些代码是为了减少操作员的错误而优化的。例如,在摩尔斯电码中,最常见的字母具有较短的代码。这可以保持传输速度,并减少操作员的压力。一旦电报开始产生一份物理记录,操作员可以在发送信息之前进行二次或三次检查,那么在机器编码的优化方面,性能的最大提升便得以实现。在电码长度为 1 到 5 个单位之间的字母,机器处理起来并不容易。机器在每个字母的长度相等时表现得更好。现在最好的编码是那些稍微复杂一些的、长度固定的编码,最终能够存储更多的数据。
开发了几种不同的系统。第一个被广泛采用的是由埃米尔·博多(Emile Baudot)在 1870 年开发的。这个被称为博多码的系统,也叫国际电报字母表第 1 号,是一个 5 位二进制系统。
快速推进到早期计算机时代,人们开始开发巨大的房间大小的机器,这些机器也使用二进制系统。他们需要一种输入数据和指令的方式,但当时并没有可视界面。直到 1964 年,贝尔实验室才将第一款原始的可视界面融入到 Multics 时间共享系统中,计算机才开始与显示器配合使用。我们当时没有办法看到计算机接收的输入信息,因此我们借用了电报的接口,而电报的接口又借用了 18 世纪法国织布工的接口。
技术也是如此。它按照周期发展,但这些周期偶尔会碰撞、交叉或融合。我们不断借用我们在其他地方看到的想法,要么是为了改进我们的系统,要么是为了给用户一个参考点,使得他们采纳新技术变得更快、更容易。真正的新系统通常会吞并旧系统的接口,以创造可以对齐的差异。
这就是为什么长期维护技术如此困难的原因。虽然盲目追求新事物因为其新颖性而具有危险性,但不与时俱进也是危险的。随着技术的进步,它积累了越来越多的接口和模式。它从其他领域吸收这些元素,并保留了那些已经不再有意义的历史成分。它围绕最深藏的特性建立假设。如果你的系统保持不变太久,你就会陷入迁移数十年假设的困境。
Unix 吃掉了世界
一个常见的建议是,构建成功软件时要保持简单。但究竟是什么让一个设计看起来简单,而另一个设计却显得复杂呢?为什么一行 80 个字符的代码看起来更简单、更易读?它很简短,但如果我告诉你,用户体验研究实际上把理想的字符数定在 50 到 60 个字符之间呢?这意味着 80 个字符比实际测试中效果最好的长度长了 50%。
人类的大脑对熟悉的事物有强烈的偏见。我们把已经知道的概念和构造看作更简单、更容易、更高效,仅仅因为它们对我们来说是已知且舒适的。我们不需要成为某个构造的专家,甚至不一定要喜欢它,熟悉感就会改变我们对它的认知。20 世纪 60 年代,心理学家罗伯特·扎扬茨进行了一系列实验,记录了即使是对某物的单次接触,也能增加我们在后续接触中的正面情绪。他在语言、单词和图像上都发现了这一效应。后来的研究者也观察到了类似的偏好,比如金融专业人士如何进行投资(3),学术研究人员如何评估期刊(4),以及我们在吃东西时喜欢什么味道^(5)。在心理学中,这个现象被称为单纯接触效应。仅仅接触某个概念,会让大脑更容易处理这个概念,因此,用户会觉得它更容易理解。
因此,开发新技术或振兴旧系统最有效的方式往往是建立在熟悉的概念之上。参考点创造了可以对齐的差异,帮助我们评估新事物的价值,但这些相同的参考点也使新技术显得简单易懂,降低了进入的门槛,提高了其被采纳的可能性以及采纳的速度。
考虑一下 Linux 操作系统。它很可能是目前最受欢迎的网络服务器操作系统之一,甚至在计算机领域也是如此。目前有数百个版本可以自由安装,而且还有许多专业版本。Linux 是在激烈的竞争中脱颖而出的无可争议的胜者,它的目标是开发一个既能在多种不同类型的计算机上运行,又不受限制许可证约束的操作系统。
Linux 常被描述为最受欢迎的 Unix 操作系统版本,尽管这两个操作系统在实现方面几乎没有任何相似之处。
Linux 的故事始于 1982 年贝尔系统的解体,距其诞生将近十年。1956 年对 AT&T 的一项同意判令禁止该电信巨头从事“任何除提供公共通信服务以外的业务”。这意味着,当贝尔实验室的计算机科学家 Dennis Ritchie、Ken Thompson 和 Rudd Canaday 在 1970 年代开始开发 Unix 时,没有人确定 AT&T 是否被允许出售它。AT&T 的律师们决定采取保守态度,允许将其源代码和软件一同出售给学术和研究机构。^(6)
拥有源代码使得将 Unix 移植到不同的机器上变得容易,也可以对其进行修改和调试。人们将其打印出来并附上自己的注释。Unix 成为教学中一种便捷的选择,帮助学生理解操作系统是如何工作的。它在各种不同的机构中迅速传播开来,包括大学、博物馆、政府组织,甚至早期还有一所全女子私立学校。
用户开始将他们修改过的 Unix 版本放在磁带上,并相互分发。这些本质上是“分叉”和“拉取请求”,远在这种基础设施出现之前。分享的主要动机是分发错误修复和补丁。
与此同时,AT&T 的律师们正在努力决定如何处理 Unix,并且他们在原始的决策和更为传统的知识产权限制方法之间摇摆不定。Unix 历史学家 Peter Salus 讲述了 AT&T 的开发者是如何积极参与盗版自己知识产权的故事。
大量的错误修复被收集起来,而不是一次性发布,每次修复都会由 Ken [Thompson] 汇总成一个修复带。一些修复非常重要……我怀疑相当一部分修复实际上是由非贝尔公司的人完成的。Ken 尝试发布,但律师们一直拖延,拖延,再拖延。
最终,出于完全的厌恶,有人“发现”了一盘在 Mountain Avenue [贝尔实验室的地址是 600 Mountain Avenue, Murray Hill, NJ] 上的磁带,其中包含了这些修复。
当律师们得知此事后,他们打电话给每一个许可证持有者,威胁如果不销毁录音带,他们将面临严重后果……并试图找出他们是如何得到这盘录音带的。我猜没有人会真正告诉他们是如何得到录音带的(我没有)。这是 AT&T 律师们为了证明自己存在价值并扼杀 UNIX 的第一次尝试。^(7)
当那些在计算机科学课程中学习 UNIX 的大学生毕业并找到工作时,他们把 UNIX 带到了各自的工作岗位。随着每个新版本的推出,AT&T 的许可证变得越来越严格,公司试图弄清楚它在法律上能做些什么来利用这一意外形成的繁荣社区。
然后,在 1982 年,美国司法部解决了对电信行业的第二起反垄断案件,并拆分了“Ma Bell”。AT&T 突然摆脱了那项令其无法完全将 UNIX 视作产品的同意令,它毫不犹豫地开始严厉打压这个在十多年间逐渐壮大的社区。
如果你经历过类似的尝试来阻止分享其他形式的知识产权,比如音乐和电影,你就能理解一旦人们习惯了拥有一个免费且可修改的操作系统 UNIX 后,他们不愿放弃它,也不愿回到以前的状态。剥夺对 UNIX 源代码的访问迫使社区寻找一个开源的、理想情况下免费的替代品。
一个早期的竞争者是伯克利开发的 UNIX 变种,称为伯克利软件发行版(BSD)。BSD 拥有一个日益壮大的社区,但它的基础使用了 UNIX 的部分源代码,因此很快陷入了诉讼之中。UNIX 的继承者需要表现得像 UNIX,但又不包括 AT&T 的任何知识产权。
于是 Linux 诞生了,它是计算机科学学生 Linus Torvalds 作为个人项目开发的。Linux 从来没有意图成为一个完整的操作系统;它只打算成为针对特定芯片架构的内核,而 Torvalds 恰好有该架构的访问权限。因此,Linux 操作系统是由来自其他团队的各种软件拼凑而成的。它的大部分类 Unix 接口来自理查德·斯托尔曼的 GNU 项目,而 GNU 本身设计上并不包含任何 UNIX 代码。
所以在某种意义上,Linux 是 UNIX 的后代,但它没有直接使用 UNIX 的任何代码。但,为什么要坚持保持 UNIX 的外观和感觉呢?一旦决定开始编写一个完全新的系统,那么保持看起来像 UNIX 的形式有什么价值呢?对斯托尔曼来说,情况很明确:自由软件是一项道德使命。目标不是建立一个免费的 UNIX 替代品,而是建立一个免费的替代品,完全取代并淘汰 UNIX。他毫不犹豫地将 GNU 项目的策略描述为极端:
随着 GNU 项目声誉的增长,人们开始主动为该项目捐赠运行 Unix 的机器。这些机器非常有用,因为开发 GNU 组件最简单的方式就是在 Unix 系统上进行,然后逐个替换该系统的组件。但是它们也提出了一个伦理问题:我们是否应该拥有 Unix 的副本?
Unix 是(并且仍然是)专有软件,而 GNU 项目的哲学认为我们不应该使用专有软件。但是,运用与自卫中暴力行为正当化相同的推理,我得出结论,当使用专有软件对开发一个可以帮助他人停止使用该专有软件的自由替代品至关重要时,使用专有软件是合法的。
但是,即使这是一个可以辩解的邪恶,它仍然是一个邪恶。今天我们不再拥有任何 Unix 的副本,因为我们已经用免费的操作系统取而代之。如果我们无法用免费的操作系统替代一台机器的操作系统,我们就换掉了那台机器。^(8)
Stallman 使用 Unix 的接口,因为他明白,如果 GNU 的接口与现有软件的接口匹配,专有软件的用户将有更大的动力去切换。^(9)
让我们再深入一层:为什么 Unix 最初有它现在这样的接口?大多数 Unix 命令是两个字母的缩写,代表那些似乎不需要缩写的词。《The UNIX-HATERS Handbook》的作者将这种接口归因于 Unix 创建者当时可用的硬件:
初学 Unix 的用户总是对 Unix 命令名称的选择感到惊讶。无论在 DOS 或 Mac 上接受多少培训,都无法为那些神秘而美丽的两个字母的命令名称如 cp、rm 和 ls 做好准备。
我们这些使用过 70 年代早期 I/O 设备的人怀疑,退化的根源在于 ASR-33 电传打字机的速度、可靠性,以及最重要的,它的键盘,ASR-33 是当时常见的输入输出设备。与今天的键盘不同,今天的键盘按键行程基于反馈原理,所需的力量仅仅是关闭微动开关的力量,而电传打字机上的按键(至少在记忆中)需要移动超过半英寸,并且需要足够的力量来驱动一个小型电动发电机,就像自行车上的发电机一样。你在这些怪物上打字时,手指关节可能会受伤。
如果 Dennis 和 Ken 使用的是 Selectric 而不是电传打字机,我们可能会打出“copy”和“remove”,而不是“cp”和“rm”。再次证明,技术往往限制我们的选择,而不仅仅是扩展它们。
经过二十多年,继续延续这一传统的理由是什么?历史的强大力量,也就是现有的代码和书籍。如果一个厂商用比如说 remove 替代了 rm,那么每一本描述 Unix 的书籍将不再适用于它的系统,而每个调用 rm 的 shell 脚本也将不再适用。这样的厂商不如直接停止实现 POSIX 标准。
一个世纪前,快速打字员正在使他们的键盘卡住,因此工程师设计了 QWERTY 键盘来减慢他们的速度。计算机键盘不会卡住,但我们今天仍然在使用 QWERTY。一个世纪后,世界依然会在使用 rm。^(10)
就像程序员现在写的代码行适合打孔卡片一样,他们也使用那些接口设计上最适合电传打字机键盘的操作系统。利用熟悉的结构来促进技术的普及可能会创造出奇怪的传统。
继承路径
如果人们更快地接受遵循已知模式的技术,即使他们讨厌这些模式,也值得探讨人们最初是如何接触到这些模式的。从一开始,计算机行业就是一个跨功能的行业。围绕计算机的开发以及最可能使用计算机来做其他工作的职业,形成了人们的网络。在计算机的早期,这意味着计算机用户既是构建应用程序、开发语言和设计架构的计算机科学家,也是像科学家、数学家和银行家这样的专业人士。即使在今天,这些群体仍然倾向于将自己孤立起来,限制了他们接触为其他用例创建的接口。
请考虑以下情况:最早期的成功编程语言之一是 COBOL,然而现代编程语言几乎没有继承 COBOL 的设计模式。例如,我们不会将代码划分为不同的部分,也不会使用句点来结束代码行。很少有程序员会猜到 PIC 是一个可变字符字符串。COBOL 的一些特性出现在其他语言中,但它的语法和接口几乎没有保留下来。相反,COBOL 自己则采纳了许多后来的语言结构,试图清理它的设计。
另一方面,ALGOL60 深刻地影响了几乎所有现代语言的结构和语法,但今天你很难找到一个程序员曾经听说过它。^(11)
当我们审视各种编程语言的成就时,COBOL 显然是赢家。COBOL 程序至今仍然处理着数百万笔交易和数万亿美元的资金流动,从 A 点到 B 点。很难提到 ALGOL60 曾经实现过的任何具有重大意义的事物。与 ALGOL60 同样有影响力且不为人知的语言 BCPL,幸存下来并成为 C 语言的祖先。那么,早期计算机科学家们怎么会比第一种真正成功的跨平台高级编程语言的模式更熟悉那些失败语言的模式呢?
答案是,COBOL 是一种为那些不想理解计算机如何工作的人的需求而设计的语言;他们只是想完成工作。当数据系统语言委员会(CODASYL)在开发 COBOL 时,那些致力于计算机研究与开发的人认为,你应该学习适用于你特定机器的汇编语言。让编程更易于访问、让代码更具人类可读性被视为一种反模式,认为这种做法是在降低编程的美感,迎合不值得的观众。
然而,这些观众实际上是那些为了实际目的而使用计算机的人,他们中的许多人对每次升级机器时都必须重写程序的想法感到无聊。这个群体的人不在乎是否是“真正的程序员”。他们关心的是比竞争对手做得更好、更快。如果可能的话。技术正确性不重要,优雅不重要,执行才是关键,任何能降低使用计算机执行目标的门槛的东西,都比那些更强大但更难学习的工具更可取。
这一时期的计算机科学家们有着相反的动机。尽管 COBOL 用户根据他们通过计算机更快地完成非技术性任务的能力来评判和奖励,但 ALGOL60 用户则根据他们扩展机器原本能做的功能的能力来评判和奖励。通常,这一领域有两种成就类型:让机器做一些新的事情,或者让机器比以前更高效地完成某些事情。对于计算机科学家来说,编程语言本身就是输出。开发完成后,下一步不是编写程序,而是撰写关于该语言的论文,并与其他学者分享以获取反馈和进行研究。
大致上,从 1950 年代到 1970 年代之间,有三种人群在编程计算机:科学家和数学家、数据处理人员,以及学者或计算机研究人员。
科学家和数学家用计算机进行计算,他们更倾向于使用尽可能反映科学和数学符号的语言。这个群体使得 FORTRAN 广为人知。当达特茅斯的两位数学教授希望创建一种更容易让学生学习的编程语言时,他们大量借鉴了 FORTRAN II 的语法,开发出了 BASIC。BASIC 后来衍生出了数百种变种,其中许多至今仍在使用。
数据处理人员使用计算机从一个源读取数据,然后进行计算或以某种方式转换数据,再保存到另一个源。这些正是 COBOL 的用户,而这种语言证明了它的有效性,至今仍在使用。
如果你想证明采用决策受人们网络间共享知识而非严格凭借优点的影响,考虑这一点:今天那些试图替换旧的 COBOL 应用程序的组织,并没有将其迁移到现代编程语言中最适合数据处理的首选语言——Python,而是迁移到继承了 COBOL 作为企业通用语言市场的语言——Java。
语言的设计从来都不是最重要的,重要的是人群。那些原本会成为 COBOL 程序员的人,现在正成为 Java 程序员,这使得 Java 成为自然的选择,尽管它并不是为了处理 COBOL 所优化的用例而设计的。
也许这就是为什么 COBOL 仍然在许多地方存在,并且抵抗了所有试图淘汰它的努力。
学者和计算机研究人员专注于计算机的发展。当他们最终从汇编语言转移时,他们转向了专门用于文档化和实现算法的语言。ALGOL60 可能没有被用来构建很多应用程序,但它是计算机协会(ACM)用于在教科书和学术资源中描述算法的语言,超过了 30 年。这使得它对研究人员后续开发的语言产生了强大的影响。
剑桥大学基于 ALGOL60 开发了剑桥编程语言(CPL)。CPL 后来发展为 BCPL,BCPL 被简化以创建 B,进一步修改后又形成了 C。接着,C 成为了这一群体的首选编程语言,并促成了大量编程语言的开发,这些语言被各种各样的程序员使用:Java、Go、PHP(通过 Perl)、Ruby、Python 和 Swift。
对这一群体来说,Lisp 也很受欢迎。因为最初的 Lisp 只是一个理论设计文档,直到今天,不同的实现层出不穷,紧随其后的是无果的标准化尝试。在 1960 和 1970 年代,Lisp 与人工智能研究紧密相关,并且基本上被归类为这一领域的利基技术。具有讽刺意味的是,在我们的计算时代,人工智能取得了更多的进展,但 Lisp 几乎没有发挥关键作用。相反,今天的 Lisp 被看作是一类通用编程语言,偶尔将一些思想和结构注入到更主流的语言中。
所以,计算机科学历史上的这一关键时刻,出现了两类人群,他们编程是为了实现一些与计算机本身无关的实际目的,还有一类人群则与计算机合作,推动计算机本身能够做的事情的边界。现存的大多数编程语言保留了这些第三类程序员熟悉的构造,尽管 COBOL、FORTRAN 和 BASIC 拥有更广泛的用户群体。
总体而言,接口和理念通过人际网络传播,而不是基于优点或成功。接触到某种配置会产生一种认为它更容易且更直观的感知,从而使其传递给更多代的技术。这里要学到的教训是,那些对人们来说熟悉的系统,总是比那些有结构优雅但与预期相悖的系统提供更多价值。
在接近遗留系统时利用接口
当我在处理遗留系统时,我总是首先评估潜在用户。谁将长期维护这个系统?他们习惯使用哪些技术?谁将最常使用这个系统?他们期望系统如何工作?
这并不意味着不能改变事物或引入新概念。特别是如果系统已有几十年历史,接口可能与不再合理的过程和关联捆绑在一起,就像 80 字符的行源自打孔卡,两个字符的 Linux 命令源自电传打字机,桌面应用程序上的保存图标是软盘一样。有时候,改变接口以去除不再相关的需求是件好事。如果系统是全新的,那么定义今天一个最小可行产品(MVP)的需求是什么,是在制定攻击计划时进行思维实验的一个好方法。
然而,即使变化的结果是净正面的,改变接口也不是免费的。让人们思考会增加摩擦,增加失败的可能性,即使新的接口更好,更符合产品的整体愿景。
工程师往往高估了秩序和整洁的价值。计算机系统真正重要的,只有它在执行实际应用时的有效性。Linux 之所以能够主导操作系统世界,并不是因为它从零开始经过精心设计;它是从多个不同系统中收集了想法和实现,集中力量在一个关键地方——内核上,创造了价值。
即便是撰写学术论文的愿望已被撰写流行博客的愿望所取代,仍然存在奖励个人软件工程师独特性、创新能力,或以创新方式完成旧事物的激励机制。然而,当技术建立在共同的事物之上时,它更有可能取得成功。这两股力量在任何软件项目中总是相互冲突,但遗留系统尤其容易受到影响。
我们知道,例如,基于现有解决方案进行迭代更可能改善软件,而不是进行完全重写。完全重写的风险已经有许多文献记录。Fog Creek Software 和 Stack Overflow 的 Joel Spolsky 将其描述为“任何软件公司都可能犯下的最糟糕战略错误。”^(13) 微软初创企业部门总经理 Chad Fowler 是这样描述的:
几乎所有生产软件的状态都糟糕到几乎无法作为重新实现自身的指导。现在,拿这个已经糟糕的情况,只挑选出那些足够庞大、复杂且脆弱到需要大规模重写的产品,那么采用这种方法成功的概率就会显著降低。^(14)
Fred Brooks 在 1975 年创造了第二系统综合症这个术语,用以解释这种完全重写所产生的臃肿、低效且常常无法正常运行的软件现象。但他将这些问题归因于监督重写的架构师的经验,而不是重写本身。第二系统综合症中的“第二系统”并不是现有系统的第二个版本,而是架构师所制作的第二个系统。Brooks 的看法是,架构师在做第一个系统时因为没有做过软件,所以会更严格,但在做第二个系统时,他们过于自信,加入了各种华而不实的装饰和功能,最终使事情变得过于复杂。等到他们做第三个系统时,才会吸取教训。
不幸的是,当面临现有系统的问题时,工程团队往往会产生从零开始构建的最大动力。那些旨在逐步修复和恢复运营卓越性的举措,就像修缮一座老房子一样,通常在工程团队中很少有人愿意参与。这是因为 Zajonc 的单纯接触效应有一个上限。到了一定程度,熟悉感会导致轻蔑。
从经济学角度来看,风险和模糊性之间是有区别的。^(15) 风险是已知且可估算的威胁;而模糊性则是那些正负结果都未知的地方。传统的思维方式告诉我们,人类对模糊性有厌恶感,会尽可能避免它。然而,模糊性回避是那些在实验室中表现良好的决策模型,但当应用到现实世界时会出现问题,因为现实中的决策更复杂,概率定义也不那么清晰。特别是当决策涉及多个属性时,问题的积极框架可以让人们从回避模糊性转向寻求模糊性。^(16)
撇开个人赞誉的动机不谈,工程团队倾向于进行全面重写,因为他们错误地将旧系统视为规范。他们认为,既然旧系统能工作,那么所有技术挑战和潜在问题都已经解决。风险已经消除!他们可以为新系统添加更多功能,或在不担心的情况下改变底层架构。要么他们没有察觉到这些变化带来的模糊性,要么他们将这种模糊性视为积极因素,想象着性能的提升和更大创新潜力。
与此同时,现有系统的模糊性已所剩无几。它就是它,假设的潜力已被耗尽。我们知道,在超出仅仅接触的上限后,一旦人们发现了他们不喜欢的特性,他们往往会对之后发现的每个特性进行更负面的评判。^(17) 所以程序员更喜欢进行全面重写而不是迭代旧系统,因为重写保持了吸引人的模糊性,而现有系统已经非常熟悉,因此显得乏味。提出全面重写的提案通常会引入一些对工程团队来说新的语言、设计模式或技术,这并非偶然。很少有重写计划是以使用相同语言重新设计系统或仅仅解决一个明确定义的结构问题的形式出现的。全面重写的目标是恢复模糊性,从而恢复热情。而它们失败的原因在于,假设旧系统可以作为规范,并被信任为准确诊断并消除了所有风险,这个假设是错误的。
小心人为一致性
在下一章中,我将详细介绍如何平衡这些紧张关系,以便制定策略,决定何时重构和重写,何时利用现有的、熟悉的接口。但目前为止,从这次关于特征如何传递的探索中,我们应该得到的结论是,简洁的感知受到你在技术使用场景中所接触到的事物的影响。当事物变得熟悉时,它们看起来更容易。熟悉感是由你与技术互动的方式以及与你一起使用技术的人决定的。
但熟悉性也有其弊端。在处理遗留系统时,你会遇到许多提案,声称通过建立人为一致性来改善系统。人为一致性意味着将设计模式和解决方案限制在一个小范围内,可以在整个架构中标准化并反复使用,而这种做法并没有提供技术价值。理解人为一致性的重要点是,它关注的是形式和分类的一致性,而非功能的一致性。举个例子,Node.js 和 React.js 都是 JavaScript 的形式。这两种技术看起来一致,但它们做的事情不同,并且构建在不同的抽象层上。它们都是 JavaScript 的形式,并不能让 Node.js 在与 React.js 互动时比任何其他后端语言更具优势。一个工程师在其中一者上的技能,并不一定能转化到另一者上。
人为一致性可以为非技术过程带来价值。例如,标准化使用一种编程语言可以让招聘、雇佣和最终共享工程资源变得更加容易。但当现代化努力的主要目的是提供技术价值时,要小心不要被这种假设迷惑:即看起来相同,或者我们用相同的词来描述的东西,实际上并不一定能更好地集成。
另一个人为一致性发挥作用的地方是在数据库上。十年前数据库的首选不再是今天的首选,因此高层领导有时会要求将遗留数据库迁移到与新系统使用的数据库更一致的选项中。就像前面的例子一样,这样做有合理的非技术原因,比如不想承担同时支持两种基本相同数据库的费用,但当工程团队被要求移除用于缓存的键值存储,转而使用关系数据库时,问题很容易失控。
确定一致性何时能带来技术价值,何时只是人为的,是工程团队必须做出的最艰难决策之一。人类是模式匹配机器。发现熟悉的事物更容易的反面是,我们往往会过度优化,屈从于人为的一致性,尽管有更好的工具可以使用。
第三章:评估你的架构
当人们谈论他们现代化计划的各个阶段时,如果是从他们将使用哪些技术的角度出发,而不是从他们将带来什么价值的角度出发,这对我来说是一个很大的警告信号。这种区分通常是一个很明显的标志,表明他们认为任何新事物都一定比他们已经拥有的更好、更先进。
可能看起来挑剔,但专注于语言是保持现代化进程顺利进行的一个关键部分。团队往往会朝着他们所关注的方向发展。如果我们在谈论我们所做的事情时侧重于技术选择,用户的需求就会被忽视。发现价值的最佳方式是专注于他们的需求。
在围绕新的遗留系统制定战略时,我始终牢记三个原则。第一章和第二章的历史回顾详细阐述了这些原则:
-
现代化应基于增加价值,而非追逐新技术。
-
熟悉的界面有助于加速采用。
-
人们通过他们的网络获得对界面和技术的认识,而不一定是通过流行度。
但对于大多数组织而言,关于现代化的讨论从失败开始。如果系统运行良好,没人会投入时间和精力。遗留系统现代化这一术语本身有些误导。许多老旧系统因为运作正常,没人考虑更换。
因此,在制定攻击计划时,最后需要考虑的事情是导致最初现代化需求的故障的具体性质。很可能,你正在处理以下一个或多个问题:技术债务、性能差或不稳定性。
问题 1:技术债务
老旧系统不需要仅仅因为它们老旧而进行现代化。许多技术几十年来没有发生根本变化。迁移到最新最先进的技术有时会带来更多问题,而非解决问题。
以下情况可能需要进行现代化:
-
代码难以理解。它引用的决策或架构选择已经不再相关,且机构记忆已丧失。
-
合格的工程师候选人稀缺。
-
硬件替换零件难以找到。
-
该技术已经无法高效地执行其功能。
遗留系统和技术债务这两个术语常常被混淆。它们是不同的概念,尽管一个系统可能同时表现出这两种问题的迹象。
遗留系统是指一个旧系统。它的设计模式相对一致,但已经过时。升级底层基础设施的能力会带来性能的提升。由于新工程师与遗留系统所使用的技术之间存在技能差距,因此他们很难适应。
与此相反,技术债务可以(并且确实会)在任何阶段发生。它是次优权衡的产物:部分迁移、快速修补和过时或不必要的依赖项。技术债务最有可能发生在假设或需求发生变化时,组织选择快速修复而不是预算时间和资源以适应变化。与遗留系统不同,这种情况下的性能问题通常是低效代码的副产品,而非过时的基础设施。升级基础设施——增加内存和核心或添加服务器——并不总是能带来等比例的性能提升。
拥有大量技术债务的系统还会使得新工程师的加入变得困难,但在这种情况下,困难在于应用程序的内部逻辑没有意义。也许文档已经过时,或者抽象层次堆积在一起,或者函数命名不直观。
管理技术债务就是要恢复一致性。一种处理这一挑战的好方法是进行一次产品发现练习,就好像你打算构建一个全新的系统一样,但实际上并不构建一个!相反,使用这个新的愿景来挖掘并重新聚焦当前的系统。
随着时间的推移,需求自然发生变化。随着需求的变化,使用模式也会发生变化,最有效的组织和设计也会变化。使用产品发现方法重新定义你的 MVP,然后找到现有代码中 MVP 所在的位置。这些功能和特性是如何组织的?如果今天你来组织它们,会怎么做?
另一个处理技术债务时有用的练习是,将系统最初构建时可用的技术与我们今天为这些相同需求使用的技术进行比较。当处理用 COBOL 编写的系统时,我经常采用这种方法。尽管人们常说 COBOL 正在消亡,但它在某些任务上确实表现得很好。大多数老旧的 COBOL 系统的问题在于,它们是在 COBOL 是唯一选择的时代设计的。如果目标是摆脱 COBOL,我会首先整理出哪些部分的系统是用 COBOL 编写的,因为 COBOL 擅长执行这些任务,哪些部分是用 COBOL 编写的,因为当时没有其他工具可用。一旦我们有了这个映射,我们就可以开始将后者拆分成独立的服务,使用今天我们为这些任务选择的技术来编写和设计。
示例:总账
其中一个债务负担沉重的系统被设计为一个总账,用于一个大型医疗保健组织。它是一个复杂的系统,涉及多个主机协同工作。它处理来自其他主机的请求,这些主机支撑着需要发放付款的其他系统。总账的核心功能是授权并发放组织向第三方的付款。因此,该系统必须确保组织有足够的资金发放付款,请求是有效的,请求不是重复的,并且请求的情况符合所有相关的规定。此外,该系统还跟踪组织欠款,发送提醒债务人付款的请求,并为各方利益相关者生成报告。
当前的系统根据部门组织代码——例如,贷款和应付账款是系统内的不同应用程序,尽管它们有重叠的需求——并且是用 COBOL 或特定于主机的汇编语言编写的,通常运行其作业。总体而言,系统看起来像图 3-1。
很容易看出这个系统是如何以这种方式发展的。该组织规模庞大,资金充足,在计算机首次引入市场时,它立刻利用了计算机的优势(因此有了汇编语言)。该组织将纸质流程迁移到数字流程,几乎没有做出改变,并在技术中保持了原有的流程边界。

图 3-1:与总账交互的应用程序
当时,计算机是“附加设备”,是为了加快工作速度而设计的实验性大玩具,并非每个业务单元都认为这些新机器能够为它们的流程增值。最终的系统按业务单元划分,因为技术的采纳是逐步进行的,逐个单元地推进。
但是今天,计算机是默认选择,因此我们不会以这种方式构建系统。我们可能会保留应用程序与部门的映射关系,但我们会构建反映它们共享需求的共享服务。一些功能发挥了 COBOL 处理大量财务数据的优势,但在生成报告或发送邮件时,COBOL 并不一定能提供太多帮助。
在现代化这个系统时,我会识别出合适的共享服务,然后选择一个来构建。理想的情况是,当我能识别出一个仅需要提议的共享服务之一的应用程序时。我们构建该服务并重写该应用程序以使用它。然后我们回过头来,找一个需要该共享服务加上我们清单中的另一个共享服务的应用程序。我们构建第二个共享服务,并重写该应用程序以同时使用这两者。
然而,大型系统中的应用程序很少能够按复杂度的升序排列。更可能的是,我们需要先提取一个共享服务,并一一重写每个应用程序,然后再提取第二个共享服务,并一一重写每个应用程序。这可能令人沮丧,但重要的是,在我们对新服务的正常行为有足够经验之前,不能对其增加负载。
问题 2:性能问题
性能问题实际上是与遗留系统相关的较为轻松的难题之一。很少有组织在遗留系统开始影响业务运作、工作开始变慢之前,会主动采取措施。有时这是因为系统本身变慢了,但更可能的是,系统的性能保持相对稳定,而它周围的所有其他事物却变得更快了。
通常来说,关于某个任务需要多久完成以及需要多少资源的问题是高度主观的。人们倾向于认为当前的状态是可以接受的,尤其是当他们对其他系统经验有限时。如果组织认为其系统存在性能问题,那么“更好”的定义工作就已经由你完成了。除非拥有该系统的组织已明确了期望,否则一个系统不可能有性能问题。
本书将一再强调权衡取舍的信息。对现有系统所做的任何更改都是有代价的。改善系统某一特性的更改通常会使其他方面变得更加困难。擅长遗留现代化的团队知道如何识别权衡取舍并协商出最佳的解决方案。你必须选择一个目标或特性来优化,并为所有其他特性设定预算,这样你才能知道在开始失去价值之前,你愿意放弃多少。
为了提高速度,是否值得牺牲一些准确性?当迁移到托管服务使得本地测试更加困难时,是否值得迁移?当一个组织已经决定其系统存在性能问题时,回答这些问题就容易多了。组织必须对性能应该有多快或需要花费多少资金来满足要求有所预期。
一旦定义了性能要求,评估遗留系统并制定策略的任务就变成了列出一个给定任务中的所有步骤,并识别性能瓶颈。将这一点列出之后,你可以优先考虑改进,先从能够获得最大收益的领域开始。
解决每一个瓶颈并不一定要求完全消除它。如果你能做到这一点,那很好,但在大多数情况下,你会发现为了消除它所需的投入与性能提升不成正比。不要低估 5%、10%和 20%性能提升的力量。只要你采取的方法能使系统朝着更好的整体状态前进,5%的提升可以随着项目的推进产生回报。其他改动可能会让这 5%的提升在以后变成 30%甚至 50%的收益。
话虽如此,不要仅仅为了修补问题并获得性能提升而抛弃工程最佳实践和良好的架构。你可以通过以下特点识别这种解决方案:它们往往避免触及显然的根本问题。提出这些解决方案的人通常对系统的问题感到沮丧,并且被逐步改进所需投入的时间(几个月或几年)压倒。它们反对那种让系统变得更好的 5%的改动,因为他们认为 5%的改进永远不够。因此,他们提出一种能够提供更大性能提升的解决方案,但这种方案可能会加剧根本问题或使以后修复变得更加困难。举个例子,我们有一个系统,其中多个服务需要访问一个庞大的无结构数据存储。数据的规模已经增长到,删除其中一些数据所需的过程非常消耗资源,已经影响了正常的读取和写入性能。
问题在于数据的无结构性,以及同时需要访问这些数据的众多服务,但这是一个很难解决的问题。将数据拆解、适当结构化并迁移服务将需要几个月,甚至几年时间。相反,项目中的工程师们希望构建一个垃圾回收服务,在低流量期间执行删除操作,因为此时性能影响较小。
这种方法有什么问题呢?首先,创建一个新服务不是一件小事,创建之后,它还需要维护、监控、测试和扩展。更重要的是,这个新服务是一个抽象,它在正常流程之外执行一个潜在的危险操作。是什么触发了这个服务,我们怎么知道它正在执行的任务是正确的?添加一个新服务只会增加系统的整体复杂性,目的是利用暂时的情况。随着负载的增加,这些低流量窗口将变得越来越小,越来越难以找到。
如果这个系统能够正常运行,它将带来巨大的性能提升,为组织争取到时间去解决真正的问题。当然,提出这个系统的工程师们的初衷就是如此。但也有可能,一旦这样的临时修补措施到位,组织就会失去解决真正问题的兴趣,而这个团队的工作也不过是将定时炸弹的倒计时重置了而已。
更聪明的做法是寻找突破数据的“微小步骤”,这些步骤将产生 5%或 10%的提升。如果你发现足够多这样的提升,这些提升会积累起来。
大问题总是通过将其分解为小问题来解决。解决足够多的小问题,最终大问题会崩塌并得以解决。
示例:案件流管理
用于通过多阶段审批流程管理应用程序的软件,随着时间的推移,逐渐变成了性能战场。这里有一个例子,我们可以通过找到足够多的瓶颈并逐步消除它们来提高系统的输出。这种应用审批流程背后的技术相当不错,但其中一些部分是自动化的,另一些则是手动的。有些部分是数字化的,有些仍然是纸质的。有些部分最近才数字化,而有些则是 20 年前数字化的。大家都同意,如果剩下的可以自动化的部分能被自动化,流程中的纸质部分能被数字化,系统中较旧的组件能得到更新,整个系统会更好,但这是一长串的改进清单。
并非所有优先级最高的任务都会影响处理申请所需的时间。例如,在流程的某个阶段,申请人必须签署一份同意书,授权组织进行背景调查。虽然纸质表单可以用简单的网页表单或与第三方服务的集成来替代,但这一部分申请流程通常与其余申请的处理是并行进行的。因此,数字化这一步骤并不会实际加速单个申请的总处理时间。
其他看似无关的问题可能会带来更大的差异。案件是批量发送到背景调查服务的。如果该批次中的一个申请出现问题,所有该批次的申请都必须等问题解决后才能继续。仅仅将作业重新配置为每次一个申请处理,就能节省大量时间。
团队没有仅仅关注系统的纯技术改进,而是通过追踪申请的流程来减少处理每个申请的时间。他们已经做了艰苦的工作,确定一个更好的系统意味着更快的申请处理时间,并且他们围绕这一目标优化了他们的方法。
问题 3:稳定性问题
另一方面,一些遗留系统能够在组织成功所需的参数范围内执行核心功能,但它们不稳定。它们并不太慢;它们能产生正确的结果,并且在组织可用资源范围内完成任务,但经常会有“意外”发生,比如出现奇怪的黑天鹅式根本原因的停机事件,或者常规升级有时会失败得很严重。因为不可预见的技术冲突出现并需要解决,正在进行的开发工作被迫停止。
1983 年,查尔斯·佩罗(Charles Perrow)创造了正常事故这一术语,用来描述那些极易发生故障的系统,即使采取再多的安全措施,也无法完全消除事故。根据佩罗的说法,正常事故并非由不良技术或无能的员工所导致。经历正常事故的系统展现出两个重要特征。
它们是紧密耦合的。当两个独立的组件彼此依赖时,称之为耦合。在紧密耦合的情况下,一个组件的变化很可能会影响另一个组件。例如,如果一个代码库的更改需要相应地更改另一个代码库,那么这两个代码库就是紧密耦合的。而松散耦合的组件则是指一个组件的变化不一定会影响到另一个组件。
紧密耦合的系统会产生连锁反应。一处变化会在系统的另一部分引发响应,进而在系统的另一部分产生响应。就像多米诺效应一样,系统的各个部分开始执行,而无需人工操作员指示它们去做。如果系统比较简单,通常可以预测故障发生的方式并加以防范,这也是经历正常事故的系统的第二个特征。
它们是复杂的。大规模系统通常是复杂的,但并非所有复杂的系统都是庞大的。软件复杂性的迹象包括直接依赖的数量和依赖树的深度、集成的数量、用户的层级结构和委托的能力、系统必须控制的边缘情况数量、来自不可信来源的输入量、这些输入的法律差异等。计算机系统随着时间的推移自然变得更加复杂,因为随着年龄的增长,我们往往会不断为它们添加更多的功能,这会增加至少一些这样的特征。计算机系统也往往在开始时是紧密耦合的,如果不定期进行代码重构,它们实际上可能一直保持这种状态。
紧密耦合和复杂的系统容易发生故障,因为耦合会产生连锁反应,而复杂性使得这些连锁反应的方向和过程难以预测。
如果你的目标是减少故障或最小化安全风险,最好的方法是从这两个特征入手评估系统:哪里是紧密耦合,哪里是复杂的?你的目标不应是消除所有的复杂性和耦合;在每个具体情况下都会有权衡。
假设你有三个服务需要访问相同的数据。如果你配置它们使用同一个数据库,它们就会形成紧密耦合(图 3-2)。

图 3-2:紧密耦合的服务
这种耦合会带来一些潜在问题。首先,三个服务中的任何一个都可能更改数据,从而破坏另外两个服务的正常运行。任何数据库架构的更改都必须在所有三个服务之间协调。通过共享数据库,你失去了拥有三个独立服务的扩展优势,因为当一个服务的负载增加时,这个负载会传递到数据库,而其他服务的性能则会下降。
然而,给每个服务分配独立的数据库,会将这些问题转化为其他潜在的问题。现在你必须弄清楚如何保持这三个独立数据库之间的数据一致性。
放松两个组件之间的耦合通常会导致创建额外的抽象层,这会增加系统的复杂性。简化系统复杂性通常意味着更多的公共组件重用,这会加剧耦合。问题不在于将你的遗留系统转变为完全简单且没有耦合的系统,而在于战略性地考虑你在哪些地方耦合、在哪些地方复杂,以及这种耦合和复杂的程度。复杂性较高的地方是人类操作员容易出错和误解的地方;紧密耦合的地方则是加速的领域,好的和坏的效果都会更快传播,这意味着干预的时间会更少。
一旦你识别出系统中紧密耦合和复杂的部分,研究这些区域在过去问题中的作用。改变复杂性与耦合的比例会使这些问题得到改善,还是变得更糟?
一个有用的思考方式是将你目前遇到的故障类型进行分类。由人为原因引起的问题——如未能阅读、理解或检查某些内容——通常可以通过简化复杂性来改进。由监控或测试失败引起的问题,通常可以通过放松耦合来改善(从而为自动化测试创造空间)。还要记住,一个事故可能包含这两种因素,所以在分析时要有思考。一名操作员可能因为犯错触发了事故,但如果这个错误因为日志不够详细而无法被发现,那么简化复杂性所带来的效果,就不如改变耦合来得显著。
示例:自定义配置
假设有一个组织希望增强其单体应用中自定义配置的功能。它构建了一个配置服务,使得软件工程师可以通过单体应用的代码设置标志(图 3-3)。应用程序向该服务发送带有用户身份的请求,以获取相应的配置值。由于这些值很少更改,90%以上的请求通过缓存处理。如果缓存失败,请求将转发到一个简单的 Web 服务,该服务会立即重试缓存,最终返回数据库以检索配置设置。该数据库与单体应用的数据库是分开的,但它运行在相同的虚拟机(VM)上。来自应用程序的流量直接与单体应用的数据库连接。自定义配置数据库使用约 1% 的虚拟机资源。
当服务从数据库接收到配置值时,它会更新缓存并将数据返回给单体应用。数据

图 3-3:请求通过自定义配置服务流动
自定义配置以键值对的形式存储,其中键是用户的身份,值是包含所有相关配置设置的字典。由于可能的自定义选项几乎是无限的,这些字典没有标准的模式。如果用户没有为某个标志设置配置值,该标志在字典中将完全不存在。缓存保留了这种结构。
总的来说,这个服务对组织表现良好,但它有一些工程师很难重现,甚至更难诊断的怪癖。一些问题追溯到缓存雪崩。用户在设置值后很少更改它们,但在缓存确实需要更新的少数情况中,整个字典都会受到影响。
我们如何从复杂性和耦合性角度来看待系统的这一部分?单体应用的行为与配置服务耦合。如果配置服务出现故障,单体应用要么无法完成请求,要么回退到一个可能完全改变用户体验的默认值。如果配置服务发生部分故障,单体应用的行为将变得极为不可预测。
将数据库托管在同一虚拟机上会在单体应用和配置服务之间产生耦合。如果单体应用的数据库出现性能问题,配置服务的数据库也会受到影响,反之亦然。然而,在这种情况下,通过将配置服务的数据库迁移到自己的虚拟机来解决问题可能没有太大价值。如果单体应用的数据库出现问题,产品本身可能已经宕机,这使得该服务的性能几乎无关紧要。由于该服务只使用了虚拟机 1%的资源,因此不太可能在不先触发值班工程师报警的情况下影响单体应用。我们可能希望出于适当扩展的考虑将它们分开,但这会增加我们支付的虚拟机数量,并不一定带来比架构图上外观改进更多的价值。
从复杂性角度来看,数据结构可能是一个不太理想的设计选择。当单体应用发出请求时,它并不需要为用户设置所有的值,只需要当前时刻相关的值。如果在键值存储中的键是用户 ID 加上标志 ID,数据就可以是平坦的,这样可以减轻缓存雪崩的风险。另一方面,我们也可以保持数据结构不变,改变单体应用的假设,使其只请求一次用户的字典,并将返回的数据存储在内存中,直到会话结束。这种方案最小化了单体应用和服务之间的耦合,但却增加了复杂性。我们需要了解在任何时刻我们将存储多少数据在内存中,以及在什么级别上它会变得有问题。我们需要定义一个生存时间,并决定如何实现它。我们是否希望确保所有用户的请求都指向同一台服务器,还是应该假设如果会话仍然有效,应用集群中的所有服务器至少会查询一次配置服务,并将相同的数据存储在它们的内存中?
现代化计划的阶段
有一天,在一次一对一会议中,我团队中的一位工程师坦言,我们在处理一个遗留系统时完全走错了方向。我最近将一位新工程师加入团队,并明确指示她彻底审查系统的测试套件。尽管这些测试是全面的,覆盖面很好,但它们脆弱、不够组织,且难以理解。这反映了系统的整体设计,因此新工程师着手重构了代码的组织方式,使其更易于测试,并让测试更可靠。
看着新工程师的贡献,我的工程师知道这个配置更好。几个月来,我们一直在处理这个系统。她为自己没有像新来的人那样看待问题而懊悔。“我们太实用主义了,”她说,“我们只是顺应了系统现有的模式,而本应该重做它。”
我不同意。我的工程师忘记了,当我们接手这个系统时,它是不稳定的。很多事情经常悄无声息地出错。错误没有得到妥善处理或记录。性能也是一个问题。
学习如何拥有那种新工程师所展示的技术视野非常好。我当然不会阻止我的团队研究她的贡献,但一开始务实是正确的。当你首次接手一个遗留系统时,你根本不可能足够理解它,无法立刻进行大规模的修改。作为这些务实变动的一部分,我们还投入了大量时间来记录和研究系统。说实话,新工程师的第一个任务是一系列小的、务实的变动,旨在帮助她也能熟悉系统,但到那个时候,我的工程师们已经非常了解这个系统,他们能够更快地帮助她上手。她在几天内就完成了那些任务。
“你认为如果我们在处理常规事件的同时进行一次重大重构,会对你产生什么影响?”我问。
“那会真的很有压力。”
事实上,非常有压力,以至于可能会影响团队的判断。这些就是人们感到沮丧并开始说服自己最好的做法是把整个系统扔掉,从头开始重建的情形。
当你的遗留系统缺乏可观察性和测试时,可观察性应该放在第一位。测试只告诉你什么是不会失败的;监控告诉你什么是正在失败的。我们的新工程师能够自由地修改系统的大部分,因为团队在实施更好的监控后,意味着当她的修改被部署时,我们能迅速发现问题。
但这里真正的教训是,现代化计划会随着进展而发展。第一阶段是评估阶段。这不一定意味着你应该停下所有工作并制定复杂的大计划,但你应该专注于立刻解决的低难度问题,并进行务实的修复。利用这些小任务来聚焦于对系统本身的调查。了解它及其独特性。你的监控盲点在哪里?修改、测试并确信它们能正常工作有多容易?官方文档说某些功能是这样做的,但实际上并非如此的地方在哪里?有多少死代码?等等,等等。
当你的团队足够了解系统时,你可以扩大范围,关注系统中的整体问题。事情是否按应有的方式组织?现在是否有更好的技术可以纳入,也许是另一种编程语言或一种新工具?
对于特别大的系统,最好将此过程做成迭代式的多级过程。换句话说,选择系统中的某一部分并专注于此。先关注小的务实问题,然后关注该组件中的更全局性的问题。再进一步回顾,看看系统中的其他地方是否也存在类似的全局性问题,然后再决定如何处理它们。然后缩小视角,解决该组件的全局问题,再继续下一个组件。这样进行本地-全局-超级全局的循环,直到系统达到你需要的状态。
团队对系统及其特殊之处理解得越深入,系统的日常行为就越可预测,也越容易做出大规模的更改。
没有银弹
现代化遗留系统的唯一真正规则就是没有银弹。接下来的章节将概述不同的开发活动组织方式。你很可能会在一个大型项目的不同阶段使用到它们中的所有方法。
需要记住的关键点是,这是一套工具包。你将大问题拆解成小问题,并选择一个能够给你提供最高成功概率的工具来解决特定问题。当然,你可能会比其他方法更频繁地使用某些方法,但每个大规模的遗留系统都至少有一个棘手的问题需要应对。如果你只会解决圆形孔洞的问题,那么任务是无法完成的。
完全重写
完全重写就是字面意思:你从头开始,旨在构建一个全新的系统。这种方法的问题在于,在构建新系统的同时,旧系统该如何处理?一些组织选择将旧系统置于“维护模式”,只为其提供必要的资源,用于修补和修复,以保持其正常运作。如果新项目进度落后(几乎肯定会),旧系统会继续退化。如果新项目失败并被取消,那么在此期间,旧系统和运营卓越之间的差距将显著加大。
新系统投入运行的时间越长,用户和业务方就需要等待新功能的时间越长。忽视业务需求会破坏与工程部门的信任,从而使工程部门未来更难以争取到资源。
另一方面,如果在构建新系统的同时继续开发旧系统,那么保持两个团队之间的设计决策同步将是一个相当大的挑战。如果这些系统处理数据,而几乎所有计算机系统都会处理数据,那么将数据从一个系统迁移到另一个系统就成为了一个巨大的挑战。
另一个需要考虑的因素是参与的人。谁来负责新系统的开发,谁来承担旧系统的维护任务?如果旧系统是用一种过时的技术编写的,只对该系统本身相关,那么维护旧系统的团队实际上就是在等待被解雇。不要自欺欺人,他们知道这一点。因此,如果维护旧系统的人没有参与新系统的建设,你应该预期他们也在寻找新工作。如果他们在新系统投入运行之前离开,你不仅失去了他们的专业知识,还失去了他们的内部知识。
尽管如此,大型现代化项目中的许多小部分并不会通过任何形式的迭代得到显著改进。如果你有一个用 ActionScript 编写的界面,可能最好是直接重写它,并作为完全替代版本推入生产环境。
就地迭代
如果你有一个正常工作的系统,有时候最简单的方法就是不断迭代它,直到它看起来符合你的需求。这在管理技术债务时效果很好,但你也可以在需要重新做架构时使用这种方法。要让就地迭代生效,必须进行一定的准备工作。你需要设置监控。至少,你应该有一种方法来跟踪应用层的错误并搜索日志,但这一工具每年都在不断变得更加复杂。你越能识别出遗留系统的正常状态,就越容易安全地进行就地迭代。
另一个需要确保你有成熟方法的领域是测试。测试应该自动运行,无需人工手动跟踪测试用例。测试还应该是多层次的,既测试小的代码单元,也测试整个流程的端到端。编写好的测试需要技巧,关于这个主题的书籍已经写了整整一本,所以我不会在这里尝试用几段话来总结。对于遗留系统现代化,最相关的指南是迈克尔·费瑟斯的《高效使用遗留代码》。
最后,确保你的团队能够快速从失败中恢复。这通常是工程中的最佳实践,但如果你正在对生产系统进行更改时尤其重要。如果你从未从备份中恢复过,你实际上并没有备份。如果你从未切换到另一个区域,你实际上没有故障转移。如果你从未回滚过部署,你的部署流水线就不成熟。
如果你有一个好的监控策略、良好的测试策略,并且能够快速回滚变更,你就能够自信地对你的遗留系统进行几乎任何修改。
尽管这可能看起来有风险,但应该将就地迭代视为默认方法。在大多数情况下,这最有可能产生成功的结果。
就地拆分
原地分拆是原地迭代的一种变体,专门用于拆分系统。这可以意味着从单体结构过渡到面向服务的架构,也可以意味着将紧密耦合的两个组件解耦。与原地迭代的区别在于,你通过将拆分后的部分重新整合来完成分拆。换句话说,当你从单体系统中抽离一个服务时,该服务很可能仍然需要从单体系统中接收输入并向其发送输出。所以你构建这个独立的服务,并最终将其与单体系统连接,然后再继续拆分下一个服务。你不断地做这个(拆分服务并重新整合),直到把整个项目拆分成一组组小的基于服务的代码。
蓝绿部署
部署中的一个常见模式,蓝绿部署技术涉及将两个组件并行运行,并逐渐将流量从一个组件转移到另一个组件。这样做的最大好处是,如果出现问题,容易撤销。通常,在技术领域,增加负载会暴露出在测试中未发现的问题。遗留系统既有现有用户和活动的优势,也有其困扰。替代它们的系统只有在高负载下发现问题的短暂宽限期。蓝绿部署允许新系统逐步承载旧系统的全部负载,你可以在负载加重之前解决问题。
硬性截止
硬性截止是一种部署策略,其中新系统或组件一次性完全替代旧系统。这是现代化工具箱中最具风险的策略之一。
硬性截止通常分阶段进行,通常按环境或地区进行。一个组织可能首先在低流量地区进行部署,监控是否有问题,然后再部署到高流量地区。这为组织提供了蓝绿部署的一些好处,因为它可以在更新中途停止(并理想情况下回滚),但这种方法不如蓝绿部署精确。环境和地区之间的差异可能并不完全可预测,问题可能被忽视。
如果你没有多个地区,或者正在使用由用户安装的软件并且无法控制有多少用户能访问新版本,那么你可能别无选择。在后一种情况下,Alpha 和 Beta 测试组有所帮助;在前一种情况下,确保你能够撤销任何更改(无论是通过恢复备份还是通过版本控制系统中的还原/回滚命令)会有所帮助。
综合起来
良好的规划更多的是关于在组织内设定预期,而不是控制每个细节。你的计划将定义现代化遗留系统的含义,目标是什么,什么时候交付什么价值。具体来说,计划应重点回答以下问题:
-
我们通过现代化想要解决什么问题?
-
哪些小的务实变更将帮助我们更多地了解系统?
-
我们可以迭代哪些内容?
-
部署变更后,我们如何发现问题?
接下来,我们将看看如何从规划阶段转向面对那些将使实施变得困难的问题。
第四章:为什么这么难?
从表面上看,每个遗留系统现代化项目开始时都感觉很容易。毕竟,曾经有一个可用的系统存在。组织不知怎么地设法弄清楚足够的事情,把某个东西投入生产并让它运行了多年。现代化团队所需要做的只是利用更好的技术、后见之明和改进的工具重复这一过程。应该是很容易的。
但是,因为人们看不到他们即将揭示的隐藏技术挑战,他们也假设这项工作会很无聊。重新实现一个已解决的问题几乎没有什么荣耀可言。一个准备开始这样任务的组织渴望新的特性、新的功能和新的好处。现代化项目通常是组织希望尽快完成的任务,因此他们通常在没有做好时间和资源承诺的准备下就匆忙启动这些项目。
我告诉我的工程师,我们必须解决的最大问题不是技术问题,而是人际问题。现代化项目通常需要几个月,甚至几年的时间。让一个工程师团队从头到尾保持专注、激励和动力十足是困难的。让他们的高级领导层准备好一次次投资于本质上已经拥有的东西也是一项巨大的挑战。创造动力并保持动力是大多数现代化项目失败的地方。
到目前为止,最大的动力杀手是那些告诉我们项目一开始应该是容易的假设。它们,按顺序排列如下:
-
我们可以在旧系统的经验教训上建立。
-
我们了解旧系统的边界。
-
我们可以使用工具来加速进程。
让我们花点时间讨论一下,为什么这些显而易见的真理可能并不像看起来那么有用。
后见之明的诅咒
在扑克中,人们称之为结果主义。这是一种将结果的质量与决策的质量混淆的习惯。在心理学中,人们称之为自利偏见。当事情顺利时,我们高估了技能和能力的作用,低估了运气的作用。相反,当事情进展不顺时,一切都是坏运气或外部因素。
遗留系统现代化项目之所以困难的一个主要原因是,人们过高估计了现有系统所提供的后见之明。他们认为现有系统的成功是技术能力的问题,而且他们在最初构建过程中发现了所有潜在的问题并以最佳方式解决了这些问题。他们看到了结果,却没有注意到决策的质量或那些促成这些结果的运气因素。
当然,往往关于原始决策的文档几乎没有,根本没有资料让他们进行研究。尽管如此,忽视运气在任何项目成功中的作用意味着团队认为自己有空间在原始挑战上进行额外的创新。
软件可能存在严重的 bug,但仍然能取得巨大的成功。Lotus 1-2-3 就以把 1900 年误认为是闰年而闻名,但它的受欢迎程度如此之高,以至于至今 Excel 的版本仍然需要编程以保持对这一错误的兼容性。而且由于 Excel 的受欢迎程度最终远远超过了 Lotus 1-2-3,这个 bug 现在成为了 ECMA Office Open XML 规范的一部分。
成功与质量不一定是相关的。遗留系统是成功的系统,但这并不意味着在设计和实施这些系统时所做的每一个决策都是正确的。大多数人认为他们知道这一点,但他们却走入了错误的方向。他们对系统持悲观看法,但尽管如此,他们还是会在路线图上增加新的功能和特性。无论他们看起来多么批评这个系统,他们依然假设基础问题已经得到解决。
我们在现代化遗留系统时遇到困难,是因为我们未能给予遗留系统真正的挑战应有的关注和尊重:背景已经丧失。我们已经忘记了创造最终设计时所做的妥协的复杂网络,也对多年修改带来的复杂性视而不见。我们没有意识到至少有一些设计选择是错误的,系统之所以能够长期良好运作,完全是凭借好运。我们把问题简化了,最终在发现错误之前就承诺了新的挑战。
对遗留系统的轻视并不能保证我们不会陷入依赖已经失去的背景的陷阱。还记得我在第三章中描述的游戏吗?当我们审视系统中哪些部分不应该使用 COBOL 时,这是一个有用的技巧,即使 COBOL 不是因素。通过挑战我的团队,在我们遗留系统的要求下,使用当时遗留系统构建时可用的技术来设计一个系统,我们被迫恢复一些背景。遗留系统中许多“愚蠢”的技术选择看起来非常不同。一旦我们被迫直接审视背景,我们就会意识到这些系统中的一些其实是非常创新的。这让我们稍微有些洞察,哪些决策是技能和远见,哪些是运气。
一个成功的系统可能有一个设计模式,虽然无法在达到某种使用规模后继续生存,但它能够在未越过那个门槛之前就实现其运营目标。这是技能还是运气?如果设计者知道系统无法扩展,但同时也知道系统永远不会达到需要那样扩展的地步,那么我们可以假设这个设计是一个有意识的决定。例如,也许这个系统仅对某些人开放,仅供内部使用。扩展到数百万次请求是没有必要的,因为它最多也只会收到每秒几百个请求。
另一方面,如果系统的设计初衷是其使用会持续增长,并且设计者选择了一个只会在某个临界点之前有效的模式,那么他们的成功就是运气问题。他们只是没有到达那个临界点。Twitter 曾是一个设计良好的系统,直到它变得如此受欢迎,以至于开始崩溃,向用户展示了著名的“故障鲸鱼”卡通而不是他们的内容。一夜之间,构建这个社交媒体平台及其所使用技术的工程师,从被认为是熟练操作员和优越代码的创造者,变成了一群使用过度炒作、简化编程语言的普通业余者。他们既不是天才,也不是傻瓜。
扩展始终涉及一些运气。你可以为一定数量的交易或用户做规划,但你无法真正控制这些因素,尤其是当你构建的是涉及公共互联网的任何内容时。软件系统通常会集成多种技术协同工作来完成某个任务。我不知道有人能预测在每一种可能的规模条件下多种技术的表现,尤其是在它们被结合在一起时。工程团队会尽最大努力减轻潜在问题,但他们永远无法预见所有可能的事件组合。因此,服务在其初始规模上是否能正常运行,并随着其增长持续正常运行,始终是技能与运气的结合。
容易也不可能
1988 年,计算机科学家汉斯·莫拉维克观察到,教计算机做一些非常基础的事情非常困难,但编程计算机去做看似复杂的事情却容易得多。那些已经发展了数千年,用来解决像走路、回答问题和识别物体等问题的技能是直觉性的、潜意识的,且极其难以教会计算机。而与此同时,那些数千年来并未成为人类经验的一部分的技能——比如下棋或地理定位——相对来说要简单得多。他将这个悖论与进化联系起来的理论,得到了其他同时期 AI 研究人员的关注,以至于这个悖论最终以他的名字命名。
用莫拉维克自己的话来说,“让计算机在智力测试或玩跳棋时表现出成人水平的能力是比较容易的,而在感知和运动方面,让它们具备一岁婴儿的技能却是困难甚至不可能的。”^(1)
那些希望升级大型复杂系统的人,最好牢记莫拉维克悖论。系统的进化速度远远超过自然,但就像自然界一样,随着系统的进化,它的基础逻辑变得越来越难以理解。当我们习惯于某个东西总是以某种方式运作时,我们往往会忘记它。一旦我们不再思考它,就会忽视它在现代化计划中的重要性。
我们假设成功的系统解决了其核心问题,但我们也假设那些不需要任何思考或努力就能正常工作的东西其实是简单的,而实际上,它们可能承载了多年迭代的复杂性,而这些我们早已忘记。
当系统有多层抽象时,这一点尤其成立,尤其是当这些抽象超出了应用程序的边界时——当它们利用操作系统 API 甚至硬件接口时。你上一次考虑过你的常用软件是否与电脑上的芯片架构兼容是什么时候?你上一次为了让一个新配件与操作系统兼容而需要寻找特定驱动程序是什么时候?如果你是在 1990 年代之后出生的,或许你从未想过这些问题。硬件和软件接口在过去二十年中并没有变得更简单,我们只是抽象掉了许多让 x86 与 x64 的问题,或者下载驱动程序成了与计算机交互时正常部分的那些令人烦恼的差异。
对于非常古老的遗留系统,可能没有抽象层,或者更糟的是,这些抽象层本身可能已经过时。我喜欢称这个问题为过度生长,值得详细描述。
过度生长:应用程序及其依赖项
过度生长是软件与构成其运行平台的抽象层之间的一种特定类型的耦合。依赖管理的危险众所周知,但对于遗留系统,依赖管理不仅仅是包管理器可能安装的内容。系统越老,它运行的平台就越可能本身就是一个依赖项。大多数现代化项目并没有以这种方式思考平台,因此会把这个问题作为一个令人不快的惊喜,等待后续发现。
我们在跨平台兼容性方面取得了巨大的进步,但我们尚未达到应用程序 100% 平台无关的状态,也不太可能完全实现这一目标。
因此,我们在现代化一个系统时,不能忽视底层平台。这个平台有哪些特性是独特的,哪些是其他选项也可以找到的?这个平台有多老,是否已经被一种完全不同的方式所取代?
使得重大迁移如此棘手的是,随着软件的老化,其所定义运行的平台的元素逐渐过时,且在其他平台上对这些元素的支持越来越少。这意味着在我们最老的系统中,通常存在需要从系统中剔除或必须在现代平台上重新构建的逻辑。现有平台变成了围绕被迁移内容的辅助软件。例如,如果你在更换数据库,你不仅仅是在迁移数据。你可能需要用不同的语言或 SQL 的不同实现来重写查询。你可能需要重新思考钩子或存储过程。一个软件语言往往有许多次要语言,专门执行特定功能。有像 bash 或 JCL 这样的命令处理器来触发作业,还有模板语言来构建界面,查询语言来访问数据,等等。商业逻辑在这些层次之间是如何分离的?逻辑是否停留在合理的位置,还是被注入到方便的位置?
大多数 web 开发项目,例如,运行在 Linux 机器上。因此,web 应用程序通常会包含 shell 脚本作为其代码库的一部分——特别是在设置/安装过程中。想象一下,如果 Linux 被其他操作系统取代,迁移这些应用程序在 20 年后的未来会是什么样子。我们可能需要重写所有的 shell 脚本,还需要迁移实际的应用程序。
聪明的工程师会指出,使用容器化和配置管理工具后,这些脚本应该已经是过去式了,但这正是过度增长成为遗留代码问题的原因。曾经,很多任务都是通过 shell 脚本来完成的;这一做法后来被另一种方法取代。如果我们想迁移一个旧应用程序,我们可能会发现这种旧的做法并不被我们想使用的技术所支持。我们必须先迁移辅助软件。
对于现代应用程序而言,过度增长通常不是一个重大障碍。来自同一计算机时代的编程语言通常共享生态系统,因此,替换一种语言为另一种语言时,只需要对周围的辅助软件进行最小的更改就更容易了。记住,过度增长只是另一种形式的耦合。如果这种耦合的价值是存在的,它不一定是坏事。
然而,在旧有的应用程序中,人们似乎很难看到这种类型的耦合。我们往往会忽略辅助软件,就像我们忘记了莫拉维克(Moravec)曾为编程计算机完成简单任务所付出的复杂努力一样。一段程序未升级的时间越长,现代平台和工具支持它的可能性就越小。当辅助软件逐渐失去支持时,现代化实际代码的挑战就变得更加复杂。
在集成点处查找过度生长,即通信层发生变化的地方。有几种不同的过渡点,你可能会在这些地方发现过度生长。
垂直迁移:从一个抽象层移动到另一个抽象层
现代软件与机器中电路中流动的物理电压之间存在许多层次。在最基础的层次上,我们可以定义三个层次:软件、硬件和它们之间的操作系统。当在这些层次之间上下迁移时,过度生长通常表现为专有标准,特别是在旧技术中,其中硬件制造商通常也会提供软件。要注意你的应用程序代码是否依赖于特定操作系统的 API,或者更糟的是,是否依赖于运行该应用程序的物理机器的芯片架构。这是旧大型机中常见的问题。软件是用一种特定于构建大型机的公司以及通常是机器型号的汇编语言变体编写的。
水平迁移:从一个应用程序移动到另一个应用程序
就像有遗留代码一样,也有遗留协议。当两个应用程序在彼此之间传递数据时,如果它们运行在由公司开发的机器上,或者在使用专有协议的网络设备上进行通信,你可能会在连接周围看到一些过度生长。这在 web 开发中不太成问题,因为互联网的去中心化特性推动了标准协议的普及,如 TCP/IP、FTP 和 SMTP——这些协议都有强大的工具生态系统,并在多个平台上得到了广泛支持。在软件开发的其他领域,专有协议的影响范围更大。这些协议的难易程度取决于相关技术的普及程度。来自大型供应商的专有协议可能会被其他选项支持。例如,Microsoft Exchange Server 协议是专有的,但得到了很好的支持,而依赖于 AppleTalk 的应用程序可能会发现迁移困难。
从客户端到服务器的迁移
这种变化可以表现为针对特定工具和集成的特定软件开发工具包(SDK),针对特定数据库连接的驱动程序,或者前端到后端的迁移。虽然这一点可能让一些工程师感到震惊,但有些内部 Web 应用仍然是为在特定的 Web 浏览器上运行而构建的,并且依赖于其他浏览器中不可用的功能或特性。Internet Explorer(IE)是最常见的罪魁祸首。每当你看到 IE 被作为内部应用的首选默认浏览器时,务必再次确认这些应用的前端没有使用 IE 特有的 JavaScript 功能。我们也常常在 Adobe Acrobat 中看到这种情况。早期的数字表单通常是为了利用 Acrobat 特定的 PDF 功能而构建的,可能在不同版本的 Acrobat 之间转换时会遇到困难。一个著名的故事来自我在美国数字服务局的工作经历,当时退伍军人事务部的一个网站只有在你降级 Acrobat 版本后才能正常工作。^(2)
向下转移依赖树
随着编程语言的成熟,它们偶尔会对语法或内部逻辑进行破坏性更改。并非所有依赖都会以相同的速度升级以处理这些变化,这就导致了应用无法升级,直到相关依赖被升级为止。在非常古老的应用中,这些依赖可能已经不再处于积极开发状态。例如,可能维护者从未推出过与最新版本的 Java 或 Node.js 兼容的版本,而要获得该支持,应用必须切换到完全不同的选项。
削减过度生长
削减过度生长从技术上来说并不难;它只是令人沮丧和士气低落。过度生长会减缓进度,如果没有准确评估,就会产生意外的惊喜,影响团队的信心。为了最小化其影响,首先需要绘制应用的上下文图。它运行在什么环境中?创建新实例的过程是什么?将其依赖关系映射到两级深度。^(3) 尝试追踪应用中数据的流动过程,直到完成一个请求。这应该能让你更清楚地看到可能存在问题的地方。如果你能把这些问题列入路线图,它们对士气的影响就会小得多。
你可能会觉得现代软件开发正在改善这一情况。跨平台兼容性比以前好多了,这是事实,但商业云的 PaaS(平台即服务)市场的增长也在增加为特定平台功能编程的选项。例如,越多地使用亚马逊的托管服务来构建应用,应用就越会符合亚马逊特有的特征,若该组织后来想要迁移,这种“过度生长”就会带来更多的麻烦。
自动化与转换
人们对遗留系统的最后一个假设是,既然计算机能够读取他们试图现代化的代码,那么一定有某种方式来自动化这个过程。他们引入像转译器和静态分析这样的工具,目的是让现代化过程变得更快、更高效。
这些工具很有用,但前提是对它们的预期是现实的。如果你把它们当作指导工具来帮助完善过程,你的现代化团队可以采取战略性方法,避免关键错误,并可能减少一些成本。然而,如果你把它们当作捷径,省略了真正对现代化的投资,它们很可能会让你失望。那些认为工具是解决方案的组织,通常会经历更长、更痛苦和更昂贵的现代化过程。
那么,这些工具究竟做了什么,如何正确使用它们呢?
转译代码
转译是将用一种编程语言编写的代码自动翻译成另一种编程语言的过程。当被读取的语言与输出将写入的语言之间的差异不大时,使用转译器是有意义的。例如,Python 3 有足够多的破坏性更改,实际上需要工程师迁移他们的代码库,而不仅仅是进行升级。与此同时,Python 3 并没有改变 Python 本身的任何基本哲学,只是一些实现细节。转译工作得如此出色,以至于 Python 3 内置了 Python 2 到 Python 3 和 Python 3 到 Python 2 的转换工具。
转译器的另一个典型用例是,当转译器读取的语言专门设计用于强制执行转译器所编写语言的良好实践时。JavaScript 有许多不同的变种,例如 CoffeeScript 和 TypeScript。
当输入和输出语言之间的差异较大时,转译变得更加复杂,节省时间的预期需要得到适当的管理,以确保成功的结果。这种用例的经典例子是从 COBOL 到 Java 的转译。COBOL 是默认的过程式、命令式和定点数语言,而 Java 则是面向对象的,且默认使用浮点数。将 COBOL 转译为 Java 可能会生成可用的代码,但除非工程师对代码进行检查和微调,否则代码将无法维护。通常这意味着需要重写部分代码。
如果你打算使用转译器进行这种升级,那么至关重要的一点是应用程序必须拥有设计良好且全面的测试套件,最好是自动化测试套件。通过将一种语言自动转换为另一种完全不同的语言所产生的错误可能是微妙的,且难以追踪。例如,当你尝试将一个八位数的数字放入一个定义为七位数的变量时,COBOL 会截断最后一位并继续执行。另一方面,Java 会抛出异常。转译器不会添加代码来处理这些异常。
人们通常投资转译器来帮助升级遗留代码,因为他们认为让计算机程序做第一次转换会节省工程时间,或者他们认为转译器会完全替代原语言专家的帮助。但当两种语言有显著差异时,这种转译器的输出通常不会遵循它所写语言的结构和约定。转译器无法重新思考你如何组织代码。转译后的 COBOL 就是将其当作 COBOL 编写的 Java,因此大多数 Java 程序员无法理解。
这种转译技术的成功案例通常来自那些将转译解决方案作为咨询服务入口的公司。也就是说,首先你购买转译器的许可证,然后你再雇佣人才将转译器的输出重写成可工作的内容。这是一种不错的策略,只要你知道你正在做什么。
静态分析
尽管在理论领域之外尚未得到广泛应用,但学术界在利用各种静态分析方法探索并最终改进遗留系统方面做了一些有趣的工作。所谓的软件翻新结合了编译器设计和逆向工程的技术,旨在引导重构过程。软件翻新是半自动的:分析是自动化的,但软件工程师需要实际进行代码重构工作。
一些常见的静态分析类型用于软件翻新,包括以下几种:
-
依赖图 在这种软件翻新方式中,依赖图会被映射,并使用聚类算法来确定是否存在重叠、冗余、未使用的库或循环依赖^(4)。
-
语法 这些是特定语言的工具,通过解析抽象语法树进行分析。通常它们会查找重复的代码或被认为是反模式的特定做法(如 goto 语句)。
-
控制流/数据流图 这些图表是追踪软件执行方式的工具。控制流图映射了代码执行的顺序,而数据流图则映射了变量的赋值和引用。你可以使用这样的分析来发现丢失的业务需求或追踪死代码。
软件翻新方法学尚未完全脱离理论研究,但静态分析工具无论是作为独立产品,还是作为大型集成开发环境或持续集成与部署解决方案的功能,都已可用。这是令人遗憾的,因为方法学是驱动大部分影响的关键。工具本身没有方法学重要,挖掘、理解、记录,最终重写和替换遗留系统的阶段才是关键。工具会来来去去。
不让事情变得更难的指南
期望管理非常重要。通常,组织会犯本章所描述的错误,因为他们认为自己正在提高流程效率。他们错误估计了现代化项目所需的时间,错误判断了可以节省多少时间以及如何节省。
当我们用以下准则替代本章开头描述的错误假设时,现代化项目会有更好的成果:
-
保持简单。不要因为旧系统成功就添加新的问题来解决。成功并不意味着旧系统完全解决了它的问题。一些技术决策是错误的,但从未引发任何问题。
-
花些时间尝试恢复上下文。将平台视为一个依赖项,并寻找那些不容易迁移到现代平台的耦合。
-
工具和自动化应该是对人类努力的补充,而不是替代。
个人贡献者通常发现,遵循这一建议的障碍不在于说服自己,而在于说服他人。尤其是当组织庞大时,压力很大,要按照大家都在做的方式来推进项目,即使这样做会牺牲成功,只为了看起来是正确的。在后面的章节中,我们将探讨如何在组织中导航以及推进目标的策略。
第五章:构建与保护动力
本书主要关注大型项目。当我讨论升级时,我指的不是运行包管理器来安装最新版本的依赖包。当我提到弃用时,我不是在讨论为你的 API 进行版本控制。本书中的大部分建议无论项目规模如何都适用,但它主要是针对大型项目的。
第三章讨论了围绕遗留系统所带来的工程挑战发展策略。在那一章中,我描述了不同类型方法的形态和性质,并讲解了如何从整体角度看待这种挑战。本章则从组织角度描述了一种类似的方式:如何制定一个能够推动动力、保持团队专注和乐观的计划,即使工作变得艰难。
大型遗留系统现代化项目的有趣之处在于,技术人员似乎突然间开始倾向于那些他们知道在其他情况下无效的策略。很少有现代软件工程师会放弃敏捷开发,去花费数月时间规划架构的具体样貌,并试图一次性构建一个完整的产品。然而,当被要求进行旧系统现代化时,突然间每个人都开始将其分解成相互依赖的连续阶段。
面对遗留问题,敏捷方法并没有得到广泛宣传。市面上有很多书籍描述如何构建软件。也有一些书籍介绍如何维护软件,甚至更少有书籍讲解如何处理当软件被遗弃或最初构建时出现问题时,重建软件所面临的挑战。
事实上,重建一个系统时有效的方法与当初建立它时所使用的方法并没有太大不同。你需要保持小范围,并且不断迭代你的成功。这个做法看起来可能不必要,因为旧系统及其历史已经为你定义了所有的需求。假设仅仅因为一个现有系统可以运行,你就完全理解了这些需求,这是一个严重的错误。建立新系统的一个优势是团队对未知因素有更清晰的认识。现有系统可能会成为干扰。软件团队把它的全功能实现当作最小可行产品(MVP),不管那个现有系统有多大或多复杂。简而言之,信息量过大,难以管理。人们会感到不知所措,变得沮丧、丧失士气。项目停滞不前,这也加固了“现代化工作不可能完成”的观念。
动力构建者:可衡量问题的乐趣
当现有系统在背景中存在时,有几种不同的方法可以限制范围。最直接的方法是从现有系统的功能阵列中定义一个 MVP(最小可行产品)。将其简化成一个更轻量的版本,作为第一版,然后逐步添加回功能。尽管这种方法合理,但它需要纪律性和强有力的领导力。现有系统的所有用户自然会认为他们使用的功能是最关键的,并会游说尽早将其安排到第一版。这一过程很快就会变得政治化。
相反,我更倾向于通过定义一个可以衡量的问题来限制范围。构建现代化基础设施不是一个目标。不同的人自然会就应该执行哪些标准和最佳实践以及应该执行多么严格产生分歧。很少有现实中的系统完全符合理想标准;总是至少有一两处地方使用了非标准的方法来使某个特定功能或集成得以工作。每个人都知道这些妥协的存在,并且它们可能以某种形式或另一种形式在新系统中继续存在,但组织不太可能就何时以及在哪些地方引入这些妥协达成共识。
但是,如果所有的工作都围绕一个可以衡量和监控的关键问题来构建,那么这些对话会变得更容易。你从寻找尽可能多的机会开始,以改善这个问题,并按预计影响的大小进行优先排序。当在方法或技术上出现分歧时,决策的标准变成了“哪个能更进一步推动进展?”
继承的现代化项目进行得更顺利时,当贡献者感到可以自主工作,并且能够根据挑战和意外的出现进行调整,因为他们明白优先事项是什么。需要向高级团队汇报的决策越多——无论是副总裁、企业架构师,还是首席执行官——延迟和瓶颈就越多。失去的动力越大,人们就越不相信成功是可能的。当人们不再相信成功是可能的,他们就会停止全力以赴地工作。可衡量的问题赋予团队成员做决策的权力。大家已经达成一致,指标 X 需要改进;任何为改善指标 X 而采取的行动无需向上层报告。
可衡量的问题创造了明确表述的目标。有了目标,你就可以定义期望项目带来什么样的价值,以及这些价值将最有利于谁。现代化会让客户的体验更快吗?它能提高扩展性,帮助你签下更大的客户吗?它会拯救人们的生命吗?或者,它只是意味着某人可以进行一次会议演讲或撰写一篇关于从技术 A 切换到技术 B 的文章?
可衡量问题的结构
想要以整体方式处理架构是很自然的。我们的思维喜欢秩序和模式,一切都井然有序、深思熟虑的整洁感。但系统就像房子;它们永远不会保持完全干净太久。使用某样东西本身就迫使它发生变化。你会有更少的内存和存储,硬件会衰退,而且你增加了新功能,这意味着更多的代码行。
优秀的现代化工作需要抑制一开始就创建优雅而全面架构的冲动。你可以拥有一个整洁有序的系统,但你不会通过一开始就按照这种方式设计它来实现这一点。相反,你需要通过迭代来构建它。
可衡量的问题将引导你的团队完成现代化工作。当遗留系统刚推出时,它的规模和运维团队都很小。随着系统的增长,内部政治也随之增长。在某些情况下,整个业务单元的诞生或重组是为了跟随技术模式的发展。让所有这些人达成一致并朝同一个方向前进是非常困难的。可衡量问题的优势在于它是客观且不可辩驳的,因此,它有助于团队在现有系统遗留下来的内部政治中找到导航方向。人们可能会对可衡量的问题是否是正确的解决问题方向产生分歧,但这将把调解分歧的责任从工程团队转移到最初批准将现代化活动聚焦于该可衡量问题的高级管理者身上。
可衡量问题的最后一个好处是,正面结果与功能发布无关。当团队尝试从现有系统创建最小可行产品(MVP)时,组织会迫使他们尽快实现与现有系统的功能对等。成功或失败变得与发布挂钩,这促使了减少投入和技术债务的发生。
很可能,组织的业务方面并不理解现有系统的问题所在。推出他们已经拥有的功能不是他们会庆祝的事情。为了推动现代化工作取得进展,必须清晰地传达现代化如何改善现状。定义一个可衡量的问题向业务方面解释现有系统如何能够更好。一旦指标和标准被定义,任何给定的行动要么朝着积极方向发展,要么不会。错误更容易被识别、定义和纠正。组织中的每个人都可以通过查看指标来了解情况。
那么,如何识别一个好的可衡量问题呢?
最简单的候选问题是那些能反映组织的业务或使命目标的问题。如果你在考虑重构系统,却无法将这项工作与某种业务目标联系起来,那你可能根本不该做这件事。
当我在政府工作时,我看到的最鼓舞人心的项目之一是努力现代化移民系统,以便达到奥巴马政府设定的难民重新安置的挑战目标。即便只是涉及难民的子系统,整个系统也庞大复杂。工程师们被它的范围以及它偶尔遇到的问题所压倒。
但这个项目的挑战不是让整个系统变得更好;而是让整个系统处理特定类型的应用程序更加高效。以这种方式定义目标为努力创造了更清晰的范围。团队首先分析了应用处理中的瓶颈,然后开始精准地针对这些领域,寻求仅做逐步改进。关于优先级的讨论集中在哪些变化可能会增加处理的应用程序数量——这些数字是团队中的任何人都可以查看和参考的。当他们朝着这个具体目标努力时,团队放弃了许多急需的基础设施变更机会,因为这样做无法在需要的地方产生结果。
初看之下,这种方法可能显得不明智甚至不负责任,但大规模努力的头号杀手并不是技术失败,而是失去动力。要在长期的架构重建挑战中取得成功,团队需要建立一个反馈循环,不断基于并促进他们的成功记录。当显而易见的是,难民团队不仅会达到这个挑战目标——一个许多人认为不可能的数字——而且他们实际上会超额完成,超过几千人时,其他更有条件做出那些急需的基础设施改变的团队也开始以新的动力投入工作。不要忽视现代化项目通常是长期的,且通常涉及协调多个团队。战略性地狭隘一些以展示价值并建立动力并不是坏主意。
好的可衡量问题必须聚焦于你们工程师关心的问题。拯救免于 ISIS 的难民数量是一个容易激起人们关注的目标。很可能,你无法说你的数据库迁移能做到这一点,但工程师们对其他事情充满热情。与他们交谈,找出那些事情是什么。
动力杀手:团队无法达成一致
当我从一个个人贡献者转变为领导工程团队时,我在技术讨论中的角色发生了变化。当我专注于促进富有成效的对话,而不是争夺决策者角色时,我看到更好的结果。你有没有曾经参加过一个感觉像是在原地打转的会议?那种大家似乎在竞争,看谁能预测出最隐晦的潜在失败的会议?那种过去的决策被重新审议,最后每个人都不确定下一步该做什么的会议?促进技术对话比当决策者更为重要,因为无效且令人沮丧的会议会让团队士气低落。
由于大型系统通常比较复杂,失控的会议可能会使得关于支撑它们的技术的决策偏离轨道。可衡量的问题帮助人们优先考虑要改进的内容以及改进的顺序,但当涉及到具体的实施细节时,并不总是能够预测哪些选项会产生最大的影响。理智的人会有不同意见,但无意义的争论需要在造成过多损害之前化解。
步骤 1:定义范围
处理不良决策会议的最佳方式是从一开始就防止它们发生,方法是定义并执行会议范围。我通常会在会议开始时列出预期的结果、我会满意的结果,以及这个决策的范围外内容。我甚至可能会把这些信息写在白板上,或放入 PowerPoint 幻灯片中以供参考。我们想在这次会议中达成什么目标?如果我们卡住了,哪些结果是可以接受的?有时团队无法达成一致,因为存在真正的阻碍因素——比如一个需要更多研究的灰色区域。如果发生这种情况,我们能做出什么最小的决定,同时还能让会议看起来是有成效的?
一旦会议确定了范围,我会定义出那些我们应该一致认为不属于该范围的领域。通常,超出范围的问题是既不构成阻碍也不依赖于其他问题的决策。这些困难的问题似乎通常与会议范围内的议题有关,因此在有疑问时,团队需要能够清晰地表述出我们范围内的决策是如何受到提出的问题影响的。例如,我曾经有一个工程团队,负责创建一个无缝的平台,工程师可以在上面运行命令,平台会自动完成构建、配置和部署等繁重工作。与此同时,组织也在考虑逐步淘汰一种编程语言,转而采用另一种编程语言。为了实现第一个目标,我们需要就工具架构做出一些决策。我们是构建一组独立的工具,还是构建一个可以扩展功能的单一工具?无论我们选择什么设计模式,都可以在两种语言中同样完成,因此关于编程语言的任何争论都不会使我们更接近决定会议讨论的主题。关于编程语言的讨论是超出范围的。尽管这个问题最终会影响我们选择的设计模式的实现,但在选择设计模式时,它既不是阻碍因素,也不是依赖因素。
在与我的工程师们一起工作时,我设定了一个期望:为了进行富有成效、自由流畅的辩论,我们需要能够迅速且轻松地将评论和问题分为范围内和范围外的内容。我把这种技巧称为“真实但不相关”,因为我通常可以将会议中的信息分为三个类别:真实的、虚假的和真实但不相关的。不相关只是说超出范围的一种更简洁的表达方式。
将会议中的评论看作真实、虚假或真实但不相关的目的,并不是为了阻止人们提出不相关的细节。当我们仅仅把贡献看作真实或虚假时,我们给个人施加了压力,迫使他们为了保全面子而为他们不相关的事实的有效性辩护。通过鼓励大家将评论视为在范围内或范围外,我们实际上是在说,发言的工程师提出了一个有效的观点,应该在不同的讨论中考虑。
与此同时,相关性往往是任何一个人都很难判断的。你不希望工程师因为担心提出超出范围的内容而自我审查。他们可能会错误地认为某些问题超出了范围,因为他们掌握的信息不完全。如果他们因为将真正但不相关的问题与失败挂钩而没有提出问题,他们可能会忽略实际存在的问题。一次伟大的会议并不是没有人提到任何超出范围的内容,而是能够迅速由团队识别并处理这些超出范围的评论,而不会让它们偏离话题。
第 2 步:检查优化策略冲突
即使在范围定义清晰的情况下,工程师们仍然可能会发生冲突。当两个有能力的工程师无法就某个决策达成一致时,一个快速的技巧是问问自己,每个工程师在他们建议的方法中优化的是什么。记住,技术有很多权衡,优化一个特性会削弱另一个重要特性。例如,安全性与可用性、耦合性与复杂性、容错性与一致性等等。如果两位工程师实在无法达成共识,通常是因为他们对这两个极端之间的理想优化点有不同的看法。
在模糊不清且基于价值的情境中寻找绝对真理是痛苦的。有时,最有帮助的是明确指出争议的根源实际上是关于优化什么,而不是单纯的技术正确性。每个优化的影响是什么?过度优化某一方向的负面影响能否得到缓解?
第 3 步:进行时间限定实验
如果争议在范围内,并且不是由于优化策略冲突导致的,那么解决的最佳方式是进行时间限定的实验。找到一种方法,在小范围的样本中尝试每种方法,并设定一个明确的评估日期和具体的成功标准。成为实验的高手对于几乎任何组织来说都是非常宝贵的。这是迭代的基础——你建立某个东西,收集它的性能数据,修改它以提高性能,然后重新开始这个周期。这就是有效技术的构建方式,所以工程团队应该习惯使用它来做出艰难的决策。
动力杀手:失败的历史
你现在正在进行的现代化工作很可能不是第一次尝试。那些能够成功维持技术的公司通常不需要进行大规模的现代化项目。它们通过逐步变革和定期维护来保持技术的更新。如果你所在的团队只是负责清理技术债务并迁移到更合适的技术平台,那么这意味着现有组织未能适应变化。
你的具体情况可能有着比单纯忽视常规维护更深层次的失败历史。这真的是第一次现代化项目吗?如果不是,每一次以前的尝试都可能在组织中留下了创伤,你需要考虑这些因素。一个项目经历的失败越多,建立成功所需的动力就越困难。
现代化工作中的首批交付成果必须考虑到这种失败的历史。人们对遗留系统的现代化项目感到悲观和缺乏灵感,并不是因为他们不关心或者没有意识到现代化的重要性。通常,他们之所以有这种感觉,是因为在经历了多次失败后,他们确信成功是不可能的。
与此同时,我至今没有遇到一个工程团队,他们不愿意相信自己能够达到更好的状态。当你证明成功是可能的时,改变人们对失败不可避免的看法是相当容易的。
受启发并充满动力的工程团队能更顺利、更高效地推进现代化进程,因此,请围绕提前创造价值来设计你的现代化战略。哪些变化能够产生最直接的积极影响?
我曾为一个面临拆解其单体架构的重大挑战的组织工作。该组织希望构建一个标准化平台,产品工程团队可以利用它轻松地将服务部署到生产环境中——这是一个合理的目标——但产品本身却是三个单体架构堆叠在一个虚拟机上。如果你愿意的话,这可以被看作是“单体中的单体”。在当时该架构的构建符合业务需求,但在随后的几年中,组织经历了爆炸式的增长。等我加入时,那个架构已经不再适用了。
这个组织面临两个问题。首先,平台建设和单体拆解相互制约。产品团队不想在能够部署到平台之前将单体拆解为服务。可以理解的是,他们不想把某个东西放到发布管道中,结果平台到来时还得再迁移。另一方面,平台组在没有产品团队设定的需求之前无法构建平台。他们必须能够根据真实服务的实际需求来构建平台,而这些服务因为尚未从单体架构中拆解出来,所以并不存在。
第二个问题是,组织之前其实已经尝试过这两个过程,但每次都失败了,且失败了不止一次。他们曾尝试构建平台,并且迁移了一些小型、无关紧要的服务,这些服务拆解后不需要太多的重设计。根据我的估算,他们至少尝试了三次,每次都因失去动力而未能完成。
组织曾多次尝试拆解单体架构。每一次都被任务的复杂性所压倒。拆解单体架构很少,仅仅是把一些代码复制粘贴到不同的代码库中。当软件被设计为紧密耦合时,工程师通常会利用这种紧密耦合的优势,基于这种便捷的耦合关系进行构建。在这种情况下,这意味着他们的测试套件中大多数是端到端测试,而非单元测试。这也意味着多个组件访问同一个数据存储,并共同承担相同的信息责任。当他们将紧耦合的单体架构转变为解耦服务时,测试将失败,并且需要制定一种保持服务之间数据一致性的计划。
面对他们的第四次尝试,乐观情绪相当低落。每个人都希望这个项目能够成功,但没有人愿意成为第一个投入大量工作却在努力再次失败时只剩下空手的团队。
平台团队的知名工程师被要求提出一个计划。他们花了数周时间收集数据并采访团队,最终提出了以下折衷方案:他们将把三个单体应用推到自己的发布渠道上,配备自己的虚拟机,从而确保平台能够支持产品所需的一切,而不需要产品团队立刻拆分任何东西。
这个计划的问题在于,它并没有真正改善任何事情。现在,不再是一个有负责人和有序计划决定何时将代码推送到每个区域和环境的发布周期,而是组织将有三个发布周期,而没有人负责它们。每次部署都必须在多个团队之间精心协调,以确保变化不会意外地早早或晚些时候影响某个单体应用的生产环境。
它也不会降低成本。商业云服务提供商按照每个虚拟机的使用时间收费。三组独立的虚拟机意味着提议的计划会轻松使组织的托管费用翻倍甚至三倍。
我甚至不确定它是否能够顺利启动。我的团队一直在努力重新设计一个看似完全独立的服务,打算将其放上平台,但我们发现各种奇怪的地方,组件在出乎意料的方式下集成在一起。
将三个单体应用放在不同发布渠道上的价值是什么?
当我提出这个问题时,工程师们以为我在问拆分单体应用的价值是什么。经过几次对话,我才让他们明白,我并不是在质疑他们的目标,而是在质疑他们的出发点。从将虚拟机数量三倍化开始,会使得产品团队的更新变得更加复杂,并且不必要地增加支出。如果组织在拆分单体应用的过程中第一次经历了让工作变得更加困难和昂贵的情况,为什么还要继续投资这一过程呢?
关于遗留系统现代化的难题,不是技术问题,而是人际问题。技术通常是相当直接的。要保持人们的专注和动力,完成这一任务可能需要几个月甚至几年的时间,这很难做到。为了做到这一点,你需要立即提供显著的价值,尽快让人们克服天生的怀疑态度并支持你。概念验证中最重要的词是证明。你需要向人们证明成功是可能的,并且值得去做。
一个组织失败得越多,它就越需要证明现代化能带来价值的证据。当有过失败的历史时,第一步必须提供足够的价值,以建立起成功所需的动力。这样做的明显问题是,它意味着有一个自然的上限。怀疑主义达到如此之高的程度时,任何单一的第一步都无法提供足够的价值来证明项目能够成功。
那么接下来呢?
动力建设者:激发紧迫感
如果你发现自己处于这种情况,首先需要做一些尽职调查。第一个问题是,这次迁移到底是否带来了任何实际的价值?还是我们仅仅因为眼前有一种崭新的技术就去迁移?毕竟,单体系统并不全是坏的。许多成功的公司仍然在运行单体系统。
如果你相信迁移确实能带来价值,接下来你需要问自己的是,领导层是否会承诺优先考虑这一点?有时你可能会运气好,变革是有硬性截止日期的,并且错过了会有实际后果。^(1)
但如果领导层没有优先考虑此事,并且如果你相信迁移确实有商业价值,但又被反复失败的怀疑主义所困扰,那么你需要的就是一场危机。毕竟,价值是相对的。当一切顺利,钱进账时,工程师们可以容忍各种过错。而当形势不佳时,几乎任何变化所带来的价值感知都会增加。应对危机会改变组织内部对风险的权衡。
在我政府工作的时期,我们经常会达到价值规模的上限。有些系统非常陈旧,现代化的努力几乎是代代相传。拥有一场危机成了我团队运作的一个重要组成部分——以至于我们可能会拖延几周或几个月才与某个机构沟通,就为了看看是否会冒出一个我们可以借机处理的危机。
偶尔,我会走得更远,寻找一场危机来引起关注。这通常不需要太多努力。任何超过五年的系统都会至少有几个重大问题。这并不意味着撒谎,也不意味着在不存在问题的地方制造问题。相反,这是讲故事的艺术——把一些未被报道的问题提出来,突出其潜在风险。这些问题确实是问题,我对它们潜在影响的分析总是诚实的,但其中一些问题完全可以在几个月或几年内未被发现,甚至没有引发任何事件。
我最喜欢从安全性开始,然后是系统稳定性。理解这些问题出错的影响和后果并不需要太多的技术知识。也有一些领域,即使是最优秀的技术团队也会时不时遇到困难,因此如果你在这两个方面寻找潜在的危机,通常不会空手而归。
保护进展:对重大决策的配额限制
既然你已经完成了评估情况并围绕它组织工作的所有任务,你就不希望让组织本身削弱这些努力。人们本意是好的,但任何形式的变革都有风险,接受风险是困难的。别担心,你可以为组织变革创造一个让人们同意而非拒绝的氛围。
首先,你需要学会以一种方式来谈论你正在做的事情,最大限度地减少需要做出的重大决策数量,尤其是那些包括过程变更或需要多个利益相关者签字并经过多轮审批才能改变的决策。
需要咨询许多利益相关者的决策显然难以管理且痛苦。人们自然会想避免这些决策。因此,你的提案看起来包含的重大决策越多,人们就越有可能想要放慢进度或推迟一个季度。
你可能认为通过为项目起个花哨的名字、预测预算并事先解决人员配置问题是在认真工作,确实是这样!但你也在让项目看起来像是一系列重大的决策,对于那些没有日常处理遗留系统痛苦的受众来说,这看起来太冒险。考虑针对不同受众以不同的方式谈论同一个项目。有些受众会欣赏详细的计划,而另一些受众则会更倾向于高层次的方案。
在你需要减少为了推进工作必须做出的重大决策时,请注意以下几点:
-
现有的项目、计划或技术——这些是最典型的“差一错”的例子。借助已经批准的解决方案可以避免你自己去寻求这些批准。
-
有利的法规 你可以通过让重大决策看起来已经做出,从而消除这一决策,但你也可以通过让组织看似没有选择的方式来消除重大决策。合规性,尤其是与安全相关的合规性,是一个值得关注的领域,因为这些规则通常伴随着必须在特定时间完成的截止日期,否则组织将失去认证、资金,甚至可能失去客户。
-
模糊的审批过程 "请求宽恕,而非许可" 是初创公司圈子里流行的说法,但说实话,如果你能让人相信你是在诚意行事,那么最好请求宽恕。如果你绕过了一个记录清晰且广为人知的审批过程,结果往往不如当流程模糊或不存在时那样有利。
保护进展:计算机会成本
增值不仅仅是技术结果的体现。往往,商业结果提供了更清晰的优先级路径。商业结果可能是利润,但如果你在一个以使命为驱动的组织工作,商业结果也可能是服务的人数或观察到的影响。在进行一个多年的现代化项目时,得到业务方的支持至关重要。你不能指望他们理解技术成果,因此你应该知道如何通过计算机会成本来阐明商业成果的价值。
对于那些不熟悉这一概念的人,机会成本是指由于选择了其他机会而未进行某项活动,从而导致的资金损失。通常,机会成本通过未实现的预期利润来表达,但在遗留系统的背景下,我们通常会将机会成本理解为节省的资金。
机会成本更适合作为思想实验,而不是实际计算。如果我们能够准确计算出每种可能的方式升级现有系统(或者与不做升级、仅增加新特性相比)将花费多少时间和金钱,那么维护遗留系统就会变得容易。但机会成本的价值在于促使人们交流他们的假设,并为为什么组织应该做我们想要做的事情提供论据。为了提供价值,机会成本的估算不必准确,只需要为给定决策的权衡提供有洞察力的背景。
计算机会成本不仅仅是为了做出更有利可图的决策。它为团队提供了数据,以向各方利益相关者证明现代化活动的合理性。只有当技术明显失败时,投资技术健康才对所有人有意义,而到那时,问题已经变得更加严重,解决起来也更为困难。高层管理通常对任何清理活动持怀疑态度——担心它会不必要地拖慢组织的速度。
我在 Auth0 的第一个大项目是处理我们的通知系统。Auth0 当时仅为测试和开发目的维护着一个共享的电子邮件服务器。然而,客户们偶尔会忽视在进入生产阶段时,尽管有许多免费的选项可用,还是未能迁移到专用的服务提供商。当客户达到限制时,我们会将他们的邮件放入重试队列,以便稍后发送。
我们曾错误地假设客户会逐步超过配额,这是由自然流量增长造成的。如果真是这样,随着时间的推移,重试邮件是有意义的。少量邮件延迟,随着延迟变得越来越普遍,就会促使客户转向专用服务商。事实上,客户更有可能因某些活动突然突破配额限制,这些活动会触发向所有用户发送邮件——一次可能是数百甚至数千封邮件。
这就导致了一个情况:重试队列会被填满,直到 20 个工作人员需要花费数小时的处理时间才能清理邮件。这影响了所有用户的服务性能,并触发了值班人员的提醒——这一切仅仅是因为一堆大多数时候根本没人真正想要送达的邮件。
我们决定改变速率限制的工作方式,改为当超出限制时,共享服务商不再重试邮件,而是直接丢弃它们。这是一项艰巨的迁移工作,我们不仅需要改变速率限制的算法,还需要更换最初执行速率限制的技术。我们现有的速率限制解决方案正在被另一个解决方案替代。我们需要改变架构,并为升级速度较慢的本地客户制定一种向后兼容的策略,而这些客户的升级速度比云客户要慢。
这一切都需要大量的工作,而我们投入其中的动力非常个人化:当重试队列填满时,会向我们团队的某个成员发送提醒,让他们去解决这个问题。这既令人烦恼又具有干扰性。更加令人沮丧的是,解决此问题的官方方案是将重试队列中的所有邮件都丢弃掉。让人不禁觉得,要求一个人凌晨 3 点起床去做一件本该由计算机自动完成的事,简直毫无意义。
我们直到变更进行到一半时才意识到,避免发送成百上千封无意义的邮件能为我们节省多少资金。我们从为我们运行共享邮件服务器的公司每月收到一定数量的邮件。当我们超过这个限制时,我们与该服务商的账户会自动购买 50,000 封额外邮件,费用为 20 美元,并会发送提醒通知我们已经完成了购买。当我们开始实施这个变更时,我们每天大约会收到 10 个此类提醒,意味着额外花费了 200 美元的邮件费用。单次事件的费用可能从 1,000 美元到 2,000 美元不等。
当这些变更生效时,我们通过简单地取消那些客户本不想发送的邮件,就为公司节省了数万美元。这整个项目可以说是一个巨大的成功,但节省的成本给了我们政治资本,可以用来证明我们为什么没有花时间增加新功能,并为以后类似的维护工作争取支持。
在计算机会成本时可能会有些棘手,因为潜在的机会似乎是无穷无尽的。记住,机会成本是思维实验和修辞工具。你不需要列出团队可能正在做的所有事情的成本,只需要列出那些能够支持你想要做的事情的活动。这意味着要突出高优先级活动可能比组织预期的要贵,并且用通俗易懂的语言描述按照你希望的方式做事能带来多少价值。
在寻找适当的机会进行比较时,可以考虑以下三个大类中的活动。
不添加新特性的成本
这个成本通常是通过估算新特性带来的利润或影响来计算的。在小型组织中,这个成本更大,因为开发团队可能没有足够大,无法拆分成不同的部门。推出一个新特性会占用更多比例的总员工,这意味着他们无法进行现代化工作或参与其他项目。
在大多数组织中,推迟对遗留系统进行维护以优先考虑新特性和产品的压力是常态。虽然总是觉得组织如果能够度过当前的挑战,事情会平静下来并且清理工作可以开始,但实际上从没有一个合适的时机。为了避免无休止的拖延,尽量将新特性与目标状态对齐。例如,如果从单体架构迁移到服务架构,你可能想通过新特性来确定第一个需要拆分的服务。
不解决其他问题的成本
遗留系统通常不仅仅有一个问题。现代化过程中的每一步都是在选择用相同的时间和精力来解决的不同问题。我已经描述了选择修复什么以及何时修复的各种方法。机会成本实际上是关于如何向上层管理推销策略。如果组织已经定义了服务水平目标(SLOs),或者有服务水平协议(SLAs),那么这将更容易。SLOs 和 SLAs 都将性能水平与消费者价值挂钩。SLAs 甚至可能会规定当性能低于某个特定水平时,客户可以获得的具体赔偿金额。
服务水平目标(SLOs)和服务水平协议(SLAs)帮助团队根据问题对用户造成的痛苦程度来优先修复问题。即使你确信不需要证明何时以及如何进行现代化,它们也是一个很好的工具。但是如果你确实需要为你的策略辩护,你应该能够研究历史数据并预测在什么条件下,某个系统或系统的某个部分可能会违反其 SLO。通常,这受到规模的强烈影响,因此这是一个利用业务方雄心壮志的好机会:看看业务期望的增长水平,并基于这一增长水平如何影响 SLO 来计算机会成本。
不弃用而选择其他解决方案的成本
这是一个特别难以计算的成本,因为弃用并不是一次性完成的。在迁移或现代化的过程中,组织很可能会同时维护旧解决方案和现代化解决方案,尤其是当新解决方案需要部署代码更改时。因此,除了购买或开发新解决方案的成本外,还需要考虑弃用旧解决方案的成本。这个成本会影响多少团队?在进行这些更改时,他们不能做哪些工作?旧解决方案和新解决方案的长期维护负担如何?根据新解决方案是托管的/软件即服务(SaaS)还是仅仅是一个新的定制工具,考虑的因素可能会有很大不同。
第六章:中途介入
到目前为止,本书假设你是在启动组织中的现代化工作。我们讨论的战略也假设你最初是在现场进行规划的。这个组织可能在你加入之前就尝试过现代化,但我假设当前的现代化努力是你开始的。然而,我职业生涯中参与的大多数现代化工作并非如此。组织往往低估了现代化所需的工作量和投入程度。这个假设的不幸后果是,直到陷入困境时,他们才寻求专业帮助。
在我的职业生涯中,我启动的现代化工作远不如我跳伞式介入的现代化工作多。我很希望能够参与规划和评估阶段,但技术领导者们很少认为这有必要。
本章描述了当你介入一个已经处于困境中的项目时该做什么。当你尝试改变遗留系统时,活动可能会变得混乱,本章充满了应急的“破玻璃”技术,帮助你理顺混乱的局面。
当一个项目需要几个月或几年的持续投入时,出问题的事情会层出不穷。在第三章中,我提到过,大多数现代化的故事都以失败开始。你在计划已经开始并且进展不顺利时介入,选择的余地有限。按下重置按钮重新开始,可能会弊大于利。战地医护人员的首要任务是止血,而不是开一堆 X 光片或制定饮食和运动计划。要在一个已经开始的项目中发挥作用,你的角色必须做出调整。首先,你需要止血,然后你才能进行分析和长期规划。
找到关键问题
当然,技术项目并不会字面上“流血”;因此,识别最紧迫的问题可能是一个挑战。在本章中,我们讨论了我遇到过的最常见的情况,但我想我们首先要从一些一般性的指导开始。
发现责任空白。正式委派的责任和实际责任或功能之间总会存在脱节。康威定律告诉我们,技术架构和组织结构是一般等效的,但没有任何系统是其组织的一对一映射。系统中有一些部分是共享责任的,有些部分根本没有人负责,还有一些部分的责任分配方式是非直观的。在寻找糟糕的技术、债务或安全问题时,最有成效的地方是寻找同一组织内两个组件之间官方责任的空白。
组织通常在以下领域存在责任空白:
-
所谓的 20%项目,或者是作为副业(通常由单个工程师完成)构建的工具和服务。
-
接口。这不是指视觉设计,而是指那些在组织足够大,能够组建团队来维护之前,构建的用于标准化体验或风格的公共组件。
-
新的专业化角色。数据工程师的角色更接近数据库管理员还是数据科学家?
-
产品工程与产品运行所在平台之间的差异。Dev-Ops/网站可靠性工程(SRE)并没有解决这个问题;它只是将问题移到了更多的抽象层面。如果你已经自动化了基础设施配置,那很好——但是,谁来维护这些自动化工具呢?
当存在责任空白时,组织就会出现盲点。债务积累,漏洞未修复,机构知识逐渐丧失。
研究会议的节奏、议题和邀请名单。会议往往是对问题进行不适当解决的尝试。因此,如果你想了解项目中哪些部分最为困难,就要注意团队开会的内容,会议的频率以及谁被拉进这些会议。特别是要注意那些邀请名单很长的会议。大规模会议往往不如小规模会议高效,但它们确实通过给每个人一种“所有方都被咨询过,所有意见都被探索过”的印象,成功地分散了责任。邀请名单不断扩展的会议,通常意味着该项目的某个领域出现了问题。
会议的其他红旗包括团队的规划会议超过一小时,或者团队的签到会议通知时间少于 48 小时。
关注有职业意识的领导者的言辞。说起来有点 harsh,但人们对一个正在困境中的项目基本上有两种反应。一类人会卷起袖子,专注于帮助,即使帮助意味着做一些不体面的工作,而这些工作通常不是他们职责的一部分;另一类人则会花时间撰写借口,解释为什么失败与自己无关。大规模、混乱且进行中的项目通常会有这两类人;要留意第二类人。那些他们逃避的难题,往往是最混乱的。
寻找复合性问题。介入到项目进行中的时候,意味着项目还没有正式失败,而人们犯的错误可能会被一再加剧。项目很少因为一个关键错误而注定失败。更可能的是,组织已经在几个月的时间里陷入了功能失调的结构中,直到最终的失败。
所有这些例子都是自然的人类反应反而使问题变得更糟的地方,而不是更好。职责不明确意味着团队常常觉得被要求替别人承担责任。他们变得自以为是,开始忽视那些不在自己工作范围内的任务,导致情况更糟。会议拖慢了工作进度,几乎总是导致更多的会议。注重个人职业发展的领导者声称失败超出了他们的控制范围,暗中将责任推给团队。他们让员工感到不安全,这也鼓励员工避免接触问题区域。
如果一个项目失败了,你需要赢得正在工作中的团队的信任和尊重,才能进行调整。最好的方法是找到一个累积性的问题并停止其循环。如果一个组织开会过多,就取消所有会议,并逐渐一个个地重新引入它们。如果职业导向的领导者正在破坏心理安全,就开始教育大家无责事后分析和公正文化。与人交谈并观察团队如何作为一个整体表现。当可能时,总是更好地为别人创造胜利,而不是自己解决问题。
本章其余部分描述了我所见过的各种进行中的失败,以及我们如何将项目从死亡螺旋中拉出来。
混乱:修复那些没有损坏的东西
我们已经看过了许多组织尝试修复那些没有损坏的东西的原因。
-
他们认为新技术比旧技术更先进。
-
他们追求人为的一致性。
-
他们将成功与质量混淆。
-
他们优化过度,超出了收益递减的临界点。
组织修复那些没有损坏的东西的首要目标就是单体架构。在软件工程中,单体架构指的是一个紧密耦合的应用程序,它将各种功能和特性配置到一个独立的计算资源上运行。单体架构是网络开发发明的问题。在互联网尚未发展到分布式计算可以实现的规模之前,设计程序在一台机器上运行几乎没有理由不这么做。最近,似乎没有哪个工程师能忍受单体架构的存在。单体架构已经成为了最脏的词。工程师们不停抱怨它们。没有人愿意承认自己建造了一个单体架构。每个成功的大型技术组织似乎都会至少有一次关于英勇的多年度努力来去除单体架构的会议演讲。
但是,如果单体架构如此糟糕,为什么这么多组织最终还是采用了它们呢?
单体架构的对立面是面向服务的架构。与其将应用程序设计为将所有功能托管在一台机器上,不如将功能分解为多个服务。理想情况下,每个服务有一个单一目标,通常每个服务都有自己的计算资源。应用程序是通过协调这些服务之间的交互来创建的。
从一开始就以面向服务的架构来构建产品通常是一个错误。因为你还没有弄清楚合适的产品/市场契合度,集成和数据契约会成为一个主要的痛点。数据契约是两方服务之间用代码隐式写下的协议,必须互相通信。我们称其为契约,因为双方需要以相同的格式发送和接收数据,通信才能正常工作。如果服务器决定更改其发送的数据,而客户端没有及时更新,那么服务之间的通信就会中断。
当一个团队在进行调整和迭代时,当客户与团队之间的反馈循环最短时,数据契约经常会被打破。功能会被添加、移除或调整。假设会被做出,并且要么得到验证,要么被抛弃。在组织找到产品市场契合之前,它们可以进行疯狂且不可预测的转型。例如,YouTube 最初是一个视频约会服务。Groupon 最初是一个用于组织社交活动的平台。Slack 最初是一个在线多人视频游戏。实际上,Slack 是其创始人第二次开始构建在线游戏时,才意识到真正的产品是完全不同的东西。他的早期创业公司 Flickr 也有着相似的起源故事。
通常情况下,你的设计抽象层级应该与未经过测试的假设数量成反比。设计中包含的抽象越多,改变 API 而不破坏数据契约就变得越困难。你打破契约的次数越多,团队就越需要停止新工作并重新做旧工作。当产品还未发布时,强迫团队一次又一次地重做工作并不会提高成功的几率。
这就是为什么在产品的早期阶段,单体架构(monoliths)如此出色。它们是紧密耦合的,但它们的复杂性和抽象层级较低。当工程师做出改变导致系统的另一个部分出现问题时,她能够立刻发现并且有能力访问代码来修复她所引发的问题。
再次强调,专注于复杂性和耦合之间的平衡。复杂系统有较大的表面面积。每个过程都需要更多的步骤,每个部分都需要独立的团队来正确处理其维护。复杂性的缺点可以通过运行更多的团队并促进团队之间的沟通和知识共享来缓解。如果一个组织能够做到这一点,它就能够获得使系统更复杂所带来的好处。构建良好的复杂系统通常允许更大的定制性,能够以更大的灵活性和更大规模运作。
紧密耦合的系统则通过战略性地“打破自己”来实现灵活性。每个程序员至少都部署过一次廉价的解决方案来绕过 API 或继承模式,通常会附上一个评论:“唉,稍后按正确方式做。” 紧密耦合的系统变得杂乱无章,因为每一个解决方法都会导致技术债务的积累。紧密耦合的缺点可以通过工程标准来缓解,这些标准规定了如何扩展、修改并最终与耦合保持和谐。它们还可以通过工程团队承诺偶尔进行重构来缓解。紧密耦合的好处在于,一个人可以掌握系统的足够知识,来预测在各种条件下的行为。架构更简单,因此也更便宜、更容易运行。
一个系统有生命周期。当它是新的时,通常由一个小团队来运行,与其说它从复杂中受益,不如说它从紧密耦合中获得更多。小团队在构建新事物时,常常会把一切都推翻重建。小团队更容易遵守工程标准,因为需要达成共识的人更少。即便是大组织中的小团队,它们也倾向于构建单体架构,因为单体架构的优势在于,当你不确定自己构建的东西是否会成功并且需要快速更改时,它比其他架构更具吸引力,哪怕你修改的方式并不完美。在小型组织中,我们发现人们通常一次性做多种工作,角色定义不太清晰。每个人都在同一个空间里使用相同的资源。简而言之,小型组织构建单体架构是因为小型组织本身就是单体架构。
大型组织从复杂系统中受益更多,因为它们有强大的运营单元来支持这些系统。它们有团队来运行和维护系统的各个组成部分——平台、监控等等。它们很少会把所有东西都丢掉重新开始,因为它们的规模如此庞大,想要重新做一遍意味着一次重大迁移。大型组织在将其单体架构转化为服务时表现良好,因为解决使复杂系统正常运行的沟通和知识共享问题是大型组织本来就需要解决的问题。
但是没有人一开始就创建一个大型组织,就像没有人一开始就生下一个青少年一样。它们会成长,随着成长,复杂性–耦合性谱上的理想点也会发生变化。大多数单体架构最终都需要重新思考和重新设计,但尝试确定什么时候进行这样的改变,就像试图预测你会在什么时候长大到不再适合穿最喜欢的毛衣一样。有些组织会等得太久,有些则做得太早。不要相信那些告诉你,丢弃单体架构就能解决所有问题的人。单体架构是可以扩展的,有时它们扩展的成本较高,但认为单体架构无法扩展是错误的。问题在于,如果你仍然使用单体架构,可能会放弃一些本可以对运营卓越产生巨大影响的好处。
修复那些并没有坏的东西意味着你承担了现代化的所有风险,却无法找到有说服力的增值内容,也无法建立起推动事情发展的动力。非技术相关的利益相关者看到时间和金钱的投入,却不明白其意义何在。这会让工程师感到沮丧,并破坏团队的信任。修复错误的事情会让你更难获得完成任务的资源,也会使得在未来需要进行现代化的努力时,更难说服组织投入资源。
确定是否需要修复某些问题
将单体架构视为本质上有缺陷的做法,会促使组织在单体架构没有问题时就开始修复它。我有个朋友曾经说过,她最大的荣耀就是听到她构建的系统必须重写才能进行扩展。这意味着她构建了一个人们喜爱并且找到有用的系统,以至于他们需要对其进行扩展。大多数技术人员在构建系统时并没有这种预期。通常的假设是,最好的构建方式是将系统构建得足够稳固,长时间内不需要做任何重大改动。为了尽量减少重写而进行优化似乎是一种合理的策略,但如果没有适当控制,它会导致最终使系统变得更脆弱的行为。
ThoughtWorks 的董事兼软件架构师尼尔·福特有一句话,我很喜欢反复对我的团队工程师们说:“元工作比工作本身更有趣。”如果任由软件工程师们自主决定,他们几乎总是会过度设计,去解决更大、更复杂、更长远的问题,而不是眼前直接存在的问题。例如,工程团队可能会暂停开发某个应用程序,而去为未来的应用程序编写一个脚手架工具。团队可能会选择自己编写对象关系映射(ORM),而不是直接写 SQL 查询。团队可能会构建一个设计系统,其中包含每一个可能需要的表单组件,并进行完美的样式设计,而不是直接构建前端。
出于避免日后重写代码的动机而做出的决策通常是糟糕的决策。一般来说,任何为了取悦或给假想的观众留下深刻印象,展示你方法的表面优雅而做出的决策,都是错误的。如果你进入一个团队,发现成员们在修复一些并没有坏掉的东西,你可以确定他们是在做这件事,因为他们害怕他们的产品给别人留下的印象。他们对自己的工作技术感到羞愧,而你必须想办法说服他们不要感到羞愧,这样他们才能专注于修复那些真正坏掉的东西。
设定一个预期,即所有系统最终都需要重写。最高水平的工程师编写的程序必须进行修订。没有人足够聪明,能够预见到每一个新的使用案例或功能、每一次硬件的进步,或者每一个可能需要重写代码的调整或变化。对一个大组织有效的东西,可能会扼杀一个小组织。优秀的技术人员应该专注于当前时刻带来最大利益和最高成功概率的事情,且要有信心知道他们无需证明什么。
这需要从工程团队获得一致的意见,明确什么是“坏掉”了。我之前提到过 SLOs/SLAs,我现在再提一次:定义一个系统需要为用户提供的价值水平。如果一段丑陋的代码符合其 SLO,它可能并没有坏掉,只是看起来很丑。技术不需要漂亮,也不需要给别人留下深刻印象才算有效,所有技术人员最终都是在生产有效的技术。
但是……那么约定呢?
设定所有代码最终都需要重写的预期,确实意味着偶尔需要重写代码以使其符合现代约定或清理技术债务。什么值得修复的问题充满了微妙的差异。当我谈到不修复那些没坏的东西时,我指的是不因为单纯为了拆解单体架构而去拆解单体架构,也不是为了迎合外部的眼光而重写代码以适应最新的趋势。有很多时候,出于长期性能的需求而做出的变更,单凭现有的 SLO 是难以证明其必要性的。技术债务很少以可预测的方式影响性能。一个系统可能急需重构,但在监控仪表盘上看起来一切正常,直到某一天它突然完全崩溃。在决定是否花费时间和资金将一个系统与给定的约定重新对齐时,除了 SLO 之外,还有一些其他方式可以考虑其附加价值:
-
年龄 越古老的约定,越有可能深埋在现代技术栈的各个部分中。不符合标准的遗留系统会发现,他们可用的工具和选项越来越少。
-
说明:为什么推广这一惯例的人会推广它?它是一个好的安全实践吗?是否有充分记录的案例表明这一惯例能防止严重的失败?
-
倡导者:这一惯例的来源是什么?它是一个大型组织,许多其他组织必须与之合作吗?
-
开放性是基于开放标准的惯例吗?是否有许可或其他专有问题阻止人们采纳这一惯例?
何时拆分能增加价值?
由于这一部分花了大量时间驳斥“单体架构天生不好,必须拆分”的观点,因此在这里总结一些关于何时拆分单体架构的建议是有意义的。
单体架构是可以扩展的,但根据活动的增长方式,它们可能难以高效地扩展。例如,如果系统的某一部分比其他部分消耗更多资源,那么转变为一种架构,允许该部分分配更多资源而不影响其他部分的系统是有意义的。
单体架构拆分的原因更多的是因为组织规模的增长。如果有成百上千个工程师共同参与同一个代码库,沟通不畅和冲突的潜力几乎是无限的。在同一个单体架构上共享所有权的团队之间的协调,常常会将组织推回到传统的发布周期模型中,在这个模型里,一个团队测试并汇总一系列更新,作为一个巨大的包发布到生产环境中。这会减慢开发速度,更重要的是,它会减缓回滚,影响组织应对失败的能力。
将单体架构拆分成大致对应每个团队所拥有的服务意味着每个团队可以控制自己的部署。开发速度加快。增加一个复杂层次,即正式的、可测试的 API 规范,系统就能通过监管团队之间如何更改下游交互来促进团队之间的沟通。
复合问题:信任逐渐减少
大型且昂贵的项目启动,去修复那些并没有坏掉的事情,会破坏与组织中非技术部门的信任。它给同事带来不便,令他们沮丧,有时还会让他们困惑。现代化努力需要获得工程以外的部门支持才能成功。花时间和金钱去做那些对业务或任务方面没有明显影响的变更,会让以后很难获得那种支持。
不幸的是,软件工程师往往认为他们的学科非常困难,非工程人员甚至无法理解最基本的概念。来自组织非技术部门的抗拒往往被视为无知。这意味着,一旦信任被破坏,一个恶性循环就开始了。获取现代化支持的难度越大,工程部门就越认为问题出在非技术同事的智力和常识上。工程部门开始不再试图与业务部门沟通组织的价值和需求。他们的提案与组织需求脱节的程度越大,工程部门获得的信任就越少。
解决方案:形式化方法
修正一个正在修复并未坏掉的东西的团队是一个漫长的过程。最糟糕的事情不是修复错误的事,而是放任修复错误的事未完成。半途而废的项目会导致混乱、文档不全、维护难度加大的系统。如果你介入时,团队还没做太多改动,那么尽早制止他们所做的事是非常必要的。
否则,你必须保持专注。你的第一任务是将他们的主动性引导到一个可以停止工作的地方,而不会造成“弗兰肯斯坦怪物”般的结果。一旦你找到了那个点,接下来的挑战就是如何在流程中增加价值,让组织从错误中恢复过来并变得更强。
单体拆分和其他大规模重设计提供了改变流程以及改变代码的机会。即使是修复一个并未坏掉的东西,也能在把修复当作一个改善工程实践的机会中找到一些亮点。如果组织缺乏适当的测试,可以趁此机会建立并完善测试套件。如果组织没有监控,考虑哪些工具可能适用于新的架构。如果组织从未做过事件响应或轮班值守,可以通过创建新服务来建立这些实践。
如果组织已经做了这些事情,引入形式化方法。
形式化方法是将数学检查应用于软件设计以证明其正确性的技术。在尝试证明正确性的过程中,形式化方法可以突出那些仅通过研究代码无法发现的错误。最常见的形式化方法是形式化规范。它包括将设计写成一种规范,使用一种标记语言,模型检查器可以解析并进行分析。这些模型检查器会根据规范定义的有效输入,绘制出所有可能的输出组合。然后,它们会将所有这些可能的输出与规范中定义的有效输出规则进行比较,查找任何违反规范声明的结果。
截至目前,正式方法尚未被软件团队广泛使用。学习曲线陡峭,且几乎没有面向初学者的资源。用户社区本身规模较小,并且略偏向学术界。然而,工程团队并不需要每个成员都知道如何编写规范就能开始使用正式方法。一个组织可以从一个工程师开始,与其他团队合作起草和完善规范,就像工程团队通常有一个小型设计师团队来协作起草和完善用户体验一样。
正式方法帮助工程团队考虑更多的条件和扩展因素。它们还通过为每个人提供一个详细说明系统设计和预期行为的参考,改善了团队之间的沟通。
如果你找不到能够理解 TLA+语法、Alloy 或 Petri 网的人,开始引入正式方法的一个稍微简单的方法是使用合同测试。合同测试是一种自动化测试形式,检查系统组件之间是否破坏了它们的数据契约。在将单体应用拆分为微服务时,遵守这些契约或明确沟通何时需要打破契约,对于构建、集成和维护高性能系统至关重要。合同测试本身并不算是一种正式的规范方法,但其推广过程大致相同。它要求每个端点都必须有一个以特定标记语言编写的规范,合同测试工具可以解析并检查其中的矛盾。
强类型语言有时可以在没有额外工具的情况下进行合同测试,前提是仓库设置正确。例如,如果服务负责人负责编写端点、客户端库以及服务的测试模拟,那么他们可以自行测试是否有破坏性的变更。
混乱:被遗忘和丢失的系统
大型组织会丢失系统。我不是说系统崩溃了,而是说组织会忘记他们拥有这些系统,偶尔还会丢失它们的存在记录。为了应对这个问题,整个产品线都在设计相关解决方案:搜索网络上的虚拟机、遍历连接、检查依赖关系和管理库存。这种情况居然如此常见,真让人惊讶,因为这看起来是根本不应该发生的事情。一个组织怎么可能继续为它根本不知道存在的东西花钱呢?
当一个组织处于创业阶段时,通常会有一个小型的工程团队处理几乎所有的事务。随着架构的建设,团队不断地分裂和重组。某个时刻,组织可能会开始创建部门并委派所有权,但这就像是一场音乐椅游戏,常常会导致一些架构部分在音乐停下时找不到座位。
没有维护者的软件是发现各种怪物的关键地方,但如何找出那些没有所有者且被遗忘的软件呢?
一个潜在的方法是追溯那些在事情还很小的时候就参与的工程师的活动。在那些早期的日子里,优秀的工程师往往在项目之间跳跃,将精力集中在紧急和兴趣相结合的地方。由于软件是新的,且一段时间内会保持稳定,几乎不需要多少维护,所以他们很少考虑过渡规划。如果软件特别做得好,它可能会悄无声息地消失,默默地运行着,因为它似乎从未需要过维护。追溯那些早期工程师的活动,看看他们当初接触了哪些内容,现在谁拥有它们?
另一个选择是追踪资金流。被遗忘的服务仍然会消耗组织必须支付的资源。至少,应该有一些关于这些交易的记录。如果你使用的是商业云服务提供商,可以开始自动标记你的实例。这样可以突出显示那些未被记录的镜像。
复合问题:严重的风险规避
当架构如此复杂或如此陈旧,以至于它的某些部分被完全遗忘时,工程师们可能会觉得自己在雷区中工作。除非有效地为失败做出规划,否则没人能有效地为未知做出规划。如果没有接受并适应失败的能力,个体贡献者就会陷入“死循环”。改变一个边界不清、组件缺失的系统很可能会引发故障。不采取行动最终会增加失败的几率,但这种失败不会追溯到某一个特定的决策或行为。
工程师们做出的决策可能对系统整体健康造成更大的损害,但却不太可能引发他们作为个人会被指责的故障。系统的维护变成了一场烫手山芋的游戏,每年过去,风险都在不断增加,且越来越极端。尽管许多陷入这个困境的工程师知道自己选择的是对大家最糟糕的结果,但系统的复杂性使得他们永远无法觉得自己足够了解系统,以便安全地进行更改。
解决方案:混沌测试
最终,你必须接受可能无法追踪和考虑所有系统的事实。即使你找到了它们,搞清楚它们到底做了什么也可能很困难。如果你加入了一个已经遗忘系统的组织,你很可能会面对一个被这种现实困住的团队。这些工程师可能会在规划阶段停滞不前,因为他们徒劳地试图搞清楚最新的库存是否正确。他们可能害怕对任何系统进行任何更改,生怕会发现另一个被遗忘的系统,且这个系统也是一个关键的依赖项。
你必须学会适应未知。你可以通过强调韧性而非可靠性来做到这一点。可靠性很重要,但太多的组织将可靠性作为追求完美的目标,而这恰恰是他们不应该做的事情。站点可靠性工程师通常以“九个数”来谈论性能——也就是说,服务是否能够在 99.9%的时间内运行(三个 9),99.99%的时间内运行(四个 9),或 99.999%的时间内运行(五个 9)。由于这些数字是作为服务水平协议(SLA)的一部分进行计算的,而且 SLA 通常是写进组织与客户之间合同的条款中,因此组织中的非技术人员往往误解了这些“九”的价值。更多的“九”不一定更好。
五个 9意味着一项服务每年停机时间少于 5.25 分钟。所以如果发生问题,工程师只有几分钟的时间来唤醒、登录、诊断和修复它。即使她能够完成这些,失败每年也只能发生一次。我以前的一位同事,一位来自谷歌的资深工程师,曾经喜欢说:“四个 9 以上基本就是谎言。”你想要保证的 9 越多,工程团队就会越趋向于规避风险,越不愿进行必要的改进。记住,要达到五个 9 或更多,他们必须在几秒钟内对事件做出响应。这是巨大的压力。
SLA/SLO 的价值在于它们给人们提供了一个失败的预算。当组织不再追求完美,接受所有系统偶尔都会失败时,他们不再因为害怕改变而让技术腐化,而是投资于更快地响应失败。无论如何,这是这个理念。某些组织无法放弃追求五个甚至六个 9 的可用性。在这种情况下,平均恢复时间(MTTR)是一个比可靠性更有用的统计数据。MTTR 跟踪组织从失败中恢复所需的时间。
当我们遇到那些已经被遗忘、无法理解其功能的系统时,我们通常会选择关闭它们,看会发生什么。对老一代技术人员来说,这似乎是轻率的行为,但现代工程团队将这一做法称为混沌测试。工程中的韧性就是从失败中恢复得更强大。这意味着更好的监控、更好的文档和更好的服务恢复流程,但如果你不偶尔经历失败,就无法改善这些方面。
故意引发故障的理由是,如果发生了意外情况,那通常会发生在大家都高度警觉的时候,并且是组织专门为此安排的时间。当我们关闭一个系统时,我们会等待有人抱怨。抱怨的人要么是系统拥有者,要么是下游依赖的拥有者,但无论如何,我们结束实验时会比开始时对系统的运作有更多的了解。
如果没有人抱怨,我们往往只是将系统关闭然后继续前进。少了一个需要现代化的组件,仍然算是胜利。我们是否有时会在几个月后发现,我们关闭的系统实际上在做某些至关重要的事情?我不会撒谎;的确偶尔会发生这种情况,但这也正是为什么投资于测试和监控对任何规模、任何年代的系统来说都如此重要。如果某个组件是重要到需要专门构建来执行某项任务,那么当它没有执行时,应该有某种方式来提醒系统的拥有者。
混乱:制度性失败
如果系统的某一部分使用了一个不良的模式,那它在系统的其他地方也会存在。比如,有时候一个组织可能不知道数据库查询中的字符串连接是个坏主意。更有可能的是,你看到的不良模式是技术最佳实践变化的结果。还记得 Facebook 曾经认为 HTTPS 可以是可选的吗?几年前被认为是安全做法的东西,现在已经充满了容易被利用的漏洞。
因此可以推理,如果你有一款软件已经有几年没有人认真去维护,那肯定会有问题,而这些问题将是系统性的。它们将是贯穿整个系统的重复模式。工程团队为什么要做得不同呢?
最近,我看到这种腐化现象发生的时间越来越短,已经是几个月,而不是几年。尤其是安全问题,安全与被攻破之间的周期似乎越来越短。如果有某个东西六个月没有人碰过,那就是开始寻找问题的好地方。
一旦你发现了一个问题,下一步是判断它是一个模式还是一个错误。由于过时依赖而导致的安全漏洞显然不是一个模式。无意中移除代码中曾经存在的某些内容也不是一个模式。不对输入进行转义、以明文存储秘密、返回比请求者需要的更多信息——这些才是模式。
代码检查软件有时可以帮助追踪到坏模式的所有实例。但有些问题不容易显现,需要实际的人力去处理。如果你发现了这样的问题,首先要做的是定义代码的上下文。它在做什么?什么类型的请求会触发它,它调用了哪些过程和服务?关于模式的好处是,如果你知道它们的上下文,你可以预测它们。如果一段坏代码调用了数据库,那么寻找其他坏代码的自然地方就是那些也调用该数据库的地方。
在最坏的情况下,问题跨越了应用程序的边界。分析坏模式的上下文的一部分应该包括其来源。换句话说,谁构建了这个东西?如果同一个团队大约在同一时间构建了两个应用程序,那么不太可能使用完全不同的开发实践。
累积问题:没有负责人
系统性问题,无论是在代码库中还是文化中,的问题在于没有人真正拥有它们。如果它们影响每个人并且每个人都参与其中,唯一有权修复它们的人往往是最不具备修复能力的人。CEO 或内阁部长不会有太多运气忽视自己的职责去深入解决一个系统的实施问题,无论这个系统多么庞大或关键。这样的领导者可以将责任委派给一个更具战术性的下属,但该任命者可能会发现自己在与无休止的政治斗争作斗争。
影响多个组织单元的问题需要跨越这些边界进行协调才能解决。组织越是重视这些边界——围绕它们制定预算和招聘周期——这些单元的高层人员就越会监控自己的边界。这就引发了往往是自我强化的政治斗争。领导们有自己的“领地”,他们为现有的资源付出了艰苦的努力。如果他们将这些资源的一小部分重新分配给机构性问题,而他们的同僚忽视该问题且问题未得到解决,这些资源可能会永久丧失。当没有跨职能协作的先例时,谁会承担成为首个行动者的风险?
解决方案:黄色警戒
系统性问题几乎总是在中途出现。当我们发现它们时,我喜欢将这些问题记录下来并以BOLOs(即留意事项)的形式发布给更广泛的组织。我们会发布简短的公告,用通俗易懂的语言解释问题,指明我们发现的具体例子,并为其他团队提供我们团队的联络点,以便他们在发现类似问题时联系。如果问题特别严重,我们将会组织简短的讲座,展示坏代码的样子、如何识别它,以及描述合适和不合适的修复方法。有时我们会特别联系其他团队。
广义上讲,这些技术是一个名为Code Yellow的方法论的一部分,这是一支跨职能团队,旨在解决对运营卓越至关重要的问题。术语Code Yellow既指代团队,也指代支配团队活动的过程。这是谷歌为处理那些超出任何一个组织部分职责范围的问题而发展出来的一种做法。与谷歌的其他流程不同,它并没有被记录在成千上万本管理书籍中,所以似乎只有曾在谷歌工作的人或受过前谷歌员工培训的人才知道什么是 Code Yellow 或如何进行 Code Yellow。通过口头传承的方式,它已经传播到其他工程组织。
Code Yellow 的目的是为了创造势头。当一个遗留系统存在性能、稳定性或安全性的问题,并且这些问题是系统性的,并且与其他问题交织在一起时,这会让人感到不堪重负和沮丧。没有人能够改善局面,因为每个人都被问题的总量所分散注意力。没有单一的改进措施能够产生足够的影响来扭转局势。
Code Yellow 具有以下关键特点,这些特点确保了它比其他项目管理方法更为成功:
Code Yellow 的领导者拥有提升的权限。领导者可以指挥任何和所有执行 Code Yellow 所需的资源。这包括人员、会议室、办公室等等。领导者可以在没有正常指挥链批准的情况下,从其他团队调取这些资源,无需长时间的解释或讨论。
领导者作为工作的核心联络点。Code Yellow 问题通常既是系统性的,又是敏感性的。当组织宣布 Code Yellow 时,可能并不知道问题的全部范围。通过创建一个核心联络点,组织中的各个团队可以将问题转交给 Code Yellow 领导者,并获得明确和具体的指导。无关的问题可以轻松诊断并解决。
团队规模小。团队的构成可能会随着领导者从其他团队调入专家并将其释放而变化,但团队在任何时间点的人数都保持在八人以下。这些人应该能够实施解决方案,他们不仅仅是其他团队的代表。
团队专注。当被分配到 Code Yellow 时,团队成员会被解除所有其他角色和责任,以便他们可以将精力 100%集中在 Code Yellow 上。
Code Yellow 是临时性的。在宣布 Code Yellow 之前,组织应设定成功标准。情况需要改进到什么程度才不再是危急的,剩余的工作可以通过适当的优先级规划,纳入现有团队的路线图中?Code Yellow 可能会持续数月,但不应持续几个季度。Code Yellow 的临时性有助于克服政治竞争,这些政治竞争往往使系统性问题更难解决。Code Yellow 保证只有紧急需要的资源会被征用,并且在危机结束后这些资源会尽快归还。
如果一个问题紧急且其范围超出了一个组织的单一单位所能处理的范围,它就值得启动 Code Yellow。这通常意味着安全性或系统可靠性问题。偶尔,Code Yellow 也可用于处理一些更微妙的问题,这些问题影响组织的整体竞争力。2008 年,Google 在内部研究表明延迟对用户长期行为产生负面影响后,启动了一个 Code Yellow。
人们可能认为实验中涉及的微小延迟是可以忽略不计的——这些延迟在 100 到 400 毫秒之间。但即便是这些微小的搜索结果延迟也成为了未来搜索的障碍。搜索量的减少虽小却显著,甚至在 100 毫秒(十分之一秒)延迟下也能测量出来。更重要的是,即使延迟被消除,那些曾经暴露于较慢结果中的人们也需要很长时间才能恢复到他们之前的搜索水平。
[. . .]
这个 Code Yellow 在一个 TGIF(感谢周五)活动中启动,Hölzle 测量了全球各种 Google 产品的性能,Charlie’s Café 的大屏幕上展示了实时跑马灯,指出各个产品的性能缺陷。“当人们看到 Gmail 在印度的速度极慢时,你可以听见房间里的针掉下来,”Google 公关总监 Gabriel Stricker 说。^(1)
在 2010 年,另一个 Code Yellow 被启动以应对“奥 Aurora 行动”后的影响,这是一次中国政府的网络攻击,攻击者侵入了 Google 的公司网络并允许中国情报部门窃取信息。
在 2015 年,Chromium 团队(支撑 Google Chrome 的开源项目)启动了一个开发者生产力 Code Yellow,旨在改善性能,使吸引和留住贡献者变得更加容易。
所有这些问题都是至关重要的,但它们各自有所不同。它们有不同的范围。只有一个呈现出传统的危机特征。但在每种情况下,问题都会对单个团队或孤立的部门造成解决上的困难。通过建立一个小而有权力的团队来启动响应,Google 能够创造出一种焦点和动力,使看似不可能的问题变得可以解决。
黄色代码在问题脱离关键阶段时结束,而不是在问题完全解决时结束。黄色代码的一部分应该是制定执行长期改进、升级和开发工作的计划。如何分配后黄色代码的工作?谁来追责?黄色代码团队的组成应反映这一点;如果长期工作涉及特定团队,那么这些团队的成员应成为黄色代码的一部分。在黄色代码结束时,这些成员将返回各自的团队,并继续将黄色代码工作纳入常规路线图中。
在定义关键性问题的界限时,领导层对风险的容忍度很重要。黄色代码的效果会随着工作时间的延长和紧迫感的降低而减少。一旦恢复正常状态,制定清晰的工作完成计划并追究责任,往往能比依赖小规模精英团队来挽救局面取得更好的结果。
发起黄色代码
黄色代码应由最低级别的领导者发起,而不是最高级别的领导者。只有那些对所有受影响组织部分具有权力的领导者才能发起黄色代码。在一个小型组织中,黄色代码通常由非常高级的领导发起,但随着组织的发展,这种做法变得低效且官僚化。通过将权力下放给最低级别的领导者,且该领导者对所有受影响领域拥有权力,组织能够继续快速响应关键问题。
换句话说,如果问题是跨越多个团队、由多个总监负责的工程问题,那么有权发起黄色代码的人是这些总监的上级工程副总裁。如果问题还涉及到其他副总裁领导的团队,则有权发起黄色代码的人是副总裁的上级。
有时,随着团队揭示更多细节,黄色代码的范围可能会扩大,影响到组织的更大部分。在这种情况下,通常不会重新审视发起黄色代码的决定,尽管在沟通策略和成功标准上可能需要做出一些调整。例如,状态会议可能会扩大,涵盖来自其他小组的领导者。
一般而言,工程经理或总监不会发起黄色代码,因为他们的影响范围应该足够小,可以通过其他项目管理策略来管理问题。黄色代码用于系统性问题;如果某个问题完全处于单一工程经理的领域内,而没有触及或影响到其他任何小组,那么它就不是系统性问题。
运行黄色代码
Code Yellow 的负责人扮演的角色类似于事件指挥官,负责分配任务给团队成员,并作为最终决策者。他们需要具备足够的技术知识和实施经验,以便自信地作出决策。他们还需要能够将 100%的注意力集中在 Code Yellow 上。出于这些原因,高层领导通常不适合担任 Code Yellow 的负责人。腾出他们的日程专注于 Code Yellow 会阻碍组织内其他重要事务的进行。他们可能不了解产品或架构的详细工作原理,无法快速做出决策。然而,负责人不一定非得来自工程团队。产品经理、员工工程师和首席工程师都可以成为优秀的 Code Yellow 负责人。
理想情况下,Code Yellow 的负责人应当对组织有足够的了解,知道谁是专家,哪些团队负责哪些部分的产品,等等。这使得他们能够保持团队规模较小,减少在识别资源时的讨论量。
在宣布 Code Yellow 时,确保整个组织都知晓是很重要的。如果组织规模较大,可能不需要向所有员工广播 Code Yellow 的公告,但每个可能有相关信息或资源,并且将在 Code Yellow 中重新分配的团队都需要知情。这有助于负责人在接触其他团队时更加顺利。
由于 Code Yellow 通常比较敏感,因此在宣布时不需要提供过多的细节。如果 Code Yellow 是由可供所有人访问的问题、工单或讨论引发的,公告应链接到这些内部讨论以供参考。否则,Code Yellow 公告可以仅定义当时已知的范围(例如,“我们正在宣布应用安全的 Code Yellow”)。
Code Yellow 公告必须清楚地指出负责人是谁,通常会使用类似“John Doe 可能会就此事联系你”的表述。
作为 Code Yellow 负责人,他们的一项责任是处理与 Code Yellow 相关的沟通,包括向领导层报告进展情况。尽管 Code Yellow 可能会带来压力,但负责人花费在与高层领导开会讨论 Code Yellow 进展的时间越多,解决 Code Yellow 问题的时间就越少。
每日进行 5 到 15 分钟的站立会议是一种平衡方式,但并非强制要求。一些组织会创建一个实体或远程的“作战室”,让 Code Yellow 团队成员在其中工作。如果组织的监控工具足够强大,能够在无需大量工程工作下应对这一情况,那么设置仪表盘以跟踪与 Code Yellow 相关的关键指标有助于让每个人保持专注。
混乱:领导层已经失去控制
失去房间是一个体育术语。它意味着教练已经失去了队员们的尊重。团队不再按照命令行动,合作变得困难,反而挣扎着自我组织。
本书花了很多时间讨论价值和动能,因为遗留系统现代化的成功更多的是关于技术执行,而不是进行现代化的团队的士气。那么,如果你接手的团队已经士气低落到连听你说话的耐心都没有,无法采用本书中提到的其他技术,你该怎么办呢?
人们往往过于急于将士气问题与性格缺陷等同。激励因素在一个组织中谁能有效工作,远比他们的性格这一虚构的概念要重要得多。那些拒绝对自己创造的员工困境负责的组织,很难实现运营卓越。他们发现自己有一种独特的能力,能够在数百个候选人中找到并聘用那几个“坏苹果”。他们看到有选择的才俊离开,却抱怨缺乏忠诚、正直或心理韧性。
记住,没有人愿意在工作中表现得很差。流行文化常常传播关于懒惰、愚蠢、不关心的官僚的神话。将人们归类为这样的人很容易。相信这些问题实际上是性格缺陷,意味着你无需认识到你已经创造了一个让人感到被困住的环境。他们被相互冲突的动机所困,没有任何取胜的办法。
一个组织并不一定是政府的一部分才算是官僚。领导者失去“房间”通常是因为组织已经把工程团队推到了一个无法成功的地方。不要忽视这个结果中“不信任”因素的重要性。团队不会因为一个项目失败,甚至因为一个项目多次失败而拒绝他们的领导。团队拒绝他们的领导,是因为他们觉得成功被从他们手中夺走了。要么他们做出了真正的贡献却被忽视,或者他们为了实现运营卓越所做的努力被组织中的领导者所破坏。
有时,你仅仅通过改变环境,就能恢复信任并将团队从死灰中复生。当团队失去对领导的信任时,移除旧的领导者并替换上新领导者,他们将赢得信任,而不是假设自己已经拥有了信任。
但是,允许恶性局面持续下去,时间越长,心理根源就会越深。对组织及其领导的不信任,会削弱团队对自己的信任。被自己的领导背叛是创伤性的。人们处理这种创伤的一种方式是自问是否配得上这种对待。移除被排斥的领导者可能解决了表面问题,但并不能让团队恢复卓越。
缺乏信心的人会自我破坏。他们创造自我实现的预言,并表现出学到的无助感。例如,我曾经有一个团队,他们经历了频繁的部署失败,每周至少发生一次生产环境问题,需要回滚。与此同时,组织还不断要求他们生产更多的成果,同时削减他们的人员并限制他们的资源。在他们的旧经理被解雇后,我接管了这个团队,从我们最初几次的对话中很明显,问题并不在于他们的工程能力。他们被要求改进一项已有很长时间没有更新的遗留技术。那项技术几乎没有测试、没有监控,且部署流程复杂。
这个遗留系统之所以没有得到更新,是因为组织一直拒绝为此任务配备团队或投入任何重要的资源。更糟糕的是,处理数百万交易的整个基础设施已经由一个人维护了多年。
团队成员完全丧失了士气。他们失去了对安全交付代码的信心,因此放弃了更大、更具创意的技术挑战解决方案,这些解决方案可能本可以帮助他们。他们对现状感到无奈,仿佛停机是不可避免的。
他们没有进行测试。当问题发生时,他们没有进行彻底调查,确认故障点到底在哪里。他们回避这些事情,不是因为他们不明白它们的重要性,而是因为他们对自己能力的信心丧失。在经历了这么多的失败和多年未得到资源支持的情况下,他们觉得组织中的其他人认为他们是差的工程师,并且拼命避免证实这一点。
这个情境可能听起来违反直觉。如果他们如此害怕失败,应该更加测试,更加深入调查。为什么他们还会坚持一个自己明知有问题且会增加失败概率的流程?就像薛定谔的猫,如果他们没有一个合适的流程,他们可能同时既活着又死了。如果他们没有一个合适的流程,他们就永远不必面对这样一个潜在的现实:他们只是差的工程师。总有可能一个更好的流程能解决他们所有的问题。
然而,如果他们实施了更好的流程,但仍然失败,他们将失去那条他们依赖的心理救命稻草。这个团队把自己注定走向失败,因为他们害怕发现一直以来问题就在他们自己身上——而不是他们的流程、不是组织的资源匮乏、也不是那个没有经验的经理。
累积问题:自我破坏的团队
信心先于成功。成功很少能创造信心。当团队没有信心时,他们总会找到某种理由来否定成功的结果。他们运气好。结果没有达到预期,或者如果是另一支团队负责,结果可能会更好。成功的结果没有超越过去的失败。
当人们无法接受成功的结果时,他们往往会完全避免成功。他们会自我破坏,因为现状是安全的。
信心问题总是会不断累积。唯一能说服人们停止贬低自己的,是知道他们赢得了同伴的信任和接纳。
解决方案:谋杀委员会
谋杀委员会是我在政府工作时学到的技巧,并且我将其重新应用于工程团队。在政府中,我们使用它来为国会证词或确认听证会做准备,但将其应用于技术挑战并不是完全不被理解的。NASA 的艾姆斯研究中心就用它来准备卫星发射和请求研究资金。
谋杀委员会的运作方式是,你将组建一个专家小组,他们会提问、挑战假设,并尝试找出计划或提案中的漏洞。这个过程被称为“谋杀委员会”,因为它的目的是充满对抗性。专家们不仅仅是在指出提案中的缺点;他们的目标是直接“谋杀”这些想法。
谋杀委员会是那种只有在特定情况下才非常适用的技巧。为了使其成为一个富有成效且有益的练习,谋杀委员会必须发生在极为紧张的事件之前。谋杀委员会有两个目标。第一个是通过确保候选人能回答每一个问题、回应每一个关切,并为每一个可预见的问题准备应对策略,来为他们准备一个紧张的事件。谋杀委员会的第二个目标是增强候选人的信心。如果他们能够知道自己经历了谋杀委员会的过程并成功渡过,那么他们会明白,自己计划或证词的每个方面都已经经过了严峻的考验。
我为我的团队安排了一个“谋杀委员会”,尽管这个过程有些糟糕,但我理解,在任何事情变得更好之前,团队成员们首先需要明白,工程团队的同事们并不会看不起他们。他们需要看到每个人都希望他们成功,他们过去经历过的目标不断变动、承诺的资源被拿走、以及以不良信念做出的行动都已经结束。
他们还需要克服对自己不够好,无法改善流程,或者认为改进后的流程不会影响他们成功的几率的恐惧。我让他们写下测试、部署和监控一个关键变更的计划,并准备为其辩护。他们对做“谋杀板”并不兴奋。我花了一些时间说服他们,这个活动可能是有益的。他们担心的部分原因是,他们觉得这个活动会让同事们过度管理他们、贬低他们,或者像对待愚蠢、不可信任的人一样对待他们。
我辩称,这是一个向大家证明他们的工程挑战有多么困难的机会。他们将带着一个新流程离开“谋杀板”。如果新流程仍然失败,大家都会知道它已经得到了组织内最佳工程师的审查,他们再也做不到更好。通过这种方式,我利用“谋杀板”解决了他们打开薛定谔盒子的恐惧。在更好的流程下的失败并不意味着他们是糟糕的工程师。那个流程已经在“谋杀板”上经受过考验,但仍然失败了。
为了实现这些目标,确保“谋杀板”双方都明白此次活动的目的是让候选人变得更强大是至关重要的。必须毫无疑问地让每个人都明白,大家是同一支队伍,为候选人的利益而工作。批评应该是严厉的、挑剔的、不宽容的,但只有在与即将到来的紧张事件相关时才应提出——在本例中是一次部署。因此,在没有即将到来的紧张事件作为背景时,我们不会做“谋杀板”。
给“谋杀板”设定一些边界是很有用的。我们不会在“谋杀板”上翻旧账或者算账。我们不会用“谋杀板”来贬低或侮辱别人。我们也不会用它来做宏大的演讲。董事会成员可以提问、指出缺陷或提供假设情况。他们可以提供详细的解释,充分阐述他们想要强调的问题,但他们应该尽量避免这样做,让团队成员尽可能用自己的话来说。最重要的是,董事会的评论应该完全是负面的,即使所呈现的计划有明显的优点。“谋杀板”能够建立信心,因为它们是经受住考验的。
停止流血
本章讨论的技术都围绕着将陷入困境的项目转变为一种问题不再成循环积累的状态。如果组织正在进行的变革无法提供足够的价值来证明其费用是值得的,就通过将这些变革转化为更好的工程实践来提升它们的价值。如果组织因为缺乏信息和未知的复杂因素而陷入停滞,促进韧性并消除失败的恐惧。如果问题超出了任何一个团队可以单独解决的范围,让组织围绕这个问题临时重组。如果团队士气低落到自我伤害的程度,挑战组织的其他部分来帮助他们取得成功。
遗留系统现代化项目并非因为犯了一个错误或某件事情一次性出错而失败。它们失败的原因在于组织部署了实际上会加剧不成功局面的解决方案。如果你在项目进行到一半时加入,作为领导者,你最重要的任务就是找出这些恶性循环并加以制止。
第七章:设计即命运
设计并不仅仅是让事物看起来漂亮。
许多我曾与之共事的软件工程师在被指出之前,从未考虑过这一事实。这是一个容易犯的错误。设计思维最显著的输出是包装——我们如何谈论事物,事物的外观,功能的位置以及功能的行为。当我们考虑最终结果时,设计师似乎最有效的时候是被 relegated(委派)去完善产品的最后阶段。当我们这样忽视设计师的工具包时,实际上是在给自己和团队带来不利影响。设计对于做出好的技术决策至关重要。《美国陆军/海军陆战队反叛乱作战手册》^(1)最好地表述了这一点,它建议士兵:
“规划是解决问题,而设计是设定问题。”
问题-解决与问题-设定的区别在于,前者是反应性的,而后者是应对性的。反应性的团队往往漫无目的地四处奔波。挫折削弱了他们的信心和协调能力,势头很难保持。而应对性的团队则更冷静、更深思熟虑。当新信息到来时,他们能将其整理到不同的范围和情境中。因为设计思维让他们洞察到变化的原因,所以他们能够在不影响信心的情况下改变方法。
对于任何大型复杂项目,团队能够明确问题并适应新信息时,成功的概率会提高。当问题设定得当时,它使团队的每个成员都能够自主行动,依靠直觉和判断力。至少,问题设定可以让所有人对项目的目标和成功的标准达成一致。最大化设计思维影响的传统项目不仅仅是现代化,而是创新。
如果这些说法听起来很熟悉,那是因为我在前几章中已经描述了几个用于问题设定的设计练习。在第二章,我讨论了如何通过从熟悉的接口出发来提高技术被采纳的可能性。在第三章,我解释了如何根据复杂性和耦合性来映射一个系统。在第五章,我介绍了如何通过界定问题来解决困难的技术对话。所有这些都是设计练习。现在,是时候深入探讨我之前提到的一些问题设定方法的变化形式了。
本章的第一部分集中于将设计技巧应用于技术决策:如何构建技术对话、界定问题以及达成共识。
本章的第二部分集中于使用设计技巧来对齐激励。在上一章中,我提到过,冲突的激励会导致项目失败并打击团队士气;本章将介绍如何找出组织内的激励因素,并根据这些信息来为团队定位成功。
设计技术对话
第五章介绍了“范围”这一概念,作为避免无效会议的解决方案,但实际上,管理重大现代化项目的过程就是在操控范围。
范围是由你想要解决的问题决定的,但很少有问题是完全独立于其他因素的。决定哪些因素对解决这个重要问题的成功或失败有影响,哪些没有影响,需要经过深入和定期的反馈。你必须变得擅长收集数据,因为有许多因素可能会使现代化项目变得复杂。它们包括历史背景、技术约束、可用的人力资本技能以及内部政治。
此外,通过反馈循环传递给你的信息可能是错误的,或者你会错误地解读它们。最简单的设计练习就是与用户交谈。这样做总比什么都不做要好,但在非结构化的对话中,反馈的质量可能会有所不同。有时候用户不知道自己想要什么。有时候,用户和研究人员使用相同的词语来表示不同的意思。有时候,用户和访谈者之间的权力动态过大,用户会告诉访谈者他或她想听的话。
设计思维改变了我们解决这一挑战的方式。它强调我们提问的方式、提问的对象以及谁来提问是决定哪些信息会浮现并首先被讨论的决定性因素。
不要低估社会动态在扭曲信息准确性方面的作用。我们知道人们在被观察时会表现得不同。我们知道人们倾向于避免冲突并随波逐流。我们知道并不是每个工程团队中的声音都有同样的分量。设计练习之所以能够成功,是因为它们考虑了这些影响,而普通的技术对话往往忽视了这一点。
如果我们把普通的技术对话看作是具有对抗性的,个人要么提出解决方案,要么挑战他人的想法,那么团队成员就有很多机会参与不生产性的行为。在团队面前看起来聪明的做法不一定会转化为好的技术战略。
但是在设计中,我们可以改变赢得论证的路径。在正常的团队对话中,个别成员通常是在寻求提升或维持自己在小组中的地位。那么,什么能提升他们的地位呢?击败他人的想法。展示他们发现了别人忽视的某个关键缺陷。提出一个绝妙的解决方案。在这些选项中,提出一个绝妙的解决方案是最难做到的。打击他人的想法通常要容易得多。因此,在成员争夺地位的环境中,这种行为可能会被过度选拔。
现在,想象一下我们开始对话时告诉团队,我们会根据他们提出的解决方案是否使用特定技术来给予积分。随着每个人都集中精力编制最长的潜在解决方案清单,用来反驳创意的时间将大幅减少。
这就是设计的价值。当我们设计对话时,我们将其转化为游戏。我们将团队成员的精力转移到提供更多、更好的答案上,而不是仅仅纠正自己或让同事犯错。
如何进行设计练习
本书中包含设计章节的目的不是将软件工程师培养成设计师。我对技术人员习惯性地认为自己可以迅速掌握其他人花费多年培养和研究的学科持怀疑态度。我相信技术人员应该专注于将技术专长带到桌面上,并寻求其他专家来补充他们的技能。因此,我鼓励你通过聘请设计师或更好的是,咨询你已雇佣的设计师,将设计思维融入到你的流程中。
话虽如此,了解设计思维如何运作还是很有帮助的。设计练习有各种形式和大小,但它们都有以下四个明显的阶段:
-
热身 热身活动为参与者提供一个从日常生活干扰中暂时脱离的机会,让他们能够集中精力在当前任务上。最简单的热身活动是列出几句介绍你的主题/目标/意图的句子,但更积极和复杂的活动可能会花费更多的时间和精力进行热身。提出一个简单的问题进行小组讨论、配对工作,或通过问卷调查人们的经验,都可以作为热身活动。
-
研究问题 当我们进行设计练习时,心中通常有一个特定的研究问题。我们面临一个问题或需要做出决策,并希望听取其他人的观点。或者,我们即将投资一款新产品,希望了解用户是否会喜欢它。工程团队最常见的设计练习是观察潜在用户与产品的互动。一个好的研究者会小心谨慎,不引导用户,也不教他们如何使用产品,而是让他们自然地与产品互动,并通过精心措辞的问题引导他们关注与研究目标相关的功能。
-
跟进问题 人们在设计练习中常常会说出我们未曾预料到的话,这需要我们稍微偏离我们预设的结构,去理解这一新出现的信息。跟进问题或活动用于在个别问题出现时进行深入探讨。
-
聚合 在某个时刻——可能是在一次练习之后,或者在一系列访谈之后——我们需要查看所有数据并得出结论。就像工程学一样,设计通常是一个迭代过程。一项练习的结论可能会为下一次研究提出新的问题。例如,如果用户研究揭示出用户不理解如何与产品互动,那么未来的研究会测试不同的界面,直到组织找到一个适合用户的解决方案。
关于后续问题:为什么与如何
创建有效的后续问题本身就是一种艺术。与研究问题一样,要小心它们不要暗示自己的答案或产生可能会偏向数据的模糊性,但与设计研究问题不同的是,几乎不可能事先预见你可能需要跟进的所有问题。你需要临时编写这些问题。
一个好的经验法则是,以为什么开头的问题会产生更抽象的陈述,而以如何开头的问题则会产生更具体、可操作的答案。想一想,如果后续问题是“什么是最适合这项工作的工具?”和“你怎么知道这些工具最适合这项工作?”你的回答会有什么不同?在第一个问题的回答中,你可能会列出一堆常见的解决方案,确信它们之所以好是因为它们很受欢迎。而在回答第二个问题时,你更有可能描述自己使用过的工具的各种经验。
为什么问题和如何问题都可以是有用的。为什么问题通过引入尚未见过的因素和力量,扩展了研究领域的边界。如何问题则让你置身于用户的思维中,这样你就能看到他们理解这些因素的方式。为什么问题通常会引出如何问题。
一些有用的工程团队设计练习
设计是一个充满有趣方法和哲学的丰富行业,远超一个单独章节所能囊括的内容。为了帮助你入门,我提供了我最喜欢的一些技术对话练习。可以把它当作一个工具包。这些练习有些是从 Henri Lipmanowicz 和 Keith McCandless 的《释放结构的惊人力量:简单规则激发创新文化》一书中松散改编的,这是一本很好的进一步学习资源。^(2)
练习:关键因素^(3)
这是一个与团队进行头脑风暴的练习,帮助优先考虑围绕现代化活动初期阶段的讨论。为了项目目标成功,必须发生什么?又必须避免发生什么?每个人发言并记录自己的想法后,团队编辑列表,确保其中的每一项确实值得保留。一个好的方法是,团队根据是否在其他所有关键因素都朝着有利方向发展时,项目是否能成功,来讨论每一项。唯一应该留在列表上的项目是那些有能力单独让整个项目失败的因素。
行动之后: 早期的技术讨论应集中在实现或维持这些关键因素的良好结果上。范围内的问题推动这些关键因素向积极方向发展。范围外的问题不会影响这些结果。
练习:破坏者^(4)
一个与关键因素练习相似但相反的头脑风暴练习是要求你的团队扮演破坏者角色。如果你想保证项目失败,你会做什么?怎样才能达到最糟糕的结果?一旦列出这个清单,团队讨论是否有任何内部或外部合作伙伴的行为,接近破坏者清单上的项目。
行动之后: 一些破坏者清单上的行为可能是需要改变的习惯或低效的流程。根据你的结果,这些项目可能值得作为关键因素处理。然而,更可能的是,破坏者清单将向你展示你团队中的裂缝。它们最容易受到什么干扰?他们对真正的威胁了解多少?团队成员之间是如何体现内部政治的?破坏者练习应帮助你预见可能会被提出的超出范围的问题,以及这些问题可能来自谁。从一开始就有这种预感有助于保持技术对话的正确轨道。如果你能通过定义什么是范围内,什么是范围外,来开启会议,那么让每个人负责就变得容易得多。
练习:共享的不确定性^(5)
这个练习同样要求团队成员识别可能影响项目成功的潜在风险和挑战,但这次你关注的是不同的人如何看待这些风险。给每个团队成员一个四象限图,图中包含以下坐标轴:
-
简单到复杂:问题如果定义清晰并且理解透彻,就很简单。如果它们的原因不明确,或者解决它们意味着必须放弃其他有价值的东西,那它们就变得复杂。
-
有序到混乱:当解决问题的方法没有太多争议,尽管这些解决方案可能漫长而繁琐时,问题是有序的。当解决方案可能会意外地使局势更糟时,问题就是混乱的。
每个团队成员将挑战放置在这个图谱的某个位置。然后,作为一个小组,他们会比较结果。它们之间有多远的差距?共享的焦虑点在哪里?是否有人完全不同步?根据团队的组成,您可能希望事先商定要映射的挑战,或者让个体在小组内提出要映射的挑战。未在映射前达成一致的好处是,如果团队成员来自不同的组织单位或职能领域,您可以通过不要求他们使用相同的挑战,更好地看到知识差距。
执行后: 这个练习最大的好处无疑是它以一种非对抗性的方式引入了替代的视角和优先级。在公开讨论中,不同的视角常常被作为对他人视角的回应。这使得贡献显得像是反驳,鼓励人们不去相互理解或倾听对方的意见。
当团队成员之间的重叠和共识度很高时,优先级的感知也会自然产生。如果每个人都认为某个挑战是有序且简单的,团队可能会倾向于认为它不在讨论范围内,直到围绕更困难的问题制定出策略。
关于简单/混乱和有序/复杂的问题,如果你遇到其中任何一种,它们是很适合早期讨论的好问题。它们通常是最具威胁性和引发焦虑的。
练习:15%的改善^(6)
在第三章中,我谈到了将某些事物改善 5%、10%或 20%的价值。这个练习要求团队成员绘制出他们自己能够做到的,推动项目实现目标的具体措施。它们有什么权限?他们预见到哪些阻碍因素?这些阻碍因素何时变得重要?他们在没有批准的情况下能走多远?当时需要谁的批准?
让每个团队成员头脑风暴出一个按优先顺序排列的行动清单,列出他们现在能采取的措施,以使局面改善 15%。数字 15 是任意的;不要纠结于这些行动的实际效果是否只会带来 8%的改善。关键是,这些行动不需要接近解决问题的程度;它们只需要推动事情向前发展。
当每个团队成员都有了自己的清单后,团队应当讨论这些条目,必要时进行优化,并承诺执行。
执行后: 最好的技术讨论是那些你不需要进行的讨论。这个练习帮助团队弄清楚在哪些地方需要做决策,哪些地方仅需要建议和支持其他团队成员。讨论潜在的阻碍因素和批准者有助于集中邀请那些最相关的人参与需要安排的会议。没有什么比让不必要的人参加会议更能引发偏题讨论。
专门针对决策的练习
前面描述的所有练习都假设一旦信息收集并呈现给团队,正确的决策是显而易见的。但事情并不总是这样。当你作为团队收集完所有数据并进行了充分的讨论后,下面是两个额外的练习,专注于决策制定。
练习:基于概率结果的决策制定
基于概率结果的决策制定更广为人知的名字是下注。这是一个很好的技巧,适用于那些难以逆转、可能带来严重影响并容易受到确认偏差影响的决策。例如,我通常会在招聘委员会中广泛使用这个方法。解雇员工很困难;错误的招聘可能会摧毁一个团队的生产力,而且人们往往只看到他们想看到的潜在候选人。
这个方法是这样的:作为一个团队,我们列出需要做出的决策可能带来的潜在结果。比如“通过这样做,我们能够实现 2 倍增长”或“我们将在这个日期之前实现这个新功能”。如果你愿意,可以将正面和负面的结果混合在一起,但我发现如果结果列表只是正面的或负面的,讨论通常会更顺畅。
然后,团队成员根据结果是否会实现进行下注。传统上,这个练习使用的是虚拟货币。根据具体的决策,我有时会要求他们用时间而不是金钱下注。
这个赌注的机制与其他任何情境中的机制一样。如果你下注很多并且赢了,你就获得很多;如果你下注很多但输了,你就损失很多。因此,仅仅要求某人在结果旁边标注一个单位值,就迫使他们表述自己的信心水平。这种设计的奇妙之处在于,如果你要求人们在 1 到 10 之间给出自己的信心水平,大多数人会很难回答。正是单位本身,也就是他们知道一美元或一小时对他们的意义,以及失去一定数量的钱或时间意味着什么,帮助研究对象表述他们的感受。即使他们不会失去所下注的内容,仅仅想象这么多钱或这么多时间就足以帮助人们将自己的感受放置在一个范围内。
当你在为自己的决策感到困惑时,可以独自做这个练习。当我与团队一起做时,我喜欢将每个结果的赌注放在共享文档或白板上。然后我们讨论团队对不同决策方式能够达到正面结果的信心有多大。到此时,正确的决策通常会变得更加明显。
练习:亲和图
亲和图法是一个常见的设计练习,涉及将个人的想法和陈述视觉化地聚类。这通常需要一个大的空白表面,通常是墙面或白板,以及一些标记笔和便签纸。你可能做过亲和图法练习。每个人将他们的想法写在便签纸上,然后将其贴在墙上。同时,主持人会将便签纸移动,按照共同的想法或感受将其组合成群组。
亲和图法在构建类别时效果很好,但它也能揭示出达成某一决策共识的具体困难。通常在开放讨论中,人们会错过彼此的观点,或者在表达不同概念时假设大家的意思相同。亲和图法能够揭示出团队成员之间真正的差距,以及最大的分歧点在哪里。
团队结构、组织结构与激励措施
1968 年,梅尔文·康威(Melvin Conway)发表了一篇名为《委员会是如何发明的?》的论文^(7)。这篇论文最初是为哈佛商业评论所写,但因其内容过于推测性而被拒绝,论文概述了组织的结构和激励措施如何影响其所生产的软件产品。该论文反响平平,但最终被北卡罗来纳大学教堂山分校计算机科学系的系主任弗雷德·布鲁克斯(Fred Brooks)看到了。当时,布鲁克斯正在思考他在 IBM 离职面谈中提到的一个问题:为什么管理软件项目比硬件项目更难?康威关于软件结构与发明该软件的委员会结构之间的联系的见解,似乎足够重要,促使布鲁克斯在 1975 年出版他关于有效管理软件团队的指南《神话中的人月》时,将这一理论重新包装为“康威定律”^(8)。
然而,这并不是康威论文中唯一有用的观察结果。自布鲁克斯将其作为普遍真理之后,康威的定律已被数百本计算机科学教材引用,但支持康威论点的更为细致的观察大多被忽略。康威定律已经成为一种迷信咒语——人们只会在事后相信它。很少有工程师将他们架构上的成功归因于组织结构,但当产品出现问题时,康威定律的解释便很容易被接受。
康威的原始论文不仅阐述了组织结构如何影响技术,还指出了人类因素对技术发展的贡献。他的其他一些观察结果包括:
-
个人激励在设计选择中扮演了角色。人们会根据某个特定选择——例如使用一种崭新的工具或过程——如何塑造他们的未来,来做出设计决策。
-
小幅调整和返工是不光彩的,它们让组织和未来看起来不确定,突显出错误。为了挽回面子,重组和全面重写成为了更可取的解决方案,尽管这些方案更昂贵且往往效果不佳。
-
组织的规模影响其沟通结构的灵活性和容忍度。
-
当一个经理的威望由其下属人数和预算规模决定时,经理会受到激励去细分设计任务,这反过来会影响技术设计的效率——正如 Conway 所说:“当前许多设计不佳的系统背后最大的共同因素,就是有一个急需工作量的设计团队。”
Conway 的观察在维护现有系统时比在构建新系统时更为重要。组织和产品都会发生变化,但变化的速度不一定相同。弄清楚是改变组织结构还是改变技术设计,是另一个规模扩展的挑战。
个人激励
软件工程师如何获得晋升?一个工程师在某个级别上需要完成什么任务,才能被提升到更高的级别?这类问题通常被委派给工程经理的领域,而不会被纳入技术决策中。然而,答案显然会对技术产生影响。
我们大多数人都曾在实际中遇到过这种情况:一个服务、一个库,或是系统的一部分,与其连接的其他应用程序之间存在莫名其妙的差异。有时这是系统的一个较旧组件,使用一套不同的工具重新实现;有时是一个新功能。它总是当时流行的技术。
当一个组织没有为软件工程师提供明确的职业发展路径时,他们通常通过在外部建立声誉来推动自己的职业生涯。这意味着他们会参与到成为第一个证明新范式、语言或技术产品在生产规模下优势的竞争中。尽管工程团队在迭代过程中试验不同的方法是好的,但引入和支持新的工具、数据库、语言或基础设施会增加系统在维护过程中的复杂性。我曾为一个组织工作过,他们为缓存、路由和消息处理等领域定制了大量的解决方案。高层管理对此深感反感,但他们的抱怨——甚至是要求停止这种做法——似乎并未起到太大的纠正作用。在文化上,工程组织呈扁平化,团队是按需成立的。能够参与有趣的技术挑战的机会是根据个人关系来分配的,因此组织中的定期黑客日变成了关键的社交活动。工程师们希望构建复杂而困难的解决方案,以便向正在组建团队的首席工程师展示他们的技能。
对选择适合工作的技术的重要性进行严厉的讲解并没有停止这种行为。这种情况直到组织雇佣了开发职业晋升阶梯的工程经理才得到了遏制。通过明确每个经验级别工程师的期望,并聘请能够辅导并为工程师争取机会的管理者,工程师们可以在不需要炫耀的情况下获得晋升和机会。
组织最终会得到拼凑的解决方案,因为技术社区奖励探索者。作为第一个记录、实验或摧毁一项技术的先行者会增加个人的声望。通过采用新颖创新的东西来推动性能的边界,声望会更高。
软件工程师被激励放弃经过验证的方法,转而迎接新的前沿。如果任由软件工程师自行其是,他们会繁衍出各种工具,忽略功能重叠,只为那一个工具 X 在特定情况下比工具 Y 做得更好的特性。
集成良好、高效运作且易于理解的软件通常是低调的。简单的解决方案往往无法提升个人品牌,它们很少值得一谈。因此,当组织未能为软件工程师提供晋升路径时,他们会倾向于做出强调个人贡献而非与现有系统良好融合的技术决策。
通常,这种情况表现为三种不同的模式之一:
-
创建框架、工具和其他抽象层,以使那些不太可能有多个使用场景的代码在理论上“可重用”
-
将功能拆分成新的服务,特别是中间件
-
为了优化性能而引入新语言或工具(换句话说,没有任何提升 SLO 或现有基准的需求,仅仅为了优化性能)
本质上,工程师的动力来自于创造有名字的事物。如果某样东西能被命名,就能有一个创造者。如果这个命名的东西变得受欢迎,工程师的声望就会增加,她的职业生涯也会向前发展。
这并不是说好的软件工程师就应该永远避免在生产系统中拆分新服务、引入新工具或尝试新语言。只是需要有一个令人信服的理由,说明这些行动是有益于系统的,而不是有益于个人工程师的前景。
我所负责拯救的大多数系统并不是构建得很糟糕,而是维护得很糟糕。突出个人独特贡献的技术决策并不总是能被团队其他成员理解。例如,从语言 X 切换到语言 Z 可能确实显著提升了内存性能,但如果团队中的其他人对新语言不够了解,无法继续开发代码,那么随着时间推移,由于没人知道如何修复的技术债务,所获得的收益将逐渐被消耗掉。
工程文化的愚蠢之处在于,我们常常羞于通过选择当前合适的架构来为组织签下未来重写的任务,但对于生产那些让别人难以理解,因此无法维护的系统却没有任何顾虑。对于像美国数字服务和 18F 这样的组织来说,这一直是软件工程师在响应公共服务号召时的一个常见问题。在现代化关键政府系统时,团队应该在什么时候使用常见的私营部门工具并训练政府人员使用这些工具,什么时候应该使用政府工作人员已经掌握的工具来构建解决方案?难道最新最强大的 Web 应用栈不是最好的选择吗?Conway 反对追求普适正确的架构。他在 1968 年写道:“经验丰富的系统设计师普遍信仰的一个观点是,给定任何系统设计,总有一天有人会找到一种更好的设计来完成同样的工作。换句话说,除非在空间、时间、知识和技术的背景下理解,否则谈论某个特定工作的设计是具有误导性和不准确的。”
微调作为不确定性
Joel Spolsky 曾经形容重写软件是任何组织可能犯下的最糟糕的战略错误,但他将其几乎普遍的吸引力归因于一句巧妙的格言——代码写起来比读起来容易。^(9)
这也是真的;代码写起来比读起来容易。几乎每个软件工程师都有过翻看旧项目时,发现自己曾写的代码几乎无法理解的经历。
但这并不能解释为什么我们会在基础设施、数据存储和其他不涉及写代码的产品中看到相同的行为。
影响系统随时间退化的一个主要因素是人类在概率方面的弱点。我们往往高估已经发生过的事件再次发生的可能性,而低估尚未发生事件的可能性。人因学和系统安全学教授 Sidney Dekker 称这一认知问题对系统安全的影响为漂移。(^(10))系统通常不会一下子完全失败;它们通过反馈回路逐渐“漂移”到失败,原因是想要防止失败。假设一名工人被给了一套检查清单,包含维持系统正常运行所需的步骤。如果她漏掉了一步且系统没有立即失败,她对风险的感知发生了变化。跳过这一步变得不再那么重要,看似不太可能导致失败。她越是跳过这一步,就越确信自己的行为是安全的。她忽视了自己可能只是运气好而已。她越是走捷径,系统就越容易发生故障。
与此同时,如果系统因她检查清单中没有涵盖的原因出现故障,她会高估此类故障再次发生的概率。系统可能因为存在重大缺陷而失败,或者可能因为一系列不太可能重现的随机事件而失败。她能否做出恰当反应取决于她是否能正确评估刚刚发生事件的概率。如果她高估了概率,她会为检查清单添加新步骤,以确保不太可能发生的故障不再重现。随着时间的推移,检查清单变得越来越繁琐,增加了她或她的同事跳过某些步骤的可能性。
我们喜欢从头开始重写的系统通常是我们一直忽视的系统。我们无法知道失败的可能性有多大,因为我们只有在系统故障时才会关注它们,其他时候则将它们忘记。一个遗留系统上有一百个错误,并不意味着它容易失败,尤其是当它在这段时间内处理了两百万个请求时。看待遗留系统时,我们往往会过度强调故障。
我们喜欢从头开始重写的系统通常也很复杂,具有许多抽象层次和集成。当我们对其进行更改时,事情并不总是顺利进行,特别是当我们在测试覆盖方面出现疏漏时。我们在进行更改时遇到的问题越多,我们对未来故障的高估就越严重。系统看起来越脆弱、易失败,并且似乎无法挽救,完全重写就越像是一个更容易的解决方案。
我们对风险的感知会触发另一种认知偏差,使得重写比对一个运行良好的系统进行增量改进更具吸引力:我们是在努力确保成功还是避免失败。当成功似乎是确定的时,我们倾向于选择更保守、规避风险的解决方案。而当失败看起来更可能时,我们会完全转变心态,变得更加大胆,承担更多的风险。^(11)
如果我们正确地评估了概率,这种行为是有道理的。既然现有系统注定会失败,为什么不批准那个数百万美元的重写呢?
问题是,我们很可能并没有正确判断概率。我们过度强调了可能稀有的失败,低估了重写所需的时间和重写本身带来的性能提升。我们正在用一个仍在运作并且只需调整的系统,换成一个昂贵且复杂、没有经过验证的迁移方案。
系统的细微调整,尤其是那些一段时间没有积极开发的系统,会让人产生失败不可避免的印象,并推动原本理性的工程师做出重写的决定,而重写在某些情况下并非必要。
组织规模与沟通
每一个职场人士都体验过组织规模如何影响沟通模式。当组织较小时,沟通通常是开放和流动的,每个人都可以与其他人建立关系。随着组织的扩大,认识所有其他人变得越来越不可行。协调需要信任。在有选择的情况下,我们更倾向于基于我们熟悉的人的品格来建立信任,但当组织规模扩展到无法做到这一点时,我们逐渐用流程取代了社会联系。通常这发生在组织规模达到 100 到 150 人左右时。
例如,微服务的一个好处是,它允许多个团队独立地贡献于同一个系统。而单体架构则需要以代码审查的形式进行协调——即同事之间的个人、直接互动——而面向服务的架构通过流程扩展了相同的保证。工程师记录合同和协议;自动化被应用以确保这些合同不被违反,并且当违约时,系统会指示行动方案。
因此,想要“提前”从一开始就使用微服务构建的工程师往往会遇到困难。复杂性和抽象层次与组织的沟通模式不匹配。
经理的激励
工程经理在技术型组织中是一个奇特的存在。我们该如何评判一个好的经理和一个坏的经理呢?不幸的是,经理们往往通过管理更多的人来晋升。如果组织没有对这一点进行适当的控制,系统设计将会因需要传达重要性而变得过于复杂。
随着组织的成长和变化,偶尔会出现从工程经理晋升为高级工程经理的机会。这是处理一个团队和处理多个团队之间的区别。经理离职,新团队成立,现有团队的规模也超出了理想的范围。一个优秀的经理可以轻松地在正常的工作过程中获得这些机会。然而,从高级经理晋升为总监则更具挑战。再从总监晋升为副总裁或更高职位则更加困难。一个组织要通过自然成长达到这一层级需要很长时间。
那些没有为人才成长做好准备的组织,最终会出现一些经理,他们有动机在没有足够人员或足够工作来维持这样一个小单位之前,就将团队划分为更多的专门化单元。这些经理可以勾选自己在管理多个团队、招聘更多工程师、承担更具雄心的项目等方面的职业发展经历,而整体架构的需求却被忽视了。
在没有真正需要扩展的情况下扩展组织,和在技术尚未需要扩展时就扩展技术,产生的后果是相似的。这会限制你未来的技术选择。复杂的架构意味着组织必须成功地预见到许多未来的需求,并确定如何根据这些预测最佳地抽象代码,以创建共享服务。这些预测很少能完全正确,但一旦共享服务部署上线,改变它就变得非常困难。
同样,经理有时会在没有必要时就将团队细分。当这种情况发生时,他们实际上是在预测未来可能出现的需求,而这些需求可能会成真,也可能不会。在我上一个职位上,我们的工程总监决定我们正在构建的新平台需要一个专门的团队来管理数据存储。关于未来扩展挑战的预测支持了她的结论,但为了获得这个新团队的人员配备,她不得不从已经在处理组织现有扩展挑战的团队中裁减人员。突然间,我们开始开发一些围绕数据存储的新抽象,而这些抽象实际上我们并不需要,而影响我们服务水平协议(SLA)的系统则被推迟了维护和更新。
完成现有的项目并不如开辟新天地那样具有吸引力。但围绕技术的理想未来状态来设计团队结构的问题是,如果这种理想状态没有实现,团队就会被推入重组的混乱之中。
设计团队:康威定律的应用
积极而有效地应用康威定律的挑战在于,技术项目中的工作分配可能会根据正在解决的技术难题而发生变化。
假设我们有一个组织,正在构建一个由三个 Web 服务组成的系统。每个服务都有自己的代码库、机器镜像和部署计划。每个服务都有三层结构:应用层、数据访问层和前端。在开始时,前端和应用是逻辑上分开的,但为了方便,它们托管在同一个代码库中。前端只是 HTML 文件和一些 CSS、JavaScript 文件。
我们的工程团队可能反映了这种结构。对于每个服务,我们都有一名前端人员和一些后端人员。我们希望这些服务的外观和感觉保持一致,因为它们是一个系统,因此我们有一个与三个开发团队分开的设计团队,但它会产生所有团队共享的样式指南和资源。也许我们为每个工程团队指定设计团队的具体联系人。我们对运维和安全团队也做同样的事。他们的工作是跨部门的,并且适用于所有团队,我们希望保持一致的实施。我们不希望每个工程孤岛都重复造轮子。
现在假设我们想开始使用像 React、Angular 或 Vue.js 这样的前端框架。我们仍然希望每个服务有相同的外观和感觉,但我们也希望减少重复的工作。它们应该重用 UI 组件。谁来编写这些代码?这些代码存放在哪里?我们是把前端工程师从产品工程团队中调出来,放到像设计师、安全工程师和运维人员这样的独立小组,还是将他们留在原地并建立矩阵式分工来处理共享的开发工作?
将康威定律视为规定性原则的问题在于,技术中充满了像这样的感知小变化。我们例子中的技术并没有从根本上发生变化,但我们对“什么属于什么”的分组发生了变化。我们也可以反过来讲这个故事:如果我们想从传统的运维团队过渡到 DevOps 模型怎么办?我们的运维人员是否会被调动到产品工程团队?后端工程师是否会学习 DevOps 工具,而运维则充当监督职能?我们是否将运维保持在原地,仅要求他们进行自动化?
组织重组是创伤性的
组织重组是全面重写的常见误用工具。正如软件工程师倾向于推翻一切重新开始,以展示信心和确定性一样,软件工程师的经理也倾向于通过重组来解决所有形式的制度性问题。
就像全面重组一样,这有时确实是合适的策略,但它并不像常用时那样是最合适的策略。重组极具破坏性,它令人士气低落。它传递给普通工程师的信息是,某些事情出了问题——他们做错了什么,或者他们做的产品无法正常工作,或者公司正面临困境。这会增加工作场所的焦虑感,并降低生产力。重组几乎总是以一些“被淘汰”的人最终被解雇作为结局,这种情况加剧了问题。
重组也容易弄错,导致重新建立信息孤岛,而这些地方曾经是信息自由流动的。组织几乎总是稍微滞后于捕捉和记录正在进行的事情。重组使正在进行的项目“孤立无援”,尤其是那些专注于长期维护的项目,导致信息丢失和后续工作被搁置。
我把重组看作是重大手术。如果有什么问题非常严重,冒险一试是值得的,但你不会相信一个医生因为肾脏稍微偏右就想为你开刀。同样,你也不应该雇佣那些因为看了一篇博客文章,说工程团队在某种结构下运作更好的人,而去做重组。
有时一个组织的成长并不按部就班,结果是团队最终拥有一些不相配的东西,或者共同拥有本应只有一个负责人管理的事务。这些情况是重组合适的场合。
康威定律是一种趋势,而不是一种命令。大型复杂组织能够开发出灵活且具有韧性的沟通渠道;这只需要合适的领导力和合适的工具。只有在组织结构完全与其执行方式不匹配时,才应该进行重组。
找到合适的领导力
现代化项目归根结底是关于转型的。你正在调配资源、调整流程并重新构想实施方式。开始时合适的团队,最终未必仍然适合。
要找到合适的领导力,寻找那些在各种不同环境中都取得过成功的人——无论是旧有系统、新系统、大型官僚机构,还是小型初创公司。不要抱有幻想去招聘。不要只招聘那些在与你希望达成的最终状态相符的公司工作过的人。不要根据你希望组织具备的特质来招聘。这是一个相当常见的错误。想要扩展的组织往往从大公司中招募高管,想要迁移到云端的组织则从管理云产品的高管中招聘。
过渡本质上是模糊的,任何进入过渡阶段的领导者最重要的特质就是适应模糊带来的变化的能力。你可以在面试中评估这些技能,但最好的指标通常是候选人的职业发展轨迹。善于适应的候选人在简历上会有不同行业和规模的经验。他们可能曾做过非营利或政府工作。他们可能尝试过不同的职业或角色。他们可能曾暂时离开工作世界,之后成功回归。
在同一种类型的组织中工作了七八年的候选人,可能会带来很多有价值的经验,但他们也可能过于依赖某一种做事方式。他们可能不理解为什么某些方法在这种情况下有效,而在那种情况下却无效。他们可能具有官僚主义倾向,风险厌恶,并且不愿意迎接不同环境下的挑战。
过渡是关于变化的,但确定什么应该变化和何时应该变化是重要的问题。我们不是一蹴而就地走到今天的地方。为什么我们应该以这种方式去到其他地方呢?能够适应模糊不清的领导者更有可能弄清楚从起点到最终状态之间的所有中间阶段。
练习:最小可测试单元
我开发这个练习是为了规划故障演练(一些软件工程师称之为混乱实验)。最后,我将它重新用于面试问题,以评估候选人设计过渡路线图的能力。
我们从一个想要实现的大目标开始。例如,假设我们有一个 web 应用,其中的秘密存储在一个明文配置文件中。三十年前,这种方式可能是构建应用程序的正确方式,但现在它的安全性不足。许多解决方案都可以提高安全性,但组织可能无法使用所有这些解决方案。这是遗留现代化中的典型问题:理想的解决方案依赖于不具备或无法实现的条件。领导者必须决定是否妥协使用其他解决方案,或者投入时间和精力解决首选解决方案的依赖问题。
你可能熟悉“剃 yak”这个表达。它指的是每个问题都有另一个必须先解决的问题才能处理的情况。某种程度上,最小可测试单元的练习就是一个剃 yak 的练习。你通过问“我们需要什么来做到这一点?我们如何测试它是否具备?”来推进每个阶段。对于前面的例子,路线图可能长这样:
-
我们需要将机密信息转移到一个安全的秘密管理解决方案中。为此,我们需要知道我们有多少个秘密,它们在代码中的位置,以及谁或什么需要使用它们。
-
我们可以通过仔细记录访问现有机密的人来弄清楚谁需要使用我们的当前机密。为此,我们需要一种聚合日志并搜索它们的方法。我们应该小心,不要记录实际的机密,只记录请求机密的日志。
-
我们可以通过让应用程序的不同部分将独特的消息写入日志,并检查这些消息最终会出现在哪里来测试我们是否有能力聚合和搜索日志。为此,我们需要访问应用程序的源代码。
-
我们可以通过找到代码库、阅读代码并尝试提交更改来测试我们是否能访问源代码。为此,我们需要某种版本控制解决方案。
诸如此类,等等。
如果做得好,候选人会从最终状态开始,逆向规划路线图,识别出越来越小的变革单元。每一步,我们都在设计测试,找出我们可以解决的组织运营卓越中的弱点。重要的是,我们的路线图应围绕通过简单测试证明我们已经拥有某些东西,而不是断言我们已经具备某些东西的步骤来构建。在大型项目中,人们容易感到困惑或错误报告真实情况。了解领导者如何构建测试以验证信息是非常有用的。
一位对模糊性容忍度低的领导者要么看不见这些障碍,要么不承认它们,因此她会下达一个自上而下的指令,要求实施新解决方案。工程团队可能会迅速做出修补或变通方案来处理这些障碍,或者直接忽视这一自上而下的指令,导致改进遗留系统的努力停滞不前。
组织团队以应对过去的失败
遗留系统的现代化从来都不是关于一个团队或一个领导者的。遗留系统之所以能够存活,是因为它们很重要;重要的系统往往会带来相应的流程,组织也会围绕这些流程发展。即使你选择专门成立一个团队来进行现代化改造,该团队的工作也会依赖于并影响其他团队。
现代化的三种有效结构如下:
复制现有组件的团队。如果过去有失败的短暂历史,你可能能够依赖当前的劳动分工来推动进展。团队由现有团队的全部或部分组成,因此它们之间的协调以跨职能会议小组的形式进行,该小组由每个现有组件的负责人或由组件指定的代表组成。与任何其他结构相比,这种选项高度依赖于人际关系。如果组织中已经开始形成小圈子和敌对关系,保持团队的专注将变得困难。
领导团队和子小组。在这种模式下,领导团队制定现代化努力的高层次视图,然后将任务分配给各子小组,子小组有权做出关于如何执行这些指令的所有细节决策。一个现代化项目的失败记录越多,我就越倾向于将我们的努力与常规业务区分开来。这意味着该结构可以采取架构组为业务部门提供建议的形式(我们可能已经设置了这个),或者我们可以暂时将人员从其正常团队中调出。最好避免将相同的人分配到相同的角色,通常如果角色调动是出于良好意图且目标明确,你会看到动力立即得到提升。
正如我在第六章提到的,想要更有效地完成某事,最直接的方式就是改变人们的环境。有关这种结构如何工作的更多信息,请参考第 116 页的“解决方案:黄色警戒”。
一个嵌入式团队。当失败的历史较长时,有时最佳的选择是将人员嵌入到现有团队中,专门负责实施解决方案。在这种模式下,一个团队决定计划,然后将其成员分派到组织中不同的部门,负责解决方案的实施。做到这一点的关键是身份认同。嵌入式团队的成员之间必须有强烈的同袍情谊。他们必须感受到自己是一个团队。他们应该以同情和共情的态度对待主办团队,但同时也应该将主办团队视为客户或顾客,而不是同行。
这与从每个团队中抽调代表组成联合委员会不同。在委员会解决方案中,个人与她所在的团队有联系,而对委员会上的同事没有特别的依赖。在嵌入式团队中,动态应该是相反的。
实施这三种结构本身就是一项练习,旨在帮助有机地找出组织在新系统完成后如何围绕它进行自我组织。康威定律最终与沟通和激励有关。激励方面可以通过为人们提供一条与现代化工作相辅相成的荣誉和职业发展道路来实现。设计沟通路径的唯一方法实际上是给人们一些值得沟通的内容。在每种情况下,我们通过设计能够鼓励新沟通路径形成的结构,来让新组织的愿景逐渐显现出来,以应对我们的现代化挑战。随着工作的继续,这些沟通路径开始逐渐固化,我们可以开始文档化,并正式确定新的团队或角色。通过这种方式,我们避免了重组的焦虑。工人们根据他们如何适应问题来决定自己的位置;那些通常被忽视的工人会得到时间和空间来学习新技能或证明自己可以担任不同角色,而在新组织结构由领导层批准之前,每个人已经在这种方式下工作了几个月。
根据你认为为了使新系统可维护所需的组织变革程度,选择你的现代化团队结构。
保持团队不变的假设是新系统的抽象将与旧系统相匹配。不会有新的职责,也不会有新的角色。从旧系统到新系统唯一的变化是像语言或工具选择这样的实现细节。许多迁移将看起来像这样。
拥有一个由子团队组成的领导团队意味着将会有一些跨越性的问题,现有的团队无法解决这些问题,或者没有足够的信息来解决这些问题。在新系统完成时,可能会围绕这些问题发展出新的团队。例如,组织可能会意识到需要开发新的服务,或者为了在整个工程组织中推行良好的实践,他们需要内部工具。在这种结构下,我们知道工程团队的某些部分将保持不变,而某些部分将发生变化,但我们并不清楚具体会怎么变。
最后,嵌入式团队为将专业知识根据需要注入到其他团队中树立了先例。当新系统的目标状态与旧系统有显著不同时,我会使用这种结构。当发生如此大的变化时,通常会引入对现有工程师来说完全陌生的技术和实践。从主机转移到云端、关闭数据中心并转向云端、推出 SRE 或引入编排,都是现代化挑战的例子,这些挑战通常会导致现有团队出现技能差距。被注入的专家将开始通过弄清楚旧团队需要做的工作如何分配,来推动新团队的形成。例如,如果现代化工作涉及一项新技术,并非每个团队成员都需要达到同样的熟练度。与其让高级经理决定谁去哪里,组织应该让现有团队自己去做,并看看谁能发展出相应的能力。
你不想做的事情是根据你对新系统中团队如何安排的愿景绘制一张新的组织结构图。你不想这么做的原因和你不想在产品开发初期就事先设计好一切的原因一样。你对新系统外观的构想在某些细微的方面是错误的,而这些错误是你无法预见的。你不想把你的团队锁定在一个无法满足他们需求的结构中。
相反,你应该问自己,哪些人需要在现代化项目的不同阶段之间进行合作,并选择一个使这种沟通变得容易的结构。
练习:组内/组外
在你开始时,谁需要与谁沟通可能并不明确。这是我用来帮助揭示沟通路径在哪里或应该在哪里的一个练习。我给每个人发一张纸,上面画着一个圆圈。说明是把他们依赖的人的名字写在圆圈里(换句话说,“如果这个人落后了,你会被阻塞吗?”),把给他们建议的人的名字写在圆圈外。如果没有特定的人,他们可以写下一个小组或团队的名字,或者是一个特定角色,比如前端工程师。
然后我会对每个团队的结果进行比较。从理论上讲,圈内的人是工程师需要密切合作的对象。每个结果应该与该工程师的实际团队相似,也许会有一些基于当前问题的增减。圈外应该是所有其他团队。团队外的专家应该被视为与同一领域的其他专家可以互换的。
每个人的差异会存在一些小变化,但如果人们所画的图像看起来和他们当前的团队不一样,那么你就知道你现有的结构无法满足你的沟通需求。
你可以修改这个练习,聚焦于未来工作流的研究问题,而非现有系统的沟通需求。不要仅仅问哪些人可能是阻碍者或顾问,而是要求人们根据特定的现代化任务,想象内群体和外群体的划分。
要点
本章涵盖了很多内容。设计思维是一个丰富的领域,包含了许多对遗留系统现代化任务有价值的洞察和策略。我尽力展示了足够的价值,以鼓励你在没有设计师的情况下,邀请设计师加入你的团队。回顾一下,这里是你应该从本章得到的要点:
-
设计是问题设定。将其融入到你的过程中将有助于你的团队变得更加韧性。
-
单纯的技术对话往往会激励人们通过批评想法来维持现状。设计可以通过赋予对话一种游戏的结构和通向胜利的路径来帮助缓解这些效应。
-
遗留系统现代化最终是一个过渡过程,需要有较高容忍模糊性的领导者。
-
康威定律并不意味着你应该将组织设计成你想要的技术样貌。它意味着你应该注意组织结构如何激励人们的行为。这些力量将决定技术的样子。
-
不要设计组织结构;让组织通过选择一种能够促进团队沟通的结构来自我设计,以便完成工作。
在下一章,我将继续探索沟通的概念,解决破坏性变更的问题,以及如何防止这些变更阻碍前进的步伐。
第八章:重大变更
在政府工作时,我们有一句话:“政府最讨厌的,不是改变,而是现状。”同样的惰性也存在于遗留系统中。没有破坏就无法改善一个庞大、复杂、负债累累的系统。如果幸运的话,导致的停机问题将很快得到解决,并且数据丢失最小,但这些问题是不可避免的。
另一种在我的政府同事中流行的表达是“空中掩护”。拥有空中掩护意味着你有信心,组织会帮助你的团队应对这种不可避免的破坏。它意味着你有一个相信并理解变革价值并能保护团队的人。作为团队领导,我的工作就是确保这种空中掩护。当我回到私营部门时,我作为经理应用了相同的原则——建立网络、建立关系、招募、做些小恩惠——以便我能给我的团队成员提供做艰难工作的安全感和保障,而这些正是我雇佣他们的原因。
在本章中,我探讨了重大变更的概念。你如何在诚实地阐述风险的同时推销危险的变更?何时应该破坏某些东西,如何快速恢复?
但是,我想从空中掩护的概念谈起。商业写作者有时会提到“心理安全”,这也是描述同一概念的一个很好的方式。为了有效地工作,人们需要感到安全和支持。领导的支持是创造空中掩护感觉的一部分,但为了使空中掩护有效,它必须改变组织对风险的感知。
风险不是表格中的一个静态数字。它是一种可以被操控的感觉,虽然我们可能会用统计数据、概率和事实来为这种感觉辩解,但我们对风险程度的感知往往与这些数据点没有关系。
被看见
一两年前,我受邀在哈佛大学甘尼迪政府学院给软件工程师工作讲座。当我开始给出实际建议时,我的第一张幻灯片上用大字写着:“人们怎么才能被看到?”
被看见并不单单是关于赞扬。它更多的是关于被注意或被认可,即使这种认可的情感是中立的。正如寻求地位的行为影响人们在会议中说什么,寻求被看见也会影响人们愿意容忍的风险程度。对变化的恐惧完全源自于对风险的感知。人们根据两个维度来构建风险评估:惩罚或奖励的程度以及被抓到的概率。
在这两者中,人们对被抓到的概率变化比对惩罚或奖励的程度更为敏感。^(1) 如果你想威慑犯罪,增加警察有效性的认知,并且让犯罪分子觉得自己会被抓住。如果你想激励某种行为,关注在组织中被注意到的行为。
组织可能口头上大力宣扬良好的行为,但仍然忽视它们。被看见并不是与组织的理论理想相匹配,而是关于你的同事会注意到什么。组织的言辞很容易与实际工作环境中的价值观脱节。同事们关注的是组织的真正价值观。无论组织的宣传有多么热情或一致,来自同事的关注总是比演讲更具影响力。
认同的具体形式也非常重要。以社会认可的形式进行的积极强化,往往比传统的晋升、加薪和奖金激励结构更有效。行为经济学家丹·艾瑞里将这一现象归因于社会市场和传统货币市场之间的差异。^(2) 社会市场由社会规范(即同伴压力和社会资本)所支配,它们往往能激励人们比那些更昂贵的传统工资换取工作激励更加努力和长时间地工作。换句话说,人们会为积极的强化而努力工作;但他们可能不会为多赚一千美元而更加努力。
艾瑞里的研究表明,即使是通过提供小额财务激励来促使人们更加努力工作,也会导致人们停止考虑与同事之间的联系,而转而将其视为一种货币交换^(3)——这是一种更冷漠、较少个人化的、通常也较少情感回报的空间。
认为一个人需要财务奖励才能为一个组织做好工作,这种想法是愤世嫉俗的。它假设员工有不良的动机,这会引发怨恨。因此,传统的激励措施几乎没有积极影响,因为它们破坏了原本基于信任和尊重的个人关系。行为学家阿尔菲·科恩是这样说的:
惩罚和奖励是同一枚硬币的两面。奖励具有惩罚效应,因为它们和直接的惩罚一样,都是操控性的。“做这个你就能得到那个”其实和“做这个,否则你会面临这样的后果”并没有太大区别。在激励的情况下,奖励本身可能非常渴望;但通过将奖金与某些行为挂钩,管理者在操控下属,而这种被控制的经历随着时间推移可能会变得具有惩罚性质。^(4)
这是一个实际的例子。当我在 USDS 工作时,我的老板经常抱怨人们一次又一次地做出他明确告诉他们不应该做的事情。具体来说,他不断告诉团队不要把系统从那些拥有它们的组织手中夺走。USDS 的运作方式,至少理论上,是基于咨询模式的。我们应该协助并为各个机构提供建议,而不是长期接管他们的遗留系统,且没有任何退出计划。我的老板一次又一次地抱怨这种做法。他无法理解为什么人们总是倾向于选择一种更为困难、成功可能性更低且违背他意愿的策略。
每周,我们都会召开一次员工会议,大家展示自己正在做的工作并汇报进展。不可避免地,USDS 的成员们会对他们的更新内容进行审查,只有在取得成功后才愿意谈论。这意味着产品发布。我们在员工会议上讨论的全是产品发布的事情。最终,这种惯例变得自我强化。人们开始认为,在项目没有发布计划或没有达到某个里程碑之前,不应该谈论,认为那些小小的成就不值得一提。
问题是,大多数 USDS 项目涉及的是老旧的系统,解决方案可能需要几个月的时间来梳理,甚至不考虑政府官僚体制的因素。仅仅谈论产品发布意味着某些团队可能会花上一整年时间在某个项目上,直到他们的同事才听说到这个项目。
我老板关于“不把系统从拥有它的组织中拿走”的建议,对于长期的可持续性来说是很好的建议,但它意味着当同事们讨论他们的工作时,你将没有什么可贡献的,因为要等到产品发布可能需要几个月。那么,加速这一过程的最佳方式是什么?接管系统,绕过或以其他方式规避拥有该系统的政府客户,带上一支聪明的年轻 USDS 团队来完成所有工作。这虽然能让产品更快发布,但将它们交给政府利益相关者几乎变得不可能。他们对新系统一无所知。这正是我的老板试图避免的,但人们忽视了他的建议,优先考虑那些能够让自己早早被同事看到的方法。
当我们意识到这一点时,我们决定在每次员工会议的结束时安排一个 10 分钟的时间块来进行“表扬”。表扬是对整个组织中小小成就的认可和祝贺。会议进行得顺利吗?写个表扬。有人超越了职责范围去解决问题吗?写个表扬。团队在项目失败中展现出诚信和决心吗?写个表扬。我们会在一周内将所有表扬收集到一个特定的仓库里,然后在员工会议的结束时,由某人将它们全部朗读出来。
在金钱奖励和社会奖励之间做选择时,人们几乎总是选择能获得社会提升的行为。因此,当你在审视为何某些失败被视为比其他失败更具风险时,一个重要的问题是:人们在这里是如何被看到的?哪些行为和成就能让他们有机会与同事谈论他们的想法和工作,并得到认可?
如果你想提高人们对某些类型风险的容忍度,可以改变组织在奖励和认可这两个关键维度上的定位。你有四个选择:增加好行为被注意到的机会(尤其是同事之间),减少坏结果被注意到的机会,增加好行为的奖励,或者减少坏结果的惩罚。所有这些都会改变组织对风险的认知,并使得进行突破性变革变得更容易。
注意良好行为和坏结果之间的区别。当员工决定如何执行一组任务时,他们会考虑两个问题:组织希望我如何表现?如果尽管表现正确,事情仍然出了问题,我会被惩罚吗?如果你希望人们在风险面前仍然做正确的事情,就必须接受失败的存在。
其中一个最著名的例子是 Etsy 的“三臂毛衣奖”。^(5) 三臂毛衣的形象在 Etsy 中被用来象征犯错。例如,它在 Etsy 的 404 页面上非常显眼。三臂毛衣奖颁发给触发最严重故障的工程师。庆祝失败不仅仅是每年的传统;Etsy 员工还有一个邮件列表,向全公司广播失败故事。^(6) 因为 Etsy 想要建立一个公正文化,在这个文化中,人们是一起从错误中学习,而不是试图掩盖它们,公司找到了将这些行为的认可融入日常运营的方式。这些做法帮助 Etsy 将技术规模扩大到每月 4000 万独立访客。^(7)
如果你希望你的团队能够处理破坏性问题,就要注意组织庆祝什么。无责事后分析和公正文化是一个很好的起点,因为它们都影响人们对失败的看法,并建立良好的工程实践。
谁来划定界限?
但是,无责事后分析真的完全能没有责任吗?
2008 年,系统安全研究员 Sidney Dekker 发表了一篇名为《Just Culture: Who Gets to Draw the Line?》的文章^(8)。Dekker 的文章讨论了“无过错”事后分析是否为公正文化的理想状态,在这种分析中,任何人都不会因错误而受到惩罚。人们想要心理安全,但他们也希望有问责制。没有人愿意宽恕真正的疏忽,但如果在应该无过错的错误和应该追究责任的错误之间有一条界限,谁有权划定这条界限呢?
一项流行的计算机科学一年级学生的练习是编写一个假设程序,指令机器人穿越房间。学生很快会发现,最简单的指令在字面意思上理解时可能会导致意外的结果,练习的目的是教会他们一些关于算法的知识,以及计算机可以和不能做出什么假设。
像 Dekker 这样的安全研究人员也基本上以相同的方式看待组织程序。规定的安全、保安和可靠性过程只有在操作员能够在应用时行使自由裁量权时才有用。当组织剥夺了负责系统的软件工程师的适应能力时,任何程序所覆盖的空白就会变成致命弱点。
这就是为什么谁有权划定界限对公正文化如此重要的原因。界限划定模式离维护系统的人员越近,韧性就越强;离得越远,越容易变得官僚和功能失调。
同时,界限从来不是一次性划定的;它是主动重新谈判的。没有哪位规则制定者能预测到组织及其技术可能面临的每一种可设想的情况或环境。因此,关于无过错行为和应该追究责任的行为之间的界限会被重新划定。这些修正可能会受到文化、社会或政治力量的影响。
就像理解人们如何被看待对构建能调节人们对风险认知的激励机制很重要一样,理解谁有权划定可以接受的错误与不可接受的错误之间的界限,对于理解特权在一个组织中的分配也同样重要。成功的最高概率来自于尽可能多的人参与其中并获得执行的授权。那些无法划定界限或重新谈判界限位置的人,是组织中最缺乏特权、最需要投资以充分发挥其努力效益的成员。
通过失败建立信任
提议接受失败或现代化团队故意破坏某些东西会让人感到不安。假设失败是一种损失——失败总是让你变得更糟。
真的如此吗?
科学描绘了一个更复杂的图景。虽然一个经常出现故障的系统,或者一个以无法预见的方式突然故障的系统,会失去用户的信任,但反之未必成立。一个从不发生故障的系统并不一定能激发高度的信任。
意大利研究人员克里斯蒂亚诺·卡斯特尔弗朗基和里诺·法尔科内提出了一个普遍的信任模型,在该模型中,信任会随着时间的推移而降低,无论是否有任何行为违背了这种信任^(9)。人们往往对那些过于可靠的系统视而不见。在卡斯特尔弗朗基和法尔科内的模型中,保持信任并不意味着建立一个完美的记录;而是意味着不断积累韧性的观察。如果某项技术如此可靠,以至于被完全遗忘,那么它就没有创造出这些常规的观察。虽然技术本身没有错,但用户对它的信任会慢慢降低。
在我们这些从事遗留系统现代化工作的人看来,这种情况时有发生。组织变得急于去除那些已经稳定高效运作了数十年的系统,因为它们“老旧”,因此管理层坚信系统崩溃即将发生。
我们也在更现代的系统中看到了这种情况。例如,谷歌反复宣传这样的观点:当服务的性能超出其 SLO 时,团队会被鼓励制造故障,以降低性能水平^(10)。这样做的理由是,运行完美的系统会带来一种虚假的安全感,从而让其他工程团队停止建立适当的故障安全机制。这可能是真的,但换个角度来看,服务表现得越好,谷歌的 SRE 团队对整体系统稳定性的信心就越低。
认为某事更可能出错,仅仅是因为在长时间没有出错之后,发生了错误,这是一种赌徒谬误的表现。系统更可能失败的原因不是因为缺乏故障,而是因为在维护计划上的疏忽、未能适当测试或其他偷工减料。一个过于可靠的系统是否有危险,这个假设是否合理,取决于人们用来确定故障几率的证据。
赌徒谬误是那些普遍存在的逻辑谬误之一,它以各种奇怪的方式出现。例如,在 1796 年,一位法国哲学家记录了当地父亲们在看到其他妇女生下儿子时所感到的焦虑和绝望,因为他们坚信这样会降低他们在同一时期生下儿子的几率^(11)。
正因为如此,偶尔的系统故障和问题——特别是当问题迅速且干净利落地解决时——实际上可以增强用户的信任和信心。这个效应的技术术语是服务恢复悖论^(12)。
研究人员尚未能够确切确定服务恢复悖论的本质——为什么在某些情况下会发生,而在其他情况下却不会——因此,你不应该过度优化客户满意度,例如通过触发故障来实现。话虽如此,我们知道的是,快速恢复并且透明地说明故障的性质和解决方案通常能改善与利益相关者的关系。考虑到“公正文化”带来的心理安全和生产力提升,技术组织不应回避故意破坏系统。在合适的条件下,用户信任的成本极小,而收益可能是巨大的。
破坏性变更与破坏
在我深入探讨故意破坏系统的具体细节之前,我应该承认,我使用“破坏性变更”这个词来指代所有破坏系统的变更。破坏性变更通常并不是这么广泛的定义。
通常,当我们说“破坏性变更”时,是指违反数据契约并影响外部用户的情况。破坏性变更是指需要客户或用户升级或修改他们的系统以保持一切正常运行的变更。这是破坏了其他组织拥有的技术的变更。
为了方便起见,我在这里使用“破坏性变更”这一表达,指代任何形式的变更——无论是内部还是外部——都会对系统功能产生负面影响。本章的其余部分讨论了我们在废弃和重设计过程中所做的变更,这些变更破坏了外部 API 的联系,也讨论了我们在试图降低系统整体复杂性时所做的变更。
为什么要故意破坏系统?
任何重大的遗留系统现代化项目都不太可能在没有至少一次破坏系统的情况下完成。然而,作为其他变更的意外后果而破坏系统,知道存在破坏风险并且故意破坏系统是两种不同的情况。我主张,你的组织不应该仅仅选择接受弹性而避开风险,还应该偶尔故意破坏系统。
什么样的场景可以证明故意破坏系统是合理的?在处理遗留系统时,最常见的情形是失去组织记忆。在任何旧系统中,往往有一两个组件,似乎没人确切知道它们的作用。如果你想要最小化系统的复杂性并恢复上下文,这些知识缺口是不能忽视的。需要注意的是,无法通过查看日志或翻阅旧文档来理解一个组件的作用的情况虽然少见,但确实会发生。只要系统不控制核武器,关闭这个组件并观察哪些功能失效,是一种在所有其他方法无效时应当具备的工具。
系统中有一部分没人理解是一种弱点,因此出于害怕破坏系统而回避这个问题并不应被视为更安全的选择。将故障作为工具,增强系统及其运行的组织的强度,是弹性工程的基础概念之一。了解系统的每个部分如何在不同条件下运行,包括各部分之间的相互作用,非常重要。不幸的是,没有一个人能把所有这些信息都记在脑海里。有关系统的知识必须在技术组织的不同运营单位之间定期共享。一个组织需要有流程来暴露相关细节和场景,进行沟通,并判断其重要性。这就是故意破坏系统的第二个理由:验证一个组织对其系统的理解是否真实。弹性工程测试——也称为故障演练——旨在有策略地触发故障,以便记录和验证系统的真实行为。
最简单且威胁性最小的故障演练就是从备份恢复。记住,如果一个组织从未进行过备份恢复,那么它就没有有效的备份。在实际中断发生后再去弄清楚这一点,并不比在你选择的时间进行一次故障演练、更有经验的工程师监督下进行的策略更安全。
你可以用同样的方式为任何故障测试辩护。是等到某个东西失败,然后希望你有足够的资源和专业知识随时应对?还是在你可以提前规划资源、专业知识和影响的时机触发故障更好?你只有亲自尝试,才知道某个东西是否按你预期的方式工作。
影响预测
与故障测试相关的两种影响类型是:第一种是技术影响:级联故障、数据损坏或安全性或稳定性的显著变化的可能性。第二种是用户影响:有多少人受到负面影响,以及影响的程度?
技术影响是两者中更难预测的。你应该对系统的不同部分如何耦合以及系统中的复杂性在哪些地方有所了解,这些都是通过前几章的练习得出的;现在,你需要将这些信息转化为潜在故障的模型。
一个好的起点是使用 4+1 架构视图模型。由 Philippe Kruchten 开发的 4+1 架构视图模型将架构模型分解成多个反映特定视角的单独图表。
-
逻辑视图 描绘了最终用户如何体验一个系统。这可能采取状态图的形式,系统状态变化(例如数据库更新)与用户行为的关系被追踪。当用户点击一个特定按钮时,会发生什么?从那时起,用户可以采取哪些操作,系统状态又会如何调整?
-
过程视图关注系统正在做什么以及执行的顺序。过程视图类似于逻辑视图,只不过方向相反。焦点不再是用户在做什么,而是系统正在启动哪些过程以及为什么。
-
开发视图是软件工程师眼中的系统。架构被分解为反映应用程序代码结构的各个组件。
-
物理视图展示了我们的系统如何在物理硬件上表现。实际通过网络发送的是什么?还有哪些东西驻留在同一台服务器上?
4+1 架构中的+1 指的是场景。场景挑选一小部分功能,连接到特定的技术或功能上并集中讨论。换句话说,这些就是用例。
通过示例更容易理解这些视图。假设有一个系统,用户上传扫描的文档,这些文档被转换为文本文件。这些文本文件会根据其上下文自动添加标签,但用户可以编辑这些元数据以及转录内容。
该系统的逻辑视图可能类似于图 8-1。

图 8-1:逻辑视图,一个状态机
我们对数据的状态进行建模,追踪其从文档到处理后的验证输出的过程。每个阶段都是由用户的操作触发的,系统的功能需求也很容易看出。
另一方面,过程视图可能类似于图 8-2。

图 8-2:过程视图,执行的技术过程
处理文件的过程从触发界面开始,用户可以选择其计算机中的文件。服务器接收到文件后,将其下载到沙盒中以确保其安全。在处理过程中,文本数据会被分词,以便提取最常见的显著词汇标签。接着,我们将数据加载到编辑表单中,以便用户验证。
尽管这两种视图描述的是相同的系统,并且具有相同的功能集,但它们突出的内容不同。过程视图包含了关于确保上传文件安全以及如何识别不向用户显示的标签的需求,但如果没有逻辑视图,我们可能不会意识到系统的意图是,直到用户验证过后,数据才应被视为最终数据。
Kruchten 开发了 4+1 架构视图模型,因为他发现传统的架构图尝试将所有视角融合在一个可视化中。结果,反而造成了知识空白,因为某个视角被过度强调,忽视了其他视角。
例如,沙箱损坏的影响在过程视图中显而易见,但在逻辑视图中却完全没有反映出来。而逻辑视图将标签和文本作为可能独立损坏的不同事物加以突出,但过程视图没有揭示这一点。
开发视图和物理视图有类似的关系。图 8-3 展示了这个假设系统在开发视图下的样子。

图 8-3:开发视图,代码结构
代码被组织成两个类:文档和标签。每个类都有一组反映创建、读取、更新和删除(CRUD)结构的方法。该系统在确认文档安全后立即保存,以便在解析器失败时不会丢失数据。它在解析时进行标记化,并在解析后创建标签。
物理视图可能更像是图 8-4。
用户通过计算机上的浏览器访问系统。该 Web 应用程序运行在与单独的虚拟机(用于沙箱隔离)、预处理文档的对象存储和后处理数据的数据库交互的服务器上。
每个视图都是不同的,通过将它们结合考虑,我们能够更清晰地了解系统某个组件可能失败的不同方式,以及这种失败会影响什么。
一旦我们对系统进行了建模,并且确信自己理解了它,我们就可以通过收集来自运行中的系统的数据来进一步完善我们的故障分析,尝试确定故障发生时将影响多少利益相关者,以及故障发生的可能性。

图 8-4:物理视图,云环境中的服务器
最简单、最显而易见的起点是系统的日志。当该系统组件处于活动状态时,是否会生成日志?我们是否知道是什么触发了它,何时触发,运行了多长时间,或者其他元数据?如果没有日志,我们能否添加它们?一到两周的数据不能提供完整的图景,但它会验证停机事件会影响哪些内容,并澄清影响的规模。
估算影响的另一个技巧是考虑缓解措施是否可以自动化。我们曾经遇到过类似模板的问题。出于安全原因,我们希望标准化一个模板语言。我们需要用户将自定义模板转换过来,但不想花费几个月与客户沟通和谈判,因此我们构建了一个工具,能够为客户完成转换,并且测试新模板是否完全一致。
最后,就是我之前提到的最极端的方法:关掉组件,看看谁会抱怨。如果请求离开了你的网络,你无法知道接收方是否正在做什么,那该怎么办?在这种情况下,你需要一点一点地消除挑战,直到你定义出影响范围。微型故障类似于“关掉看看谁抱怨”,不过你只会在短时间内移除资产,并且不会等到收到抱怨才将其恢复。
终止开关
通过分析,你应该大致了解可能出错的地方。基于这些信息,在故意破坏系统组件之前,计划的一部分应该是制定终止标准。也就是说,如果破坏触发的影响超过了某个程度,你将何时、如何撤销破坏?拥有回滚策略对于任何操作系统的更改都至关重要,但只有每个人都理解故障容忍度时,它才会有效。
在任何损害发生之前设置撤销破坏的标准和流程,可以让人们有信心承担风险。
传达失败
当然,破坏可能会影响同事和用户的事物却没有提前告知他们,这并不是很有外交手腕。通常需要一定程度的沟通,但具体需要多大程度、与谁沟通,取决于破坏性更改的目标。
如果你是在破坏某些东西来测试系统的弹性,提供过多的信息可能会限制演练的有效性。故障演练的核心目的是测试恢复程序是否按预期工作。真实的故障很少会提前预告或详细描述它们是如何被触发的。
在进行故障演练之前,完全不需要通知外部用户。理论上,你的组织会成功恢复,用户不会经历任何负面影响。包括系统用户和其他工程团队在内的内部利益相关者,应该被通知系统的某个部分将进行故障演练,但他们不需要知道停机的持续时间、具体的时间点,或者将要触发的故障类型。如果演练是常规的备份和故障保护测试,通常提前一周通知就可以。如果演练将影响系统中具有不清晰或复杂恢复路径的区域,或者如果演练是在测试恢复过程中的人为因素,那么提前通知更多时间会更好。你希望给团队足够的时间来评估他们所负责的系统部分可能受到的影响,并再次确认他们的应对策略。通常,我建议最复杂的故障演练提前不超过 90 天通知。如果通知时间太长,人们会拖延到几乎没有任何预警的程度。如果通知时间太短,团队就需要停止正常的开发工作以便准时做好准备。
如果你正在破坏某个东西,目的是将其停用或以其他方式使其永久性改变,那就不同了。在这种情况下,更重要的是将变更告知外部用户。传统的做法是设定一个停用特定功能或服务的日期,宣布其弃用,并提前几个月通知用户。给用户多少时间取决于从弃用功能迁移的复杂程度。我认为,与其提前几年通知用户,不如提前几个月通知,原因和我通常不会给内部团队超过 90 天通知失败演练的理由一样:更多的时间往往导致更多的拖延。
如果你特别不幸运,你可能会发现自己处于一种无法通过电子邮件、电话甚至信件通知用户的情况。你甚至可能不知道用户到底是谁。在这种情况下,你要么需要稍微发挥一下创造力,要么只能冒险假设看似未被使用的组件实际上是未使用的。
在用户可能查找信息的地方发布弃用通知是一种解决方案。有一次,当我们找不到其他方法来弄清楚谁在使用特定的 API 时,我们就在 API 本身放了一条信息。由于我们想要去掉的属性恰好是一个字符串,我们只是把字符串的内容改成了一条信息,告知用户出于安全原因,我们将不再提供该值,并请联系客户支持获取更多信息。
但是,不管采用什么方法,你的沟通策略中最重要的一点是,在你说好的时候执行破坏性变更。如果你犹豫或延迟,用户就根本不会去迁移,而破坏的影响将更加严重。
一旦你为失败设定了日期,无论是演练还是永久性停用,你都需要履行这个承诺。
失败是一种最佳实践
总结来说,人们对风险的感知不是静态的,且往往与失败的概率无关,而更多的是与来自同伴的拒绝和谴责的潜在感受相关。由于社会压力和奖励比金钱和晋升更能激励人们,你可以通过学习如何操控组织对风险的感知来提高成功的几率。
第一项任务是理解什么行为能够让个人在组织中得到认可。这些是人们最终会优先考虑的活动。如果这些活动不是你认为能够推动现代化进程的活动,探索那些承认互补活动的结构、传统以及——没有比这更合适的词——仪式。
第二项任务是查看哪个部门负责决定何时操作员可以行使自由裁量权,并偏离既定流程。这些人是处理失败时设定无责与问责比例的人。正是这些人会提供支持,也必须认同任何破坏性变更战略,才能确保其成功。一旦提供了这种支持,围绕失败的焦虑往往会得到缓解。
一旦你知道如何操控组织对风险的认知,成功管理故障就全靠准备。虽然你无法预见所有可能出错的情况,但你应该能做足够的研究,确保你的团队能够有信心应对未知问题。至少,每个人都应该了解并理解回滚破坏性更改的标准。
失败并不一定会危及用户信任。如果问题迅速得到解决,并且用户能够收到清晰而诚实的沟通,偶尔的失败反而能够引发服务恢复悖论,并激发更强的用户信任。
组织不应回避失败,因为失败是无法完全避免的。它只能被推迟一段时间,或者转移到系统的其他部分。最终,每个组织都会经历失败。将失败作为学习的来源,意味着你的团队会积累更多的经验,并最终提高减少对用户产生负面影响的能力。因此,练习从失败中恢复,使团队更具价值。
第九章:如何完成
当我在联合国(UN)工作时,我的老板常常以“当网站完成时……”开始对话,我会回答,“网站永远不会完成。”我认为自己的一项成就就是,在我离开联合国时,大家已经接受了敏捷、迭代的过程,将软件视为一个需要不断维护和改进的活的东西。大家不再说“当网站完成时……”
技术永远不会完成,但现代化项目是可以完成的。本章将介绍如何定义成功,以清楚地了解何时完成现代化工作以及下一步该做什么。在项目初期,成功的样子可能看起来显而易见,但通常组织中的某些人对成功的理解与其他人的假设不同。让所有人达成共识并始终保持一致,对于确保项目成功完成至关重要。
揭示假设
很多破碎的系统最终是这样形成的,因为参与实施的组织单元对自己的角色以及它们如何为更大的图景做出贡献的理解不一致。任何形式的现代化、重新架构或重新思考现有技术系统都是一场持久战。工作将持续几个月甚至几年。因此,在这些项目中工作时,团队中的每个人都必须能够回答这个问题:我们如何知道它在变得更好?
团队应该知道长期的答案,但他们也应该知道,在从现在起的几天或几周内,什么是更好的。这种工作是一个长期的过程。要做好它,你必须保持自己的韧性。你必须注意当人们认为项目进展不顺利时他们的行为。管理自己的怀疑比处理来自不理解进展的同事的破坏行为要难得多,因为他们对改进的期望与团队其他成员的期望不同。
方法 1:成功标准
成功不会迅速或一次性地发生,这就提出了一个问题:你如何知道一个项目是否朝着正确的方向发展?当我与我的团队(通常还有我的老板或其他重要利益相关者)设定成功标准时,我们首先确定评估的时间框架。如果两个人都同意发布一个功能是成功的标志,他们仍然可能在时间表上产生冲突。三个月内发布和一周内发布是不同的。对于认为功能应该在一周内发布的人来说,部署的时间推迟到很晚会像一个警告信号。对于认为应该在一年内发布的人来说,相同的部署则是验证。
如果你熟悉目标与关键成果(OKRs),那么成功标准可能会呈现类似的形式。首先,你定义你的目标,然后定义你如何知道自己已经达成目标。不同的是,OKRs 通常侧重于目标已完成的迹象,而成功标准应该侧重于你朝着正确方向前进的迹象。
这个策略的价值在于,所选择的标准明确了将要采取的方式,避免了对方法的争论。如果成功标准全部聚焦于实施新功能,那么团队可能不会优先解决技术债务。如果成功标准关注的是减少错误数量或加快最小恢复时间,那么团队就必须专注于改进现有代码。只要能够避免人们争论原则、理念和其他有关好工程与差工程的普遍性问题,就应该抓住这个机会。
同样的目标根据团队的模型可以有非常不同的成功标准。例如,咨询模型会关注客户吸收和采纳过程及最佳实践的能力,而非部署次数。顾问无法对部署有太多控制,而唯一能获得控制的方式就是不再做顾问。作为软件工程师,我们很容易陷入一个误区,认为有效的工作总是像写代码一样,但有时,通过教别人如何完成工作,而不是自己做,往往能更接近你期望的结果。
示例:添加持续集成/持续部署
-
目标:将服务迁移到自己的部署流水线。
-
时间表:一个季度。
-
成功标准:
-
部署时间减少了 20%。
-
服务团队中的任何人都可以发起并管理一次部署。
-
每周的部署次数增加。
方法 2:诊断-政策-行动
由理查德·鲁梅尔特(Richard Rumelt)开发,这种方法与成功标准使用相同的信息,但呈现方式略有不同。^(1) 信息分为三个部分:诊断、政策和行动。诊断是对待解决问题的定义。政策指的是潜在解决方案的边界——即关于解决方案应做什么或不应做什么的规则。最后,行动是团队在不违反政策的情况下为解决问题而采取的步骤。
Rumelt 方法的有用之处在于,它更加专注于你要做什么以及如何做。当关于成功的定义没有达成广泛共识,且没有单一权威做出决定时,这种方法可能会更有效;但如果你的团队在路线图上遇到困难,可能会觉得这种方法更难实施。遗留系统现代化的挑战可能是多种多样的,交织在一起,且政治复杂。团队可能无法就成功的定义达成一致,他们也可能对解决问题需要执行的任务有不同意见。Rumelt 方法更适用于那些在步骤和顺序上易于达成一致,但在改善标志的定义上较难达成共识的情况。
示例:升级数据库
-
诊断:我们的数据库软件版本已经过时好几版了,供应商将不再提供支持。
-
政策:我们希望使用蓝绿部署。我们绝不会一次性关闭一个数据库版本并开启另一个版本。
-
行动:
-
我们将在每次升级前备份数据。
-
我们将在此日期升级到 3.2 版本。
-
我们将在此日期升级到 3.3 版本。
比较
在定义成功方面,成功标准与诊断-政策-行动方法各有优缺点。成功标准能更直接地将现代化活动与其能够展示的附加价值联系起来。它在团队具体做什么上提供了更多灵活性,因为它不规定具体的做法或任务集。这是一个与上司和其他可能倾向于过度管理团队的监管者进行的极好的练习。做某事的方式应该由被信任执行任务的人来决定。因此,诊断-政策-行动方法过于注重细节,不利于团队向上管理。如果后续需要更改行动集,团队可能会不情愿这样做,并且在高级领导面前显得不一致。自由裁量权对于成功至关重要;如果没有被要求提供反馈,切勿通过呈现实施细节来放弃团队的自由裁量权。领导层需要知道的是你正在推动的成果。
另一方面,有时候你知道更好的结果是什么样子,但却不知道采取哪一组行动才能实现它。研究和实验可能无法在竞争的路线图中指出明确的赢家。组织的一方可能会说,首先需要解决这个问题,而另一方则可能争论完全不同的解决方案应该优先。若问题只能通过高层决策解决,诊断-政策-行动方法会更合适。正是这种使成功标准能够有效适应新信息和变化策略的灵活性,反而会在团队对日常工作犹豫不决时带来混乱。
标记时间
定义什么是成功有助于保持团队的统一,但由于成功标准和诊断—政策—行动的方法把挑战切分成更小的成就,你还需要防止团队失去对这些小成就的信心。如果团队觉得他们所取得的成就不值得他们投入的时间,那么你定义的成功就会失去效果。
对你有利的是,时间的感知和风险的感知一样是可变的。找到标记时间的方式,就是找到一种方法帮助人们脱离那些让时间变慢的日常挫折,帮助他们集中注意力于更大的图景。我最喜欢的标记时间的方法是子弹日记。我有一本书,每天我都会写下五件我打算做的事情,以及我认为它们需要多长时间。在一天中,我会在完成这些任务后打勾,并记下一些带有重要细节的小笔记。在工作进展缓慢的时候,我常常在边缘涂鸦或者用从供应商那得到的贴纸装饰页面。
每当我翻开子弹日记中两三周前的页面时,我都惊讶于事情变化的如此之大。我完成的任务比我想象的要多得多。这些完成的任务感觉像是几个月的工作。有时候,回顾仅仅一周前的事情,我都会感到震惊。
正如人类在判断概率时常常很糟糕,我们在判断时间时同样也很糟糕。感觉像是过去了很久的事情,可能实际上只不过是几天而已。通过标记时间,我们可以重新调整对事情进展的情感认知。找一种方式记录你工作的内容和时间,这样团队就可以轻松地回顾,全面了解他们的进展。
你可能会想说:“哦,他们可以通过我们的项目管理系统来做到这一点。”但是,项目管理工具通常是一次展示一项工作的流。标记时间更为有效,因为它能呈现出更完整的图景,展示人们生活中某个特定时刻的情况。知道某个任务是在某天关闭的,并不一定能让我回到那个时刻。有时候,一个日期就是一个日期。当你标记时间时,要用一种能唤起记忆的方式,突出那个时刻和现在团队所在位置之间的距离感。
对我来说,子弹日记很有效,因为每一页都是当时我脑海中的所有内容的快照。我记录工作项目、个人项目、事件和社交活动、假期以及疾病。任何预计会占据大部分时间的事情,我都会写下来。回顾项目进展时,凭借这些信息,我能为它赋予一些我之前没有考虑过的背景。一旦我考虑到这一点,我就意识到自己并没有在一个地方无意识地撞墙。一点一滴,逐步地,我让事情变得更好。
成功的事后分析
对于软件工程师来说,事后总结是在发生故障后进行的回顾,详细探讨故障的时间线、影响因素以及最终的解决方案。我们通常在提到事后总结时,会加上“无责”前缀,强调进行事后总结的目的是理解问题出在哪里,而不是将责任归咎于某个犯错的团队或个人。
但事后总结并不限于失败。如果你的现代化计划包括以另一个成功项目为模范,考虑对该项目的成功进行事后总结。记住,我们倾向于将失败视为运气不好,而将成功视为能力的体现。我们对失败进行事后总结,因为我们通常认为失败是复杂的,涉及多种因素。而成功则发生于简单、直接的原因。
实际上,成功和失败一样复杂。你应该采用与从失败中学习相同的方法来从成功中学习。
事后总结与回顾会议
现在你可能会想,你的组织确实会对成功进行事后总结,只不过你们叫它回顾会议。
没错。每次冲刺或上线后的回顾会议确实会问一些和事后总结相似的问题。然而,我还没有和任何一家技术组织合作过,他们会把回顾会议和事后总结看作同一回事。实际上,回顾会议要更加非正式。它们不会生成报告供其他人阅读——至少,至少我没见过。我不知道有哪个组织会把回顾会议的内容公开发布到网上供公众阅读。我参加过很多回顾会议,我们捕捉到了一些很有价值的信息并进行了深入的自我反思,但我很少见到这些成果会从白板上离开,并且被分享给其他团队。
作为一个行业,我们反思成功,但研究失败。有时候甚至过度研究。我建议,如果你是以另一个团队或另一个组织的成功为模范,你应该花一些时间真正去研究该成功是如何产生的。
进行事后总结
在讨论如何写关于成功的事后总结之前,先概述一下技术组织中事后总结的常规流程可能会更有帮助。传统的事后总结格式有一些特征,除了事故响应之外不太适用。例如,故障发生得很快,并且理想情况下在几小时内解决,因此为其创建详细的事件时间线比起持续几个月的项目更为简单。
传统的事后分析描述了故障的影响。团队会讨论并记录做得好的地方、做得不好的地方,以及在哪些地方人们认为自己得到了幸运。如前所述,事后分析通常会包括关于事件响应的详细时间线。这些时间线会分解发生了什么、何时发生以及谁做了什么。事后分析通常不会通过名字提到个人,以保护他们免受责备。相反,会使用“运维工程师 1”或“软件工程师 2”等别名来识别个人及其采取的行动。事后分析以可执行的改进步骤作为结束。组织如何改进做得不好的地方,或如何在做得好的地方进一步加强?
事后分析是通过回顾通讯记录和访谈团队成员来撰写的。然后,举行最终的回顾会议,向整个团队展示并验证所收集的信息。
在对成功进行事后分析时,你必须权衡需要投入多少时间和精力才能获得如此详细的信息,尤其是在时间线跨越几个月而非几小时的情况下。这种努力很快就会变得繁琐且官僚化。传统的事后分析通常是由应对事件的软件工程师撰写的。这些人是你希望投入到软件开发中的,而不是用来写报告的。
基于这个原因,成功的事后分析应该像它的轻量级版本回顾一样进行,但文档记录时应遵循传统事后分析的理念。事后分析的价值不在于其详细程度,而在于它在知识共享中所扮演的角色。事后分析是为了帮助未参与事件的人避免犯同样的错误。最好的事后分析还应当传播到组织外部,以通过透明度来培养信任。
事后分析建立了一个关于实际发生了什么以及具体行动如何影响结果的记录。它们并不记录失败;而是提供了背景。成功的事后分析应当发挥类似的作用。为什么某种特定的方法或技巧能够成功?最终的策略是否与团队最初的计划一致?在成功的事后分析中,你的时间线应该围绕以下问题展开:组织是如何执行原始策略的,策略发生了哪些变化,这些变化发生了何时,以及是什么促发了这些变化?
即使是最大的成功,也有一些本可以做得更好的挑战和幸运的时刻。记录这些内容有助于人们评估你的方法是否适用于他们自己的问题,并最终复制你的成功。
如果你确信另一家组织的策略对你的问题有效,不要等那些工程师开始做事后分析。你可以通过和人们喝咖啡来收集你所需的大部分信息。如果有一个你想复制成功的组织,花几周时间采访他们的人员,了解他们的策略,并使用事后分析的关键问题来引导你的访谈。
-
什么做得好?
-
有什么可以改进的地方吗?
-
你们在哪儿运气好?
两个战情室的故事
在最终确定战略之前,我会分析成功的各种因素,因为我曾看到过组织从成功中学到错误的教训。例如,我曾与一个项目严重滞后的组织合作。团队成员不仅没有对项目的任何一部分设定一个现实的结束日期,他们甚至不知道为什么项目一开始就一直在延误。这是一个需要多个不同组织单位共同合作和共享信息的大型项目。我的咨询团队刚刚结束了一个成功的项目,该项目面临类似的挑战。这个组织听说我们的项目中,我们安排了来自各个组织单位的代表在同一个房间里工作。几个月来,除了每天不再回到他们的办公室或工作区,这些人一起坐在一个大会议室里,带着他们的笔记本电脑。这就是一个战情室,而不是一个会议室。随着时间的推移,这个会议室看起来更像一个开放式的共享工作空间。
这个项目失败的组织决定在没有与我们沟通或了解我们如何运作的情况下,复制我们的战略。一个大型会议室被预定了一个月,来自各个受影响的运营单位的代表们被召集进来,并告诉他们必须一起在那个会议室里工作。
这个组织没有看到我们所取得的相同结果,最终领导层的代表们主动联系了我们,问我们为什么会这样。由于没有研究我们的成功,他们只好再次研究自己的失败。
我理解他们的沮丧。毕竟,他们完全按照我们的流程进行操作,却得到了不同的结果。至少从他们的角度来看是这样的,但当他们征求我的意见时,我注意到他们错过了战情室中的一个关键因素。
他们把错误的人放了进去。
工作小组与委员会
在本书中,我提到过一些结构,它们涉及一小群人,为更大的团队提供建议、制定计划,并在某些时候将工作委派出去。这些就是工作小组。近年来,工作小组这个词变得流行,取代了委员会一词,因为对大多数人来说,委员会听起来有些官僚化。委员会有着什么都做不成的名声,或者更糟糕的是,它们通过妥协来推动工作。就像那句老话说的,骆驼是由委员会设计出来的马。
工作小组没有这些包袱,因此人们经常会说他们想要成立一个工作小组,结果却转而成立了一个委员会——仿佛仅仅改变一个结构的名称就能让这个结构成为更有效的工具。更糟糕的是,大多数人已经无法区分工作小组和委员会的区别。这正是领导者在复制我们成功策略时忽视的地方。我们项目的战情室成员是工作小组,而失败项目的战情室成员则是委员会。
到底有什么区别呢?
工作小组放松了等级制度,允许人们跨组织单位解决问题,而委员会则既反映了组织边界,又强化了这些等级制度。我们的战情室由软件工程师和网络管理员组成。我们将需要共同合作实施项目的人聚集在同一个房间里,肩并肩工作,而不是通过电子邮件、上级和无数的电话会议进行沟通。
另一方面,失败的战情室由高层管理人员组成。这个战情室并没有将报告给不同指挥链的同事聚集在一起,而是仅仅模仿了现有的结构和组织内部的政治。更糟糕的是,大多数这些高管的日常工作几乎完全由会议构成。他们并没有和同事肩并肩地工作,而是将战情室当作一个放置自己物品的地方,在此匆忙进出其他会议室,继续进行日常工作。
工作小组是面向内部的;工作小组的客户是工作小组的成员。人们加入工作小组是为了与同侪分享自己的知识和经验。工作小组之所以有效,是因为它为跨组织合作和问题解决提供了空间。组织中的执行层人员可以向那些已经经历过类似挑战的同行提出自己的问题,并听取他们的经验或与他们交换想法。有时这会导致对领导层的建议,但工作小组的主要目的是解决问题并在整个组织或行业中进行宣传。工作小组通常由组织执行层的人员发起和组成。
委员会的成立是为了为委员会外部的听众提供建议,通常是为高级领导提供建议。而工作小组则向那些认为自己与工作小组主题领域相关的人开放,委员会由它们所服务的实体选定,通常对其他人封闭。这个外部权威决定了委员会的范围和目标。委员会向外部权威报告,并且仅为其利益存在。
委员会通常会有很多程序性规定,但没有主席和罗伯特议事规则,并不意味着委员会就是一个工作小组。
这种模式可能会有很大的变化。例如,黄色警戒小组可能不是自我选择的,忽视其建议可能会带来严重后果,但黄色警戒小组的目的是暂时重新组织和重新分配资源。这个小组最终向一个更接近同级别而非高层的领导汇报。重要的结论是,工作小组放松了组织边界,而委员会则强化了这些边界。
高级管理人员在战情室里代表他们的部门,几乎没有什么价值。由于他们并没有直接实施技术,他们无法在没有回去向自己团队的工程师请教的情况下,说明哪些妥协措施能够推动项目进展。我们的战情室之所以成功,是因为它缩短了这些对话所需的时间距离。失败的战情室则在现有的沟通障碍上又增加了一层“电话游戏”。人们永远无法确信,战情室传递出的信息是否准确,或者代表你的经理是否误解了某个实现细节。
有个有趣的故事:我曾不时地组织一些由高级管理人员组成的战情室。每当组织处于一种极度恐慌的状态,高层领导们开始对自己的团队进行微观管理时,我就会采取这种策略。如果软件工程师将所有时间都花在开会向经理汇报问题,他们是无法解决问题的。必须把“靴子”从他们的脖子上拿下来。
在那种情况下,我的团队通常会组织两个战情室:一个让工程师们一起解决问题,另一个则配备了大量的华丽仪表盘,用来让我照看高级管理人员。一个领导者最有价值的技能,就是知道何时需要退出,让团队自主运作。
成功如果没有定义,是不明显的
如果现代化项目没有清晰的成功定义,它们会发现自己在接近终点时,终点反而越来越远。不要认为成功是显而易见的。你团队的不同成员可能对“更好”有不同且相互竞争的看法。每个人都需要能够解释他们是如何知道自己的努力正在推动项目向前发展的,这样大家才能合作。需要像遗留系统现代化这种极端努力的项目,解决问题的需求永远不会短缺。通过定义成功,你能够防止终点线不断移动。
在下一章中,我将解释如何在日常维护中保持软件,以避免首先需要进行遗留系统现代化的情况。现代化工作的终点不一定是所有问题都解决,技术也达到完美。我至今还没有遇到过可以用这种方式描述的系统,我曾为从成立六个月到已经存在 200 年的组织工作过。所有技术都是不完美的,因此遗留系统现代化的目标不应是恢复神话般的完美,而是将系统带入一个可以围绕安全性、稳定性和可用性保持现代最佳实践的状态。
第十章:未来可持续性
完成现代化项目的最佳方式是确保在几年内你不会再次经历整个过程。未来可持续性不是关于避免错误;而是知道如何逐步维护和发展技术。
两种类型的问题会促使我们重新思考一个正在老化的工作系统。第一种是使用变化。第二种是衰退。扩展挑战是使用类型的变化:我们有了更多的流量或不同类型的流量,跟以前不一样了。也许使用系统的人比之前多了,或者我们添加了一些新功能,随着时间推移,人们使用该技术的目的发生了变化。
使用变化的速度并不恒定,因此很难预测。一个系统可能永远不会面临扩展挑战。一个系统可能达到某个使用水平后再也不会增长。或者它可以在短短一段时间内扩展一倍或三倍。或者它可以在多年中缓慢扩展。若真的发生扩展挑战,它们会是什么样子,将取决于多种因素。由于系统使用的变化难以预料,它们也很难被标准化。这是一种优势。当我们标准化某些东西时,我们就不再思考它,不再把它考虑进我们的决策中,有时甚至会忘记它的存在。
衰退,另一方面,是不可避免的。它们代表了向不可避免的最终状态自然线性发展的过程。其他因素可能会加速或减缓它们的进程,但最终我们知道最终的结果会是什么。例如,1999 年 9 月 9 日的日期不会因为使用变化而从 1999 年的日历中消失。不管那些被编程为在日期缺失时将 9/9/99 作为空值赋给列的机器系统行为如何,9 月 9 日的事件终究会发生。
内存泄漏是这种变化的另一个很好的例子。系统使用可能会影响泄漏何时成为一个重大问题,但低系统使用量并不会改变内存泄漏的存在,内存泄漏最终会成为问题。解决问题的唯一方法是修复它。
硬件生命周期是另一个例子。最终,芯片、硬盘和电路板都会出现故障,并需要更换。
这些衰退类型是危险的,因为人们会忘记它们。它们的影响会长时间不被察觉,直到某一天它们最终完全崩溃。如果组织特别不幸,问题已经深深嵌入系统中,并且一开始并不清楚究竟是什么出故障了。
举个例子,考虑一下 Y2K 问题。一个令人担忧的情况是,很多计算机程序设计时采用了两位数的年份,这在 2000 年时成了问题,因为缺失的前两位数字与程序假设的数字不同。大多数技术人员都知道 Y2K 故事,但你知道吗,Y2K 并不是第一次出现这种目光短浅的编程错误?也不会是最后一次。
时间
软件工程师在程序中常常把时间搞砸,这实在是令人难以置信。在 1960 年代,一些程序只有一位数的年份。TOPS-10 操作系统只能表示 1964 年 1 月 1 日到 1975 年 1 月 4 日之间的日期。工程师通过修补这个问题,添加了三个位,使 TOPS-10 能够表示直到 2052 年 2 月 1 日的日期,但他们从现有的数据结构中取走了这些位,以为它们没有被使用。结果发现,TOPS-10 上的一些程序已经重新利用了这些存储区域,这导致了一些奇怪的错误。^(1)
应该为日期分配多少存储空间是一个永恒的问题。为时间分配无限存储空间是不明智且不切实际的,然而任何数量的存储最终都会用完。程序员必须决定,在多长时间后,程序仍在运行的可能性看起来不大。至少在计算机的早期,倾向于低估软件生命周期。一个正常运行的软件很容易保持运作 10 年、20 年、30 年,甚至更长时间。但在计算机发展的早期,两三十年看起来是很长的时间。如果时间只给出足够的空间直到 1975 年,那么修复可能会延续到 1986 年。1989 年,某些操作系统编程限制了系统的成熟时间为 1997 年——如此这般,循环往复。
这些程序仍然在我们身边,我们还没有到达它们的所有“成熟”日期。某个由世界计算机公司(World Computer Corporation)创建的日期格式将在某一年达到存储限制,我们不知道是否有现有系统在使用它。更值得关注的是 2038 年,届时 Unix 的 32 位日期将达到极限。虽然大多数现代 Unix 实现已经转向 64 位日期,但网络时间协议(NTP)的 32 位日期组件将在 2036 年 2 月 7 日发生溢出,这为我们提供了一个潜在的预览。NTP 负责同步互联网上互相通信的计算机时钟。计算机时钟若严重不同步——通常是五分钟或更长时间——会导致无法建立安全连接。这个要求源自 MIT 在 2005 年发布的 Kerberos 版本 5 规范,它利用时间来防止攻击者重置时钟,继续使用过期的票证。
我们不知道 NTP 和 Unix 溢出将引发哪些问题。大多数计算机可能已经升级,因此不会受到影响。如果运气好,2038 年的这一时刻可能会悄然过去,就像 Y2K 问题一样。但是,时间漏洞并不需要触发全球性的灾难来产生戏剧性的和昂贵的影响。过去的时间漏洞曾一度清空养老金基金,干扰短信发送,崩溃视频游戏,甚至让停车表无法正常工作。2010 年,由于时间漏洞,德国有 2000 万张芯片和密码银行卡无法使用^(2)。2013 年,NASA 因为一个类似 2038 问题的时间漏洞失去了对 3.3 亿美元的“深空撞击”(Deep Impact)探测器的控制。
时间漏洞很棘手,因为它们往往在引入后几十年,甚至有时是几个世纪才会引发问题。IBM 在 1970 年代制造的主机将在 2042 年 9 月 18 日达到溢出点。某些得克萨斯仪器(Texas Instruments)计算器无法接受 2049 年 12 月 31 日之后的日期。某些诺基亚手机只接受到 2079 年 12 月 31 日的日期。多个编程语言和框架使用的时间戳对象将在 2262 年 4 月 11 日发生溢出。
并不是程序员不知道这些漏洞的存在,只是很难想象今天的技术会持续到 2262 年。同时,20 世纪 60 年代编程房间大小的主机的工程师们也从未想过他们的代码会延续几十年,但现在我们知道,这些老旧的程序依然在生产环境中运行。到了 2000 年,旧软件(有时还有与之配套的机器)不仅没有被淘汰,反而由与其创作有两三代代际差距的技术人员在维护。
解决时间漏洞通常相对直接——当我们知道它们的存在时。问题在于,我们往往忘记它们即将到来。我们已经看到由于 2038 漏洞而导致的系统故障。金融机构中必须计算 20 到 30 年后利息支付的程序,实际上是这些类型错误的早期预警检测系统。然而,组织必须了解他们的遗留系统状态(换句话说,是否已经打上补丁),并意识到这些事件正在发生。
无法逃避的迁移
面向未来的系统建设并不意味着构建一个永远不需要重新设计或迁移的系统。那是不可能的。它意味着构建并且,更重要的是,保持系统更新,以避免一个需要重组正常运作的长期现代化项目。面向未来的秘诀在于将迁移和重新设计变成不需要大力操作的日常例行事务。
大多数现代工程组织已经知道如何处理使用变更——他们会监控活动的增加,并根据需要扩展或缩减基础设施。如果给予足够的时间和优先级,他们会重构和重新设计系统组件,以更好地反映最可能的长期使用模式。早期并频繁地对系统进行更新,只是一个纪律性的问题。那些忽视清理技术负债的组织,最终将不得不进行繁琐且风险较大的遗留系统现代化工作。
我最喜欢的关于为频繁更新设定节奏的比喻来自播客Legacy Code Rocks(www.legacycode.rocks/)。推出新功能就像举办一个家庭聚会。在你清理房子之前,举办的家庭聚会越多,房子的状况就越差。虽然这里没有一条适用于所有人的硬性规则,但在每次推出n个新功能后,自动安排时间重新评估使用变更和技术负债,将使得更新系统的过程规范化,从而确保其长期健康。当人们把重构和必要的迁移与系统构建错误或故障挂钩时,他们会推迟执行这些操作,直到事情开始崩溃。而当这些变更管理过程成为日常工作的一部分时,就像剪头发或换车油一样,系统可以通过逐步现代化来确保未来的可持续性。
对于恶化问题,需要采取不同的策略。有时它们可以被监控。例如,电池随着时间的推移会老化,它们的性能下降是可以被捕捉和追踪的。某些恶化问题则更加突然。时间错误在爆发之前不会发出任何警告。如果组织已经忘记了它,就没有什么可以监控的了。
说你永远不应该在系统中引入逐渐恶化的变化是天真的;这些问题往往是不可避免的。错误在于假设当问题成熟时,系统仍然无法运行。技术有一种方式可以将其寿命延长,远远超过人们的预期。例如,纽约市地铁的部分开关控制面板可以追溯到 1930 年代。索尔兹伯里大教堂的钟表自 1368 年开始运行。在加利福尼亚州利弗莫尔市的消防站 6 号楼上,有一个自 1901 年以来一直亮着的灯泡。全世界各地,我们的日常生活都受到那些远超预期过期日期的机器的控制。
相反,管理逐渐恶化的问题归结为以下两种做法:
-
如果你引入了一些会逐渐恶化的东西,那就让它优雅地失败。
-
缩短升级之间的时间间隔,让人们有足够的时间来实践这些升级。
优雅失败
Y2K 和类似的 bug 没有导致人类文明的终结,原因在于它们不会以均匀的强度影响所有受其影响的系统。不同的机器、不同的编程语言和不同的软件处理同一个问题的方式有很大的差异。有些系统会触发恐慌,有些则会继续运行。是否让系统触发恐慌并崩溃,或者忽略问题并继续执行,通常取决于失败是否发生在事务的关键路径上。
优雅地失败并不总意味着系统避免崩溃。如果一个 bug 导致银行账户的日常批处理任务计算累计利息失败,系统通过默认归零并继续执行来从错误中恢复,这并不是优雅地失败。如果允许这种错误默默地失败,会让很多人很快感到不满,而恐慌会立即提醒工程团队问题所在,以便解决并重新运行批处理任务。
错误离用户界面有多近? 如果错误是可能由用户输入触发的,那么优雅地失败意味着捕获错误并记录事件,但最终引导用户再次尝试,并提供一个有用的消息来解释问题。
错误是否会阻塞其他独立进程? 为什么会阻塞其他进程?阻塞意味着共享资源,这表明进程并不像最初想的那样独立。对于真正独立的进程,记录错误并最终让系统继续运行可能是可以接受的。
错误是否出现在更大算法的一个步骤中? 如果是这样,你可能别无选择,只能触发恐慌。如果你可以在一个多步骤的过程中省略一个步骤,并且不影响最终结果,那么你应该重新考虑这些步骤是否必要。
错误会破坏数据吗? 在大多数情况下,坏数据比没有数据更糟。如果某个错误可能会破坏数据,你必须在错误发生时触发恐慌,以便解决问题。
在编程时,考虑到不可避免的退化是很重要的。当你没有意识到你别无选择,只能在潜在的 bug 中编程时,这种思维练习就不太有用了。你无法知道自己不知道的事情。
但是,花些时间考虑你的软件如何处理日期偏差 20 年、时间倒退一秒、出现技术上不可能的数字(比如 11:59:60 pm)或者存储驱动器突然消失等问题,还是值得的。
当有疑虑时,默认触发恐慌。更重要的是让错误被发现,以便能够解决它们。
升级之间的时间应该更短,而不是更长
几年前,我为我的厨房买了一个那种俗气的字母板——你知道的,就是那种上面写“生命如花,尽情绽放”或“爱使这个家成为家”的板子。只是,我的写着“真相是反直觉的”。我们在面对退化问题时的直觉反应是尽可能推迟它们,如果不能完全消除它们的话。就个人而言,我认为这是一种错误。我知道通过经验,工程师们做事情的频率越高,他们做得越好,越容易记得需要做的事,并做出相应的计划。
例如,2019 年发生了两个重要的时间错误。第一个是 GPS 纪元的回滚;第二个是闰秒。
GPS 回滚是一个与之前描述的时间错误完全相同的问题。GPS 通过一个 10 字节的存储块表示周数。这意味着它可以存储最多 1024 个值,1024 周等于 19.7 年。与 Y2K 问题类似,当 GPS 达到第 1025 周时,它会重置为零,而计算机无法识别这一点,导致它把所有数据回溯 20 年。
这一情况在 1999 年只发生过一次。尽管商用 GPS 从 1980 年代起就已出现,但到 1999 年时,它并没有真正普及。接收器所用的芯片价格过高,直到计算机足够快,能够将这些数据与路线计算或将物理地标与其坐标关联时,GPS 的便利性才得以体现。由于 GPS 的有用功能尚未准备好进入市场,消费者对该技术的隐私问题更为敏感。1997 年,美国联合包裹公司(UPS)因试图在所有卡车上安装 GPS 接收器而爆发了著名的罢工。
所以,第一次 GPS 回滚的影响是微乎其微的,因为 GPS 当时并不普及。然而,到 2019 年,世界已经发生了翻天覆地的变化。二十年在技术领域是很长的时间。几乎所有手机都配备了 GPS 芯片,且各种应用程序都已在 GPS 的基础上开发出来。
结果证明,人们更换配备 GPS 的设备非常频繁。对于许多用户来说,移动应用更新是无缝且自动的。我们已经习惯了每两到三年就换一部新手机,因此 2019 年的回滚事件大部分并没有引起大规模的混乱。使用旧款手机的用户遇到了一些问题,但他们被鼓励从运营商处购买新手机。
2019 年的第二个时间错误,闰秒,情况略有不同。闰秒字面意思就是:在一年中增加一个额外的秒数,以便让计算机时钟与太阳周期保持同步。与闰年不同,闰秒无法预测。日出到日落之间的秒数取决于地球的自转速度,而这个速度是不断变化的。不同的力量促使地球加速自转,另一些力量则让地球减速。
这是一个有趣的事实:改变地球自转速度的众多因素之一就是气候变化。冰层压迫着地球的陆地块,当冰层融化时,这些陆地块开始向极地漂移。这使得地球自转加快,白天的时间也变得更短。
从 1972 年到 2020 年,共发生了 28 次闰秒,但由于一些力量使地球减速,而另一些力量使其加速,因此每年是否有闰秒之间可能会有显著的间隔。在 1999 年的闰秒之后,直到六年后才需要下一次闰秒。2009 到 2012 年之间没有闰秒。2015 年和 2016 年都发生了闰秒,但接下来的三年里没有发生任何闰秒。
闰秒从来都不是一件愉快的事,但如果可以将每次最近的闰秒问题报告作为参考,长时间的间隔比起其他情况,问题会更加严重。即便是三年这样短的间隔,也足以让新技术得到开发,或者比之前更受欢迎。抽象和假设被提出,它们逐渐融入工作系统中,最终被遗忘。
云计算和智能手机相关的行业正是在闰秒的多年间隔即将到来之时开始发展的。等到下一个闰秒事件发生时,庞大的平台已在没有当时技术的基础上运行。这些技术是由那些可能根本不了解闰秒概念的工程师构建的。一些服务提供商未能及时修补更新以管理闰秒。Reddit、Gawker Media、Mozilla 和 Qantas Airways 都遇到了问题。
随后是另一个多年间隔,直到 2015 年的闰秒才给 Twitter、Instagram、Pinterest、Netflix、Amazon 和 Beats 1(现为 Apple Music 1)带来了问题。相比之下,2016 年的闰秒几乎没有引起什么反响。仅仅六个月的间隔,这次闰秒似乎只在 CloudFlare 的 102 个数据中心中的少数机器上引发了问题。
那么 2019 年的闰秒呢?它发生在又一个多年间隔后,导致了超过 400 次航班取消,因为 Collins Aerospace 的自动依赖监视广播(ADS-B)系统未能正确调整。ADS-B 并不新鲜,但 FAA 已经发布了要求到 2020 年飞机必须配备的规定,因此它的采用比上一轮闰秒时要广泛得多。
一般来说,我们处理问题的能力随着我们处理问题的次数增加而变得更强。当退化现象的成熟日期之间的间隙越长,丧失的知识就越多,而在没有考虑不可避免因素的情况下,关键功能的构建也变得更加可能。尽管 GPS 周转是在 20 年的间隙后发生的,但由于最可能受影响的设备的升级周期加速,它受益匪浅。很少有人使用 20 年的老手机或平板电脑。另一方面,闰秒的引入在每次间隙发生时,几乎始终会导致混乱。
一些退化现象在大规模下具有非常短的间隙,组织无需额外干预。例如,普通的存储驱动器寿命为三到五年。如果你拥有一块驱动器——例如你电脑里的驱动器——通过定期备份并在驱动器最终故障时更换电脑,你可以降低这种不可避免的故障带来的风险。
如果你在运营数据中心,你需要一个策略,以防止驱动器故障瘫痪运营。你需要定期备份,并几乎可以瞬间恢复。这看似是一个巨大的工程挑战,但创建这种韧性的架构在规模中本身就已经内建。数据中心不仅仅拥有几块硬盘和需要在三到五年内更换的驱动器间隔。数据中心通常有成千上万到数十万块驱动器在持续故障。2008 年,谷歌宣布它用 4,000 台计算机和 48,000 个存储驱动器在六小时内整理了一个 PB 的数据。一次运行总是会导致至少一块 48,000 个驱动器中的硬盘损坏。^(3) 同一时期进行的正式研究表明,年驱动器故障率为 3%。^(4) 以 3%的故障率,一旦进入数十万块驱动器,你开始看到每天都有多块驱动器故障。
虽然没有人会认为驱动器故障是愉快的,但一旦数据中心达到足够的规模以至于处理驱动器故障成为常规操作时,它们并不会引发停机。因此,与其延长不可避免变化之间的间隔,倒不如缩短它,以确保工程团队在构建时考虑到不可避免的因素,并且负责解决问题的团队知道该如何应对。
关于自动化的警告
如果无法完全消除恶化的变化,人们通常会选择自动化其解决方案。在某些情况下,这种自动化能够带来相对较小风险下的巨大价值。例如,定期未能更新 TLS/SSL 证书可能导致整个系统突然停摆,且没有任何预警。自动化更新证书的过程意味着这些证书可以有更短的有效期,这增加了它们的安全性。
在考虑将问题自动化解决时,最需要考虑的是:如果自动化失败,是否能清楚地知道出了什么问题?在大多数情况下,过期的 TLS/SSL 证书会触发明显的警报。要么连接被拒绝,这时证书的有效性应当在检查列表中成为可能的嫌疑问题,要么用户会收到警告,提示连接不安全。
当自动化掩盖了或以其他方式让工程师忘记系统实际上在幕后做了什么时,问题会更加严重。一旦这种知识丧失,所有建立在这些自动化活动上的系统将不会包含任何防范自动化失败的保障措施。自动化的任务成为平台的一部分,如果负责平台的工程师清楚它们的存在并对此负责,那这没有问题。
很少有程序员会考虑如果垃圾回收突然无法正确执行,会发生什么。内存管理曾经是编程中的一个关键部分,但现在大部分责任已经自动化。这之所以可行,是因为开发具有自动垃圾回收功能的编程语言的软件工程师始终把这一问题放在心上。
换句话说,自动化在明确谁对其正常工作负责,以及失败状态能够为用户提供足够信息以了解如何进行问题排查时是有益的。鼓励人们忘记自动化的做法会造成责任空白。自动化如果悄无声息地失败或出现不明确的错误信息,至少会浪费大量宝贵的工程时间,最严重的情况是引发不可预知和危险的副作用。
正确地构建错误的东西
在本书的这一章,特别是全书的核心信息是:在没有规模之前,不要为规模做准备。构建一些你之后必须重构的东西,即使这意味着一开始构建一个庞大的单体应用。做错了事情并频繁更新它。
构建“错误”但正确的技术的秘诀在于理解,成功的复杂系统是由稳定的简单系统构成的。在使用负载均衡器、网格网络、队列和分布式计算等技术进行系统扩展之前,简单系统必须是稳定的且具备良好的性能。灾难通常源自于试图立刻构建复杂系统,而忽视了决定所有系统行为(包括计划内和意外情况)基础的稳定性。
估算你的系统能承受多少复杂度的一个好方法是问自己:这个团队有多大?每增加一层复杂度,就需要一个监控策略,并最终需要人来解读监控器所提供的信息。每个服务至少需要三个人。为了便于讨论,一个服务是指具有自己代码库的子系统(尽管谷歌著名地将所有源代码存放在一个单一的代码库中),拥有专用资源(无论是虚拟机还是独立容器),并且假设它与系统的其他组件是松耦合的。
最小的值班轮换人数是六人。所以,一个有六人的大型服务可以有独立的值班轮换,或者两个小型服务可以在他们的团队之间共享一个轮换。当然,人员可以同时属于多个团队,或者同一个团队可以运营多个服务,但一个人不可能精通无限数量的主题,所以每增加一个服务,预计专家水平会减半。一般来说,我更倾向于不让工程师负责超过两个服务,但如果服务相关,我会做出例外。
我列出这些限制只是为了给你提供一个框架,帮助你思考你的系统所依赖的人的能力,以确保系统的未来可扩展性。如果你愿意,可以根据你的实际情况调整具体数字。工程师们的倾向是将系统设计得具有无限扩展性。许多团队在没有足够团队支持的情况下,会根据谷歌或亚马逊的白皮书来建模他们的系统,而忽视了他们并没有一个能够维护谷歌或亚马逊规模的团队。团队所能支持的人力资源,就是系统复杂性的上限。不可避免地,团队会增长,系统的使用也会增长,许多这些架构决策将不得不进行修订。这是可以接受的。
这是一个例子:服务 A 需要向服务 B 发送数据。维护整个系统的团队大约有 11 个人。四个人负责运营,维护服务器并构建工具来帮助执行标准。四个人在数据科学团队,设计模型并编写代码实现这些模型,其余的三个人负责构建 Web 服务。这个三人团队维护服务 B,同时还维护系统中的另一个服务。数据科学团队负责维护服务 A,同时也负责另外两个服务。
这两个团队的人员配置稍显过载,但系统的使用率较低,因此压力并不大。
那么,服务 A 应该如何与服务 B 通信呢?
第一个建议是建立一个消息队列,使得 A 与 B 之间的通信解耦并具有弹性。这将是最具可扩展性的解决方案,但也需要有人来设置消息队列和工作节点,监控它们,并在出现问题时进行响应。哪个团队负责这一切呢?持悲观看法的工程师可能会说是运维团队。通常,当团队无法支撑他们所构建的东西时,就会发生这种情况。系统的某些部分被抛弃,唯一关注它们的团队通常是负责基础设施的团队(通常只有在出现重大问题时才会关注)。
虽然消息队列更具可扩展性,但一个更简单的方案,结合更紧密的耦合,可能在开始时会获得更好的结果。服务 A 可以直接向服务 B 发送 HTTP 请求。责任的委派已经内建。如果错误发生在服务 B 端,服务 B 的团队会被通知。如果错误发生在服务 A 端,服务 A 的团队会被通知。
那么,网络问题怎么办呢?的确,网络有时会出现故障,但如果我们假设这两个服务都托管在一个主要的云服务提供商上,那么导致没有其他问题的单次网络故障的可能性很小。网络问题并不微妙,通常是配置错误造成的,而不是外部干扰因素。
HTTP 请求方案是正确的错误,因为将服务 A 和服务 B 之间的 HTTP 请求迁移到消息队列是简单的。虽然我们暂时失去了内建的容错能力并承受了更高的扩展负担,但它创建了一个当前团队更容易维护的系统。
反例是如果我们交换 HTTP 请求的顺序,让服务 B 轮询服务 A 以获取新数据。虽然这比消息队列简单,但它不必要地消耗资源。服务 A 并不是持续产生新数据,通过轮询服务 B,它可能花费数小时甚至数天发送没有意义的请求。从这种方式转变为使用队列将需要对代码进行重大修改。以这种错误的方式构建系统几乎没有什么价值。
反馈回路
另一种思考方式是描绘出维护这个系统如何在工程中产生反馈回路。从工作流、延迟、库存和目标的角度思考工作完成的方式,有助于澄清维护某一复杂系统所需的工作量是否可行。
让我们再看看服务 A 和服务 B 的问题。我们知道有七个人在这两个服务上工作,并且每个人都有八小时的工作日。服务 B 的团队在这些服务和另一个服务之间分配,所以我们可以假设他们在每个他们负责的服务上有四小时的预算。三个人就是每天大约 12 小时。服务 A 的团队维护着总共三个服务,所以他们每个人的预算是 2.5 小时,每个服务每天 10 小时。像这样的模型可能具有以下特点:
-
库存 库存 是任何能够随着时间积累或消耗的元素。传统的系统模型示例是一个装满水的浴缸。水就是库存。在这个模型中,技术债务会持续不断地为每个服务积累,无论工作量多少。债务将通过花费工作时间来偿还。我们的工作周中的任务也是一种库存,团队在操作时会将其消耗掉。那八小时的工作日也是一种库存。当系统稳定时,每天的八小时将被完全消耗并完全恢复。
-
流动 流动 是增加或减少库存的变化。在浴缸的例子中,水龙头流出的水速是流动,如果排水口打开,水从浴缸流出的速度就是另一个流动。在我们的模型中,任何时候,人们可以工作超过八小时,但这样做会降低他们的最终生产力,并且要求他们在之后的某个时候每天工作少于八小时。我们可以通过假设我们为第二天的预算借用了额外的时间来表示这一点。任务通过消耗工作时间来完成;我们可能简化模型,假设每个任务都值一个小时,或者我们可以将任务分为小、中、大不同的大小,并为每种选项设置不同的工时成本。花费工作时间会减少技术债务或工作任务的库存,具体取决于这些小时是如何使用的。
-
延迟 好的系统模型承认并非所有事情都是瞬时发生的。延迟表示流动反应中的时间间隙。在我们的模型中,新工作并不会立即取代旧工作;它是按一周一周计划并分配的。我们可以将每个任务分配之间的时间视为一种延迟。
-
反馈 反馈回路 形成于库存变化影响流动性质时,无论是正面还是负面。在我们的模型中,当人们工作超过总共八小时的预算时,他们会失去未来的时间。工作时间越长,他们为了维持连续八小时的工作日就必须借用更多的时间。最终,他们需要休息以恢复正常。或者,他们可以通过将更多预算花费在服务 A 或服务 B 上来借用时间,但这意味着他们负责的其他服务将被忽视,技术债务将不断积累。
从视觉上看,我们可以像图 10-1 中那样表示这个模型。实线表示流动,虚线表示影响流动速率的变量。
工作时间通过我们的日程安排进入模型,但会受到代表职业倦怠的库存的影响。如果职业倦怠很高,工作时间会减少;如果工作时间很长,职业倦怠则会上升。我们在某一天能够用于某项服务任务的工作时间,取决于我们的团队规模以及团队在同一时间内需要维护的服务或项目的数量。我们能投入到工作任务中的时间越多,完成的任务就越多。当工作任务完成后,剩余的额外时间会用于改善我们的技术债务。

图 10-1:团队工作负载中的反馈循环
尽管这个视觉模型看起来只是一个插图,但我们实际上可以为其编程,并用它来探索我们的团队在不同条件下如何管理工作。系统思维者常用的两个工具来实现这些模型是 Loopy(ncase.me/loopy/)和 InsightMaker(insightmaker.com/)。这两个工具都免费且开源,允许你尝试不同的配置和交互。
现在,让我们思考几种场景。假设我们有一个冲刺任务,其中 Service A 和 Service B 各有 24 小时的工作任务。这应该没问题;Service A 的团队每周有 50 小时的周容量来处理 Service A,Service B 的团队每周有 60 小时的周容量来处理 Service B。对于 24 小时的冲刺任务,每个团队都有足够的额外时间来消化技术债务。
但是,如果一个冲刺有 70 小时的工作量,会发生什么呢?Service A 的团队可以处理这种情况,如果团队中的四个人每个人从下周借五小时出来,但团队将没有时间管理技术债务,并且下周他们只会有 30 小时的时间来处理 Service A。
如果 70 小时的工作量成为冲刺的常态会怎么样?团队将逐渐疲惫不堪,无法重新思考系统设计或管理他们的技术债务。这个模型是不稳定的,但我们可以通过以下方法恢复平衡:
-
团队将他们的一项服务的所有权转交给另一个团队,从而使他们每天可以花更多时间在 Service A 或 Service B 的任务上。
-
团队允许技术债务在他们的一个或所有服务上累积,直到某个服务失败。
-
团队越来越努力工作,直到成员们感到疲惫不堪,届时他们将无法继续工作一段时间。
团队可能会采取的一种方式是,改变设计,使得集成模式能减轻服务 A 的低容量团队的工作量。假设,服务 B 不再通过 HTTP 连接,而是直接连接到服务 A 的数据库来获取所需数据。服务 A 的团队将不再需要构建一个接收来自服务 B 请求的端点,这意味着他们可以更好地平衡工作负载,管理维护责任,但这种模式会以整体架构质量的牺牲为代价达到平衡。
如果你是 Fred Brooks 的《神话般的工程人月》的读者,你可能会对这个模型的前提提出异议。它暗示其中一个可能的解决方案是增加团队成员,而我们知道软件并不是通过人小时来成功构建的。更多的人并不会让软件项目进展得更快。
但是这种模型的核心并不是规划路线图或预算人员数量,而是帮助人们将工程团队视为一个相互连接的系统。糟糕的软件就是没有得到维护的软件。面向未来意味着不断地重新思考和迭代现有系统。人们在构建服务时并不会认为他们会忽视它,直到它成为组织的巨大负担。人们未能维护服务,是因为没有给予足够的时间或资源来进行维护。
如果你大致了解一个平均迭代周期中的工作量以及团队人数,你就可以推测该团队是否有可能成功地维护 X 个服务。如果答案是否定的,那么架构设计可能对当前的团队来说过于复杂。
不要停下公交车
总结来说,系统的老化有两种方式。其使用模式发生变化,要求进行规模的扩展或收缩,或者支撑它们的资源逐渐退化,最终导致它们失败。遗留系统现代化本身就是反模式。一个健康的组织运行一个健康的系统,应该能够随着时间的推移不断演进,而不需要重新调整资源去进行正式的现代化努力。
然而,要实现这种健康的状态,我们必须能够看到我们所构建的系统系统的层次和层级关系。我们的技术系统由必须保持稳定的小型系统组成。我们的工程团队也表现为另一个系统,建立反馈回路,决定他们可以投入多少时间和精力来进行技术演进所需的升级。工程系统和技术系统不是相互独立的。
曾经有一位高级主管告诉我:“你说得对,马里安,这个安全漏洞很严重,但我们不能停下‘公交车’。”他的意思是,他不想投入资源来修复这个问题,因为他担心这样会拖慢新开发进度。他是对的,但只有在组织忽视这个问题两三年之后,他才是对的。如果他们在问题被发现时就解决了,可能只需要最低的投入。相反,问题随着工程师将不良代码复制到其他系统,并在其上构建更多内容,逐渐扩大了。他们面临着一个选择:减慢“公交车”的速度,还是等待“公交车”的车轮掉下来。
组织选择尽可能快地推动“公交车”前进,因为他们看不见所有的反馈回路。发布新代码引起了关注,而技术债务则默默累积,毫无声息。导致系统失败的不是它的使用年限,而是组织对它的忽视,逐渐积累的压力最终导致了爆炸性后果。
第十一章:结论
传统现代化的难点在于围绕系统的系统。组织、其沟通结构、政治和激励机制与技术产品交织在一起,以至于要推动产品发展,必须通过转动这个复杂且未记录的系统的齿轮来实现。
传统现代化失败的部分原因是,人类往往被激励去静音或去除建立责任制的反馈循环。我们通常无法停止这种行为,因为我们坚持将这个问题看作道德上的失败,而非无意识的偏见。例如,保持运维与开发之间分离的工程组织,最终发现他们的开发团队会设计出这样的解决方案:当出问题时,首先且最严重地影响到运维团队。与此同时,运维团队则构建了会阻碍开发的流程,将负面后果传递给开发团队。这些都是静音反馈循环的例子。决策的实施者无法像其他团队那样直接感受到决策的影响。
DevOps 和 SRE 运动对软件开发产生如此有益影响的原因之一是它们旨在重新确立责任制。如果产品工程团队参与运行和维护自己的基础设施,他们就是那些感受到自己决策影响的人。当他们构建的东西无法扩展时,他们会在凌晨 3 点收到警报。让软件工程师负责基础设施的健康,而不是由独立的运维团队负责,这样可以让反馈循环变得更加清晰。
但任何曾经尝试运营 SRE 或 DevOps 团队的人都会告诉你,让产品工程团队对其基础设施负责的期望,确实说起来容易做起来难。基础设施方面始终需要专业人才——无论是因为组织自己运营数据中心并需要硬件专长,还是因为工程师在维护基础设施时使用的工具本身也需要维护——因此,总会有人把责任推给他们。
人们并不是因为不在乎而静音反馈环路。人们之所以静音反馈环路,是因为人类一次只能在大脑中处理有限的信息。保持反馈环路开启意味着要倾听来自它的信息,这意味着首先要考虑可能回传的信息以及如何解读这些信息。开发人员静音操作通常是因为他们通常不了解基础设施如何运作的细节。工程师通常会静音来自业务方面的反馈环路,因为这些反馈以他们没有接受过培训的指标形式传递,他们很难从中提取洞察力。每个群体都有能力学习另一方的语言,但一个人应该掌握多少学科才能完成她的工作呢?在运行系统时,工程师必须考虑资源使用、容量预测、测试覆盖率、继承结构、代码行数等更多因素。难怪大多数人将自己的工作范围限制在与其实际工作直接相关的庞大技术复杂性之内。
一个高效运作的组织不能始终保持所有反馈环路开启。它必须决定哪些反馈环路对运营卓越的影响最大。在本书中,我强调了从增值的角度来思考现代化项目,而非从技术正确性的角度,因为这重新建立了最重要的反馈环路:这项技术是否服务于用户的需求?
会议、报告和对话是效率最低的反馈环路。反馈环路最有效的方式是当操作员感受到其影响时,而不仅仅是听说其影响。这是因为人们天生倾向于误解信息,以符合他们已经想要相信的东西。当反馈以不便、干扰、打断和额外工作等形式传递时,人们更难做出这种曲解。
尽管如此,由于我们不能始终保持所有反馈环路开启,传统沟通可以在影响不足以需要开启环路时起到填补作用。设计现代化工作是一个迭代的过程,而迭代本身就是关于反馈的。因此,遗留系统现代化的真正挑战并不是技术任务,而是确保在关键地方保持反馈环路开启,并且在其他地方确保沟通有序。
一般而言,决策的裁量权应该委托给那些必须执行这些决策的人。如果你不是在贡献代码或在深夜被叫醒回答故障警报,请记住,不论你的工作有多重要,你都不是实施者。你不操作系统,但你可以找到操作员,并确保他们有成功所需的支持。授权操作员。
这应该不言而喻,这需要信任。团队最终是由信任所主导的。大组织的领导者不喜欢听到这一点,因为这意味着他们将对超出自己控制范围的结果负责。依附于提供保证的流行策略更容易。这样,如果失败了,可以将其归结为不可避免的偶然事件,没有任何领导者能预防。这是遗留系统现代化的现实,没有灵丹妙药;本书中的每一种技术在不适当的条件下都可能失败。我已经尽力描述了正确的条件和每种方法的脆弱性,但我的知识和经验是有限的(并且永远不会是无限的)。最有可能找到有效策略的人,是那些在现场观察系统运转的人。
接受失败后,面对这种现实会变得更容易。试图改变生产中的复杂系统时,失败是不可避免的。系统中有太多的移动部分,太多未知的因素,太多需要修复的地方。让每一个决策都正确是不可能的。现代工程团队使用服务水平目标、错误预算和平均恢复时间等统计数据,将重点从避免失败转移到快速恢复上。不要忘记:完美的记录总是会被打破,但韧性是一项持久的成就。作为一个组织接受失败,能够减少赋能操作员的风险,并能从工程师那里获得更好的表现。
我们无法完全消除失败,因为在某个复杂度级别上,一个人——无论多么聪明——都无法理解整个系统。对于遗留系统,我们还有一个额外的复杂性,那就是系统的一些上下文已经丢失。需求、假设和当时的技术限制都没有文档化。平台中可能有一些抽象层次会让我们绊倒。现代化团队需要重新发现原系统的需求和假设,并将其更新到新系统中,但即使是最优秀的现代化团队,也有限制于能够挖掘出多少理解。归根结底,旧的软件不能作为新版本的规格说明。
技术,本质上,是人类思维的产物。因此,在现代化旧技术时,人类的思维至关重要。软件工程师很聪明,但他们和其他职业一样,会受到趋势和时尚的影响。要关注他们的激励机制。是什么让他们获得同行的认可?是什么让他们被关注,他们最终会优先考虑这些,即使这些行为与管理层的正式指示公开冲突。技术发展是一个周期过程,旧的范式不断被翻新以捕捉被忽视的市场细分。更新不一定意味着更好。好的技术并不是拥有最现代、最具可扩展性、最快或最安全的实现,而是服务于用户的需求。
但是,我们也希望有一个软件工程师努力让技术变得更快、更好、更安全的世界。我们获得既能服务用户又不断追求改进的技术的唯一方式,是事先定义成功的标准。什么是创造价值,如何知道价值何时增加?
最终,技术从未完成建设。今天的遗留现代化项目是昨天的已完成系统。计算机系统不能期望几十年不发生变化,因为计算机很少能够与外界隔离。输入会变化,输出方式会变化,网络和协议会变化,而不变化的程序最终会变成定时炸弹。
处理遗留现代化项目的最佳方法是,首先不需要它们。如果为此预算了适当的时间和资源,系统可以以这样的方式进行维护,使其随行业发展而演进。那些成功做到这一点的组织最终明白,组织的规模是系统复杂度的上限。那些比团队所能维护的更复杂的系统会被忽视,最终崩溃。
对大多数软件工程师来说,遗留系统可能看起来像是令人痛苦的死胡同工作,但现实是,不再使用的系统会被关闭。处理遗留系统意味着处理一些最关键的系统——这些计算机以无数种方式影响着数百万人的生活。这不是技术清洁工的工作,而是战场外科医生的工作。能在他们中间服务,是我最大的荣幸。


浙公网安备 33010602011771号