一行代码,“葬送”了 5 亿欧元:史上最贵 Bug 之一!

科技   2024-08-15 12:18   福建  

本文经授权转自公众号CSDN(ID:CSDNnews)

作者 | DR MILAN MILANOVIĆ,翻译 | 郑丽媛

【编者按】在编程世界中,每一个微小的细节都可能成为决定成败的关键,甚至一个小 Bug 都可能引发灾难性的后果。本文作者将讲述一些关于技术、责任以及在现代工程中微小细节重要性的故事,以此提醒我们:在追求科技巅峰的过程中,即便是最微小的疏忽也可能带来无法估量的损失。

原文链接:https://newsletter.techworld-with-milan.com/p/how-a-single-line-of-code-brought


当我们谈论软件时,心里要知道:如今它在各类技术中都处于最底层支撑,其重要性不言而喻。例如在风险极大的航空航天领域,软件故障就可能导致灾难性后果。接下来,本文将分析一些典型案例,看看在这些不容许出错的高风险环境中,软件问题会造成多么重大的影响。

1、案例 1:阿丽亚娜 5 号(Ariane 5)火箭爆炸

1996 年 6 月 4 日,欧洲航天局(ESA)首次发射了阿丽亚娜 5 号火箭,这标志着太空探索史上的一个重要时刻。然而,这次的任务十分艰难:仅因一行代码导致灾难性故障,价值近 5 亿欧元的火箭在发射 37 秒后发生爆炸。

本来,那次阿丽亚娜 5 号的发射目标是将两颗通信卫星送入地球静止转移轨道。发射初期一切顺利,但火箭很快便偏离轨道并在飞行 37 秒后爆炸。经过调查,事故的根本原因是制导系统存在软件缺陷,而该系统负责调整火箭的飞行路线。迄今为止,阿丽亚娜 5 号仍被公认是史上最贵的软件故障之一。也是这次爆炸导致了科学卫星的毁坏,从而使得对地球磁层的科学研究推迟了近 4 年。

问题到底在哪里?事故源于上一代阿丽亚娜 4 号中的一段死代码(即程序中不会被执行到的代码),这段代码始于近十年前,其中包含了一个简单且可修复的编程错误。具体来说,火箭使用了一种称为“水平偏差”(也称为 BH 值)的方法来确定其指向是朝上还是朝下。该值由一个 64 位浮点变量来表示,然后由制导系统将其转换为 16 位带符号整数进行传输。

有一点需要注意:64 位浮点变量可以表示数十亿个数值,而 16 位只能表示 65,535 个数值。如果把首位用来存储符号(正/负),那么 16 位带符号整数的范围就是 -32,768 到 32,767。然而,浮点数用相同位数可以覆盖更大的数字范围,从 -1.8e+308 到 -2.2e-308。

也就是说,如果你把一个浮点数转换为 16 位带符号整数,那它将大幅超出其范围——于是在阿丽亚娜 5 号的代码中,便出现了一个常见的整数溢出问题。

如果按照阿丽亚娜 4 号的设定不变,这段代码也不会出现问题,但阿丽亚娜 5 号采用了比之前更陡的轨迹,从而导致了极高的垂直速度——于是代码出现了问题,制导系统持续向主计算机发送错误消息,主计算机误认为火箭已严重偏航,便触发了自毁机制。

从这次事件中得到了哪些启示?

(1)复制代码却不理解其含义是一个重要问题。

(2)缺乏适当的异常处理。

(3)忽略了用户需求的变化。

(4)缺乏适当的测试。

基于以上,当时调查委员会官方给出了一些相关建议:

(1)避免使用不需要的程序或系统。在飞行过程中,除非必要,否则软件不应运行。

(2)测试至关重要。用尽可能多的真实设备建立一个测试设施,添加真实输入数据,进行彻底的闭环系统测试。所有任务必须在完全实现模拟和高测试覆盖率下进行。

(3)进行代码审查。在任何编程语言中,所有代码细节都至关重要。

(4)通过处理任务中的异常和创建备份系统来提高可靠性,确保在出现问题时也能顺利运行。在定义关键组件时,必须考虑到软件故障的情况。建立一个团队,制定严格的软件测试规则,确保高质量标准。

2、案例 2:未捕获的 SQLException 使航空公司停飞

在路透社最近报道的一起事件中,美国联邦航空管理局(FAA)提到,他们在审查中发现有一位工作人员在进行主数据库和备份数据库之间的同步工作时,“无意删除了文件”,导致 1 月 11 日当天美国 11,000 多个航班停飞。

航空公司原本计划在其核心系统所服务的数据库集群上进行故障转移,该系统采用面向服务的架构,按照功能特性来分阶段推出。该系统处理搜索请求,可根据不同输入(日期、时间、城市)返回航班详细信息列表。

据悉,该航空公司运行在一组 J2EE 应用服务器集群上,该集群带有冗余 Oracle 数据库和冗余硬件负载平衡器,这在当时是一种非常常见的高可用性架构。那一天,工程师手动将数据库故障从数据库 1 转移到数据库 2(备份数据库)。他们曾这样做过很多次,刚开始一切都按计划进行。然而两小时后,系统停止响应请求,导致其自助终端无法工作。经过一番分析后,他们决定重启应用程序——这个操作解决了问题,却导致航空公司停飞了约 3 小时。

