对于我们身边的每件事,从买杂货到与家人和朋友聊天,我们越来越依赖软件。就像我们不希望我们的汽车在下次旅行中出现故障一样,我们也无法容忍软件在某些情况下出现故障。更新一个库不应该破坏你的系统,而考虑到它们应该能够在自然环境中运行数年,不需要任何外部管理或维护,一个软件应该(尽可能地)按照设计桥梁的方式来设计。
本文最初发布于 Alfonso de la Rocha 的个人博客,由 InfoQ 中文站翻译并分享。
首先,我要在这篇文章中分享的想法可能不是每个人都喜欢,但我希望大家就这个话题展开讨论。
假设你想使用你最喜欢的编程语言为你的系统构建一个代理服务器,你该怎么办?首先选择自己比较熟悉的 Web 框架,这样就有了一个项目脚手架,然后就可以直接开始实现了。完成这个简单的步骤后,你就可以启动服务器并立即接收 HTTP 请求。在服务器中,你希望根据请求的源地址和内容体对请求进行身份验证和流量过滤。因此,除了 Web 服务器框架之外,你还得选择并安装一个中间件来处理身份验证,以及一个库来简化新请求的处理和过滤(现在不是 90 年代了,你不希望重新发明轮子,自己开发所有这些东西,你希望在两到三天内就让你的系统在生产环境中运行起来)。你可以使用一些胶水代码将所有这些部分粘结在一起,然后做一些单元和集成测试,检查一切是否正常,然后就可以开始下一步的工作了。
行动要快,基础要稳
你知道上述方法的问题吗?项目 80%的代码都是别人的,来自项目使用的外部库(见上图),20%的代码是自己的(还有一些来自 StackOverflow 的“灵感”),你自己的代码是作为胶水和粗略的定制,使你的用例可以使用那些库。这种做法是发展趋势,不要误会我的意思,如果你正在构建的是一项众所周知的技术,或者是一个辅助系统(如你个人的 CMS),或者是一个内部报表系统,就该这么做,你很快就可以让什么东西运行起来,你会尽快从你的系统受益。当你在关键系统或科技公司平台(有数百万用户)的核心实现上继续采用这种方法时,问题就真正出现了。相信我,无论公司的规模大小,都会是这样(问下他们那些由咨询公司开展的项目和人力投入——70%是应届毕业生)。
软件开发确实受到了“快速行动,打破常规”哲学的影响。这很好,我完全同意尽早测试新想法,快速迭代验证概念,并在开发中尽可能做到“精益”。软件原型设计成本很低,为什么不呢?这就是创新,对吧?当你开始交付伟大的项目和绝妙的想法时,问题就出现了,它们开始破坏生产环境,伤害你的用户。甚至 Facebook 也意识到,是时候从“快速行动,打破现状”转向“行动要快,基础要稳”了。
你永远不会期望一个土木工程师在对他的概念验证进行了几次修改后,就把他的桥(一个关键的基础设施)“完工”了。更重要的是,桥梁和土木工程项目的设计是永久的,为什么软件的设计也要是永久的?正确地设计一座桥需要花时间,并且对其原理(材料、结构、弹性等)有一个良好的理解。如果我们想要把软件工程视为一种工程实践(它值得),我们就需要开始构建健壮的、有弹性的和长久的系统,并且不要草率地在我们的项目中加入任何我们偶然遇到的代码片段。当然,我要为我的极端说法道歉,我并不是说所有的软件都是草率开发出来的。前几天,我读了 SpaceX 这篇关于软件工程的文章,我真的很喜欢他们使用软硬件冗余来最小化像航天飞机这样的关键系统中的潜在错误。我是说,让我们都更像航天飞机软件工程师一点。
在StackExchange网站的一条回复中,有人还讲述了他们与SpaceX团队在GDC 2015⁄2016上的互动。他们谈到了三重冗余系统以及SpaceX如何使用Actor-Judge系统。简而言之,有3个双核ARM处理器运行在定制的板上(根据elteto)。对于每个决策,一个“flight string”会比较单个处理器上每个核心的结果。如果输出匹配,则向不同的控制器发送命令。处理器有3个(双核),这意味着每个控制器/传感器将收到三个不同的命令。然后,控制器充当裁判,比较这三个命令。如果三个命令一致,它们就执行操作。如果有一个命令不一致,那么控制器会执行那个来自之前一直发送正确命令的处理器的命令。
这意味着在任何给定的点有6个飞行软件(flight software)的运行过程。
来源:https://yasoob.me/posts/software_engineering_within_spacex_launch/
正是这些想法促使我决定尝试摆脱一些外部依赖,看看这是否会提升我的效率和代码质量(这也是我发表本文的原因)。
不是停止使用,而是更聪明地使用
当然,我并不主张完全停止使用外部依赖,一切从头开始实现。那太疯狂了!我想说的是,避免使用那些非绝对必要的依赖。例如,当 Go 的 HTTP 标准库给了你构建一个出色的 Web 服务器所需要的一切时,为什么还要使用一个 Go Web 框架呢?(我非常同意这个看法。)这是我几年前开始构建我的第一个 Go REST API 时所遵循的做法的一个完美示例,现在我已经完全放弃了。这就是我所说的“停止滥用依赖”的一个例子。我不需要 Web 框架。是的,它在开始时大大加速了我的开发,但我不知道它的工作原理,在遇到错误或想要提高系统的性能时,我完全无法控制,我会因此犯强迫症的(我在这里简要提及了)。
使用一个框架就像驾驶一辆新车,在高速开出许多英里后,它抛锚了,而你不知道如何修理它。
我绝不会建议你将一些核心依赖项从“经常使用”的列表中删除,因为为你的项目从头编写它们提供的功能将是自杀行为(因为它们是由极具天赋的团队花很长时间开发出来的)。举几个例子,我最近在我的系统里使用了它们,比如 libp2p、Geth 客户端、Polkadot 或 Qiskit(好吧,你可能会从我选择的库中看出了我有点偏向)。如果我的实现中没有它们,我就完了。当然,如果你需要构建 p2p 网络,我不会要求你重写 libp2p。如果你需要从它那里获得“其他东西”,那么因为它是开源的,所以你总是可以大胆地提出添加建议。如果你想要玩下量子计算,那么对于 Qiskit 也同样如此。它们背后都有很棒的开发团队和研究人员致力于最大限度地发挥其产品的作用,所以,只要你知道自己在做什么,以及为什么要使用它们(务必谨记使用它们可能带来的潜在开销),就用吧。
所以我的观点不是“停止使用外部依赖”,而是“明智地使用它们”。如果你已经知道如何使用自己偏好的编程语言所提供的功能来解组和解析 JSON,那么就不要偷懒,不要使用一个没有得到良好维护的 JSON 解析库。你可以避免不必要的开支和麻烦。同样,不要使用你发现的第一个“有效”的库,你应该花一些时间研究可选方案,从而选择一个更符合你需求的,甚至完全放弃使用库,自己编写代码来完成这项任务(同样,对于一座桥所需的材料和地基,土木工程师会做一个彻底的分析)。
停止滥用外部依赖的四大理由
下面我们看看我得出这些结论的原因,以及为什么我尽量避免过度使用外部依赖。至少,你应该知道,使用库存在如下这些风险。
你使用的库和外部依赖可能已经过时、缺少维护,或者/并且存在隐藏的安全缺陷和性能瓶颈。当你是代码的所有者时,你知道自己在做什么,但是当你使用其他地方的代码时,你不知道开发人员是否犯了错误(如果你不阅读和理解你添加到项目中的代码)。你不知道他是否使用了低效的实现,或者如果你不检查源代码,也不知道他是否添加了恶意代码。更重要的是,如果开发者放弃了这个库,而你在未来的系统中还需要依赖它,那该怎么办?那就需要你了解所使用的代码。下面这段话完美地说明了使用外部依赖的风险:
攻击者经常使用社会化工程将他们的包放入应用程序中。他们创建一个具有有用特性的包,然后偷偷加入一些恶意代码。一旦代码进入应用程序,用户启动应用程序时,这些代码就会攻击用户。
从今年3月开始,一名黑客就用这种方式窃取了价值150万美元的加密货币。
第0天(3月6日):攻击者在npm上发布了一个模块electron-native-notify。这似乎很有用——它帮助Electron应用以一种跨平台的方式触发本机通知。那时,它还没有任何恶意代码。
第2天:为了完成盗窃,攻击者必须将这个模块放入加密货币应用中。他们选择的媒介是一个帮助用户管理其加密货币的应用程序Agama Wallet的一个依赖项。
第17天:攻击者添加恶意载荷。
第41到66天:重新构建应用程序,加入该依赖项的最新版本,其中包括electron-native-notify。此时,它开始向服务器发送用户的“种子”(用户名/密码组合)。然后,攻击者就可以利用这些种子掏空用户的钱包。
第90天:用户向npm报告了electron-native-notify中的可疑行为,然后他们通知了加密货币平台,后者将资金从易受攻击的钱包转移到了安全的钱包。
摘自:https://bytecodealliance.org/articles/announcing-the-bytecode-alliance
有很多这样的例子可以说明不加选择地使用库所带来的风险(快速搜索一下,看看这是怎么发生的)。
库会显著增加代码的大小和编译时间。这篇博文很棒,生动地说明了Rust的问题。我强烈推荐大家阅读一下。你可能会遇到这样的场景:你包含一个完整的库来执行某项任务,但最终使用的是整个库的一个函数。或者,在package.json中有些依赖项,你在开发和试验解决方案时用到了但最后忘了清理。所有这些都对你不利。
库可能会向你隐瞒解决方案中的许多权衡、设计决策和潜在的故障点。与信任各种外部依赖项相比,在系统所有的基本代码都由你完全设计并实现时,识别潜在的攻击媒介要容易得多。
最后,在系统中不使用任何库是一种挑战和乐趣。当然,这并不是一个令人信服的理由,但即使你不同意我的观点,我也建议你一生中至少要这样设计一次。你会看到在这个过程中你学到了多少东西,如何更好地掌握和理解你所选择的编程语言,以及完全控制你的系统、任何时候都可以清楚地了解你的系统在做什么是多么得令人心旷神怡。我要说的是,我曾经是那种拆开祖父的收音机来了解它是如何工作的孩子。
软件设计应具永久性
我们需要开始构建有弹性的软件。我们越来越意识到,这是智能合约和分布式系统领域的一个趋势。在这类系统中,网络中大量的用户共享并执行相同的基本代码。因此,软件故障或安全缺陷不会只影响一个人的基础设施,而是影响系统中的每个人。已经有很多这样的案例报道,智能合约的失败造成了数百万美元的损失,客户软件中的漏洞危害了他们的系统。
对于我们身边的每件事,从买杂货到与家人和朋友聊天,我们越来越依赖软件。就像我们不希望我们的汽车在下次旅行中出现故障一样,我们也无法容忍软件在某些情况下出现故障。更新一个库不应该破坏你的系统,而考虑到它们应该能够在自然环境中运行数年,不需要任何外部管理或维护,一个软件应该(尽可能地)按照设计桥梁的方式来设计。
我们应该开始编写一份弹性软件宣言吗?我不知道,但是让我们开始讨论这个问题。同时,我很想听听你的想法。
其它参考资料
下面这些资料也可以证明我的观点:
软件祛魅。如果你必须从这个列表中选择一篇阅读材料的话,那就选这篇吧。它完美地补充了我的观点(我是在本文写完时发现的这篇文章,所以我的讨论没有加入其中的许多思想)。
查看英文原文:
https://adlrocha.substack.com/p/adlrocha-software-should-be-designed
评论