译者 | 刘汪洋
审校 | 重楼
“错误是成长的阶梯”和“失败乃成功之母”——这些谚语为我们在犯错时提供慰藉。程序员热衷于创新,对追求新技术趋势保持着高度的热情,这就要求他们必须不断学习。基于这些观点、虚构的情节,再加上我的七年程序开发经验以及与同行的交流,我认为程序员经常会犯错。
为了发现或预防这些错误,我们采取了自动化测试、代码审查、环境隔离和灰度发布、执行数据备份、与质量工程师合作,还有利用多种工具来尽早发现问题。
即便实行了这些预防措施,偶尔还是会有漏洞在测试环节被漏掉,进而进入生产环境。这时候,我们该怎么办?我们会迅速定位问题所在,进行修复,并尽快将修改部署到生产环境。我们的目标是尽可能降低受影响用户的数量。另一方面,错误在生产环境中存在的时间越长或影响越大,就越能引起公司内部更多的讨论。
然而,某些错误因其巨大的规模或特殊的情况,不止在经历过这些错误的人群中引发讨论。这些错误可能会成为新闻头条、报纸的热门话题,有些甚至拥有独立的维基百科页面,一些错误发生 60 年后也仍然历历在目。在本文中,我们将探讨一些这类重大错误,了解它们是如何发生的,以及我们能从中学到什么。
Mariner 1:世界上最昂贵的破折号错误
1962 年,NASA 发射了首艘前往其他行星的探测器 Mariner 1,计划飞往金星并测量其温度、磁场等科学家感兴趣的数据。然而意外发生了:发射不久后,探测器开始偏离预定轨道。为了避免对地球造成潜在的风险,最终决定启动自毁程序。
人们普遍认为,这次失败是由编程错误导致的。具体来说,是由于代码中缺少了一个破折号。事实是什么呢?在代码中,存在一个涉及符号“R”代表半径的数学运算。正确的表示应为平均半径“R-bar”,在物理学中表示为 R̄。然而,这不仅仅是关于“一个破折号”的问题。这个错误导致销毁了价值两千万美金航天器。
既是一个物理和数学上的错误,为何仍然被归结为编程错误呢?这个错误最初出现在计算过程中,程序员只是将其转换成了代码。在理想状态下,这种错误应该通过测试发现,或者在程序员与数学家或物理学家的合作中被识别。程序员经常为不同领域编写代码,虽然不用成为该领域专家就可以开发应用,但掌握基础知识并和该领域的专家合作是很重要的,这样才能开发出可测试的代码场景(最好是能通过自动化测试进行验证)。
我们能从 Mariner 1 的故障中学到什么呢?
首先,对代码进行充分测试是非常必要的。在跨领域的大型项目中,团队合作至关重要。同样,总是准备一个备用计划是明智的,虽然可能不需要 NASA 那样的自毁系统,但制定出错时的应对策略总是好的。
千年虫(Y2K)问题
所有经历过 2000 年新年夜的人都记得,在午夜来临时人们担心电脑是否会瘫痪、银行数据是否会丢失、飞机是否会坠落。然而,午夜来临后,几乎没有什么灾难性的事情发生。确实有几起计算机系统故障的报告,但它们很快就被解决了。
为了向未经历过千年虫问题的年轻一代以及那些90年代末忙于其他事务仅略有所闻的人提供一些背景信息:
在 20 世纪 50 年代,计算机内存的成本极为昂贵,大约为每位 1 美元,也就是每字节 8 美元。这就是为什么那个时代的编程语言,比如 COBOL,仅用两位数字来存储年份。程序默认年份的前两位数字为“19”。
因此,程序员通过节省几个字节的方式来降低成本。他们预计计算机内存的价格最终会降低,却没想到几乎花了50年时间才解决这个问题。虽然他们的预测正确,但问题在于推迟了采取行动。
像 Bob Bemer 这样的先驱从 60 年代开始就已经讨论采取的措施,并在 70 年代撰写了相关文章。但直到 1994 年,人们才开始采取行动。那时,存在该问题的系统已经非常普遍,基于这一问题开发的新软件也非常多,以至于纠正这一问题所需的工作量非常巨大。据估计,解决这个问题的总成本超过了 3000 亿美元。
我们从 Y2K 错误中学到了什么?
我们将在 2038 年见证,当到了面临 Y2K38 错误(也称为 Epochalypse)挑战的时刻。这关系到将在 2038 年 1 月 19 日遇到相似问题的 Unix 32 位时间戳,届时它的时间位将耗尽。推迟问题的解决并非总是坏事,因为在某些情况下这可能带来好处。但无论何时推迟,都必须确保任务能及时完成。在代码中添加“待办”注释时,标明负责人和预定的完成截止日期显得尤为重要。
魔兽世界腐化之血事件
《魔兽世界》是一款在 2005 年极受欢迎的电脑游戏,它吸引了大量玩家在同一服务器上联机游戏。游戏中的一项任务要求玩家团队合作挑战地下城并击败其中的敌人。2005 年 9 月,一个新地下城的终极 Boss 释放了一种名为腐化之血的法术,它能对受击的玩家造成持续伤害,并传染给附近的玩家。原本这种效果应在玩家离开地下城后消失,但一个代码漏洞导致这种效果通过玩家的宠物在游戏世界其他地方传播。
腐化之血疫情在《魔兽世界》中迅速扩散,首当其冲的是人口密集的大城市。玩家角色可以随着时间被治愈或通过死亡复活,而非玩家角色(NPC)却持续处于感染状态,这加剧了病毒的传播。一些玩家试图治疗其他受感染的玩家,或者在城市外警告他人,以阻止疫情的扩散。然而,也有些玩家却发现传播疫情很有趣。
开发团队迅速察觉到这一问题,并开始寻找解决方案。他们尝试了多种补丁,但都未能彻底消灭病毒,因为病毒总是能在某个地方存活并再次传播。经过近一个月的尝试后,他们决定重置服务器到发布该地下城前的状态。
从《魔兽世界》腐化之血 bug 中,我们能学到什么?
其中一个教训是在编程时需要考虑到所有可能的边界情况。将虚拟“病毒”像传播真实病毒一样传播给玩家的概念,在电脑游戏中实现是非常有创意的。有趣的是,这一事件后来被一些玩家回忆起,并请求开发商再次实现类似情况(这次是有意为之)。这个 bug 的发生让一些程序员对其背后的混乱情况感到好奇,并思考什么导致了需要将服务器回滚至一个月前的状态。值得一提的是,免疫学家也对这一案例很感兴趣,因此他们将这一事件作为大流行模拟的研究对象,既研究病毒的传播也研究了特定情况下的人类行为。
心脏滴血漏洞
OpenSSL 是一个开源加密库,它提供了一系列工具,用来创建符合行业安全标准的服务器与客户端加密连接。开发者要遵守这些安全标准时,不必从头开始打造解决方案,可以将这个库直接集成进项目中,借助其现成的功能确保通信安全。
然而,当这样一个旨在增强安全的代码库出现安全漏洞时,问题就显得格外严重。OpenSSL 因一个实现 TLS 心跳扩展的错误而导致本应受保护的信息被泄露,由于这个漏洞的严重性,人们给它起了个别称“心脏出血漏洞”。
随着时间的推移,这个问题最终得到了解决。但是,所有使用了 OpenSSL 库 2011 年至 2014 年间发布版本的用户都必须通过升级到最新版本来消除这一安全隐患。据估计,2014 年时,高达三分之二的活跃网站依赖 OpenSSL,尤其是因为它在 Apache 和 Nginx 等流行的开源网络服务器上得到了广泛应用。
我们能从“心脏滴血”漏洞中学到什么教训?
对于现代程序员而言,使用开源库是日常工作的一部分。如果对每个问题都从零开发,忽视了现成的解决方案,编程效率将会大大降低。然而,每当向系统中引入一个新的库时,都应该保持警惕,不能仅仅因为某个库广受欢迎就认为它绝对安全。
GitLab备份事件
GitLab 是一个深受欢迎的软件开发协作平台,用户约有 3000 万。
2017 年 1 月,GitLab 的工程师发现数据库负载突然激增。在尝试诊断问题并恢复正常运行的过程中,他们遇到了一系列困难。因此,决定手动同步部分数据库。然而在操作过程中,他们发现一个错误,尽管几秒内就停止了操作,却仍然导致了 300 GB 用户数据的丢失。
面临这一挑战时,GitLab 所依赖的备份系统发挥了关键作用。然而,当他们尝试使用备份恢复数据时,发现恢复过程存在缺陷,且没有可用的最新备份。最终,他们只能使用六小时前转移到测试环境中的数据进行恢复。对于一个拥有 3000 万用户的平台来说,六小时的数据丢失绝非小事,这种情况暴露了紧急情况下备份和数据恢复流程的不足,这无疑令人感到沮丧。
我们能从这个事件中学到什么?
作为开发者,我们都知道处理数据时备份的重要性。这起事件警示我们,仅仅拥有备份远远不够,备份策略需要根据系统的实际情况、数据类型以及用户需求来制定。此外,定期测试备份及恢复流程的有效性也是至关重要的。
从这些编程错误中,我们能得出什么结论?
有哲学家说过:“不从历史中汲取教训的人,注定会重蹈覆辙。”幸运的是,对于我们程序员而言,需要重点关注的历史始自 20 世纪,回顾起来也不算繁琐。这既是优势,也伴随着挑战,因为哪怕是微不足道的错误——例如在游戏中引入一种魔法病毒——也可能让你在 18 年后依然被人铭记。
在你读到某公司出错的案例时,我建议你想一想自己如果处在那种境地,会如何应对,并且从他们的错误中学到教训。
译者介绍
刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 Java 工程师,拥有多个主流技术博客平台博客专家称号。
原文标题: Famous Programming Errors That Everyone Should Learn From,作者:Rino Kovačević