在对日志文件、线程转储和配置文件进行事后分析后,他们发现问题出在核心系统,而不是报错的系统。许多线程被阻塞,只为等待一个从未出现过的响应。他们发现,在最后一个块中运行的 SQL 关闭语句,在驱动程序试图告诉 DB 释放资源时,也会抛出 SQLException,而这个问题没有得到处理,从而造成了资源池耗尽。

在那种情况下,工程师怎样才能做得更好?当然,要预防所有 Bug 是不可能的,有些 Bug 总会不经意发生,但我们能做到的是避免一个系统中的 Bug 影响到另一个系统。

3、案例 3:波音 737 MAX 的灾难

2018 年 10 月和 2019 年 3 月,两架波音 737 MAX 飞机在五个月内相继坠毁,共导致 346 人遇难。造成这个事故的部分原因,是一个旨在提高飞行安全的软件系统。

这个系统名为 MCAS(机动特性增强系统),是处理波音 737 MAX 危机情况的核心,是波音公司为解决基本设计挑战而提出的一种解决方案:由于 737 MAX 配备了更大、更省油的发动机,其空气动力学特性与前代机型不同,因此 MCAS 的设计初衷是让 737 MAX 的操作与早期的 737 机型一致,确保飞行员能够无缝过渡。

理论上来说,MCAS 是一个不错的解决方案。它可以自动调节水平稳定器,防止飞机在急转弯或低速飞行时失速。但实际上,它却成了“单点故障如何演变成灾难”的教科书式案例。

关键 Bug 是什么呢?MCAS 依赖于飞机两个迎角传感器中的一个输入数据。如果这个传感器提供的数据不正确,MCAS 系统就会不必要地启动,甚至在不应该启动的情况下将飞机机头向下推。更严重的是,该系统会反复重启,这可能会让对该系统缺乏足够了解的飞行员不知所措。

不过关于 MCAS 的故事并不完整,因为在空难发生后,有一个发现浮出了水面。波音公司被曝将 737 MAX 的大部分软件开发工作外包给薪酬低至每小时 9 美元的工程师。根据前波音软件工程师的说法,公司越来越依赖于海外软件开发中心雇佣的临时工,其中不乏刚毕业的大学生。这个决定很可能是迫于削减成本和加快开发进度的压力,为 737 MAX 的悲剧又增添了一层复杂性。

作为软件工程师,我们可以从这个故事中学到什么?

(1)消除单点故障。MCAS 的悲剧给了我们一个强烈的提醒,为什么冗余在关键系统中至关重要。依赖单一数据源(在本例中为一个迎角传感器)造成了一个 Bug,并带来了灾难性的后果。因此在工作中,我们必须经常问自己:“如果这个组件失效会怎样?”

(2)保持软件和系统简单,但不要过于简单(KISS)。MCAS 系统就是一个过度工程化的典型例子。波音公司没有从根本上解决 737 MAX 设计的不稳定性,而是选择了软件“修复”,从而带来了新的 Bug。

(3)缺乏领域专业知识。许多外包公司的工程师,并没有关于航空航天工程和安全关键系统的深入经验,这就容易导致沟通问题和需要多轮修改的错误。

(4)糟糕的测试方法。尽管进行了广泛的测试,但 MCAS 的致命 Bug 在飞机投入使用前并未被发现。这就提出了一个重要问题,即当前的测试和模拟实践是否充分,特别是对于那些在现实世界条件下难以完全复制的系统。

那么作为软件工程师,我们每天可以做些什么来改善上面这些问题呢?

(1)实施适当的错误处理。在设计系统时,应假设组件会失效,实施冗余、交叉检查和故障安全机制,确保在某个组件真的失效时,系统仍能正常运行。

(2)关注用户。确保系统能够清晰地与用户沟通,尤其是关于当前状态和任何自动化操作的信息。为用户提供所需的信息和控制,方便他们在必要时做出明智的决策并在必要时覆盖自动化系统。

(3)优先考虑集成测试。单元测试虽然很重要,但这还不够。投入时间和资源进行全面的系统级集成测试,特别是测试边界情况和故障模式,确保系统在各种条件下都能稳定运行。

(4)培养开放的沟通文化。营造一种人们可以毫无顾虑地提出问题的环境,并将“失误的情况”视为一种宝贵的学习机会,而不是应该被掩盖的尴尬事件。通过这种方式,可以早期发现并解决潜在的问题。

(5)倡导合理的资源分配。作为软件工程师,应争取完成工作所需的资源。这包括反对不切实际的截止日期、不充足的测试时间或可能危及安全的成本削减措施。我们要保证有足够的时间和资源进行充分的测试和验证,以此确保系统的可靠性和安全性。

本文转自公众号“CSDN”,ID:CSDNnews

---END---

Linux学习
专注分享Linux/Unix相关内容,包括Linux命令、Linux内核、Linux系统开发、Linux运维、网络编程、开发工具等Linux相关知识和技术
 最新文章