譯者 | 劉汪洋
審校 | 重樓
“錯誤是成長的階梯”和“失敗乃成功之母”——這些諺語為我們在犯錯時提供慰藉。程序員熱衷于創(chuàng)新,對追求新技術(shù)趨勢保持著高度的熱情,這就要求他們必須不斷學(xué)習(xí)?;谶@些觀點、虛構(gòu)的情節(jié),再加上我的七年程序開發(fā)經(jīng)驗以及與同行的交流,我認(rèn)為程序員經(jīng)常會犯錯。
為了發(fā)現(xiàn)或預(yù)防這些錯誤,我們采取了自動化測試、代碼審查、環(huán)境隔離和灰度發(fā)布、執(zhí)行數(shù)據(jù)備份、與質(zhì)量工程師合作,還有利用多種工具來盡早發(fā)現(xiàn)問題。
即便實行了這些預(yù)防措施,偶爾還是會有漏洞在測試環(huán)節(jié)被漏掉,進(jìn)而進(jìn)入生產(chǎn)環(huán)境。這時候,我們該怎么辦?我們會迅速定位問題所在,進(jìn)行修復(fù),并盡快將修改部署到生產(chǎn)環(huán)境。我們的目標(biāo)是盡可能降低受影響用戶的數(shù)量。另一方面,錯誤在生產(chǎn)環(huán)境中存在的時間越長或影響越大,就越能引起公司內(nèi)部更多的討論。
然而,某些錯誤因其巨大的規(guī)?;蛱厥獾那闆r,不止在經(jīng)歷過這些錯誤的人群中引發(fā)討論。這些錯誤可能會成為新聞頭條、報紙的熱門話題,有些甚至擁有獨立的維基百科頁面,一些錯誤發(fā)生 60 年后也仍然歷歷在目。在本文中,我們將探討一些這類重大錯誤,了解它們是如何發(fā)生的,以及我們能從中學(xué)到什么。
Mariner 1:世界上最昂貴的破折號錯誤
1962 年,NASA 發(fā)射了首艘前往其他行星的探測器 Mariner 1,計劃飛往金星并測量其溫度、磁場等科學(xué)家感興趣的數(shù)據(jù)。然而意外發(fā)生了:發(fā)射不久后,探測器開始偏離預(yù)定軌道。為了避免對地球造成潛在的風(fēng)險,最終決定啟動自毀程序。
人們普遍認(rèn)為,這次失敗是由編程錯誤導(dǎo)致的。具體來說,是由于代碼中缺少了一個破折號。事實是什么呢?在代碼中,存在一個涉及符號“R”代表半徑的數(shù)學(xué)運算。正確的表示應(yīng)為平均半徑“R-bar”,在物理學(xué)中表示為 R?。然而,這不僅僅是關(guān)于“一個破折號”的問題。這個錯誤導(dǎo)致銷毀了價值兩千萬美金航天器。
既是一個物理和數(shù)學(xué)上的錯誤,為何仍然被歸結(jié)為編程錯誤呢?這個錯誤最初出現(xiàn)在計算過程中,程序員只是將其轉(zhuǎn)換成了代碼。在理想狀態(tài)下,這種錯誤應(yīng)該通過測試發(fā)現(xiàn),或者在程序員與數(shù)學(xué)家或物理學(xué)家的合作中被識別。程序員經(jīng)常為不同領(lǐng)域編寫代碼,雖然不用成為該領(lǐng)域?qū)<揖涂梢蚤_發(fā)應(yīng)用,但掌握基礎(chǔ)知識并和該領(lǐng)域的專家合作是很重要的,這樣才能開發(fā)出可測試的代碼場景(最好是能通過自動化測試進(jìn)行驗證)。
我們能從 Mariner 1 的故障中學(xué)到什么呢?
首先,對代碼進(jìn)行充分測試是非常必要的。在跨領(lǐng)域的大型項目中,團隊合作至關(guān)重要。同樣,總是準(zhǔn)備一個備用計劃是明智的,雖然可能不需要 NASA 那樣的自毀系統(tǒng),但制定出錯時的應(yīng)對策略總是好的。
千年蟲(Y2K)問題
所有經(jīng)歷過 2000 年新年夜的人都記得,在午夜來臨時人們擔(dān)心電腦是否會癱瘓、銀行數(shù)據(jù)是否會丟失、飛機是否會墜落。然而,午夜來臨后,幾乎沒有什么災(zāi)難性的事情發(fā)生。確實有幾起計算機系統(tǒng)故障的報告,但它們很快就被解決了。
為了向未經(jīng)歷過千年蟲問題的年輕一代以及那些90年代末忙于其他事務(wù)僅略有所聞的人提供一些背景信息:
在 20 世紀(jì) 50 年代,計算機內(nèi)存的成本極為昂貴,大約為每位 1 美元,也就是每字節(jié) 8 美元。這就是為什么那個時代的編程語言,比如 COBOL,僅用兩位數(shù)字來存儲年份。程序默認(rèn)年份的前兩位數(shù)字為“19”。
因此,程序員通過節(jié)省幾個字節(jié)的方式來降低成本。他們預(yù)計計算機內(nèi)存的價格最終會降低,卻沒想到幾乎花了50年時間才解決這個問題。雖然他們的預(yù)測正確,但問題在于推遲了采取行動。
像 Bob Bemer 這樣的先驅(qū)從 60 年代開始就已經(jīng)討論采取的措施,并在 70 年代撰寫了相關(guān)文章。但直到 1994 年,人們才開始采取行動。那時,存在該問題的系統(tǒng)已經(jīng)非常普遍,基于這一問題開發(fā)的新軟件也非常多,以至于糾正這一問題所需的工作量非常巨大。據(jù)估計,解決這個問題的總成本超過了 3000 億美元。
我們從 Y2K 錯誤中學(xué)到了什么?
我們將在 2038 年見證,當(dāng)?shù)搅嗣媾R Y2K38 錯誤(也稱為 Epochalypse)挑戰(zhàn)的時刻。這關(guān)系到將在 2038 年 1 月 19 日遇到相似問題的 Unix 32 位時間戳,屆時它的時間位將耗盡。推遲問題的解決并非總是壞事,因為在某些情況下這可能帶來好處。但無論何時推遲,都必須確保任務(wù)能及時完成。在代碼中添加“待辦”注釋時,標(biāo)明負(fù)責(zé)人和預(yù)定的完成截止日期顯得尤為重要。
魔獸世界腐化之血事件
《魔獸世界》是一款在 2005 年極受歡迎的電腦游戲,它吸引了大量玩家在同一服務(wù)器上聯(lián)機游戲。游戲中的一項任務(wù)要求玩家團隊合作挑戰(zhàn)地下城并擊敗其中的敵人。2005 年 9 月,一個新地下城的終極 Boss 釋放了一種名為腐化之血的法術(shù),它能對受擊的玩家造成持續(xù)傷害,并傳染給附近的玩家。原本這種效果應(yīng)在玩家離開地下城后消失,但一個代碼漏洞導(dǎo)致這種效果通過玩家的寵物在游戲世界其他地方傳播。
腐化之血疫情在《魔獸世界》中迅速擴散,首當(dāng)其沖的是人口密集的大城市。玩家角色可以隨著時間被治愈或通過死亡復(fù)活,而非玩家角色(NPC)卻持續(xù)處于感染狀態(tài),這加劇了病毒的傳播。一些玩家試圖治療其他受感染的玩家,或者在城市外警告他人,以阻止疫情的擴散。然而,也有些玩家卻發(fā)現(xiàn)傳播疫情很有趣。
開發(fā)團隊迅速察覺到這一問題,并開始尋找解決方案。他們嘗試了多種補丁,但都未能徹底消滅病毒,因為病毒總是能在某個地方存活并再次傳播。經(jīng)過近一個月的嘗試后,他們決定重置服務(wù)器到發(fā)布該地下城前的狀態(tài)。
從《魔獸世界》腐化之血 bug 中,我們能學(xué)到什么?
其中一個教訓(xùn)是在編程時需要考慮到所有可能的邊界情況。將虛擬“病毒”像傳播真實病毒一樣傳播給玩家的概念,在電腦游戲中實現(xiàn)是非常有創(chuàng)意的。有趣的是,這一事件后來被一些玩家回憶起,并請求開發(fā)商再次實現(xiàn)類似情況(這次是有意為之)。這個 bug 的發(fā)生讓一些程序員對其背后的混亂情況感到好奇,并思考什么導(dǎo)致了需要將服務(wù)器回滾至一個月前的狀態(tài)。值得一提的是,免疫學(xué)家也對這一案例很感興趣,因此他們將這一事件作為大流行模擬的研究對象,既研究病毒的傳播也研究了特定情況下的人類行為。
心臟滴血漏洞
OpenSSL 是一個開源加密庫,它提供了一系列工具,用來創(chuàng)建符合行業(yè)安全標(biāo)準(zhǔn)的服務(wù)器與客戶端加密連接。開發(fā)者要遵守這些安全標(biāo)準(zhǔn)時,不必從頭開始打造解決方案,可以將這個庫直接集成進(jìn)項目中,借助其現(xiàn)成的功能確保通信安全。
然而,當(dāng)這樣一個旨在增強安全的代碼庫出現(xiàn)安全漏洞時,問題就顯得格外嚴(yán)重。OpenSSL 因一個實現(xiàn) TLS 心跳擴展的錯誤而導(dǎo)致本應(yīng)受保護(hù)的信息被泄露,由于這個漏洞的嚴(yán)重性,人們給它起了個別稱“心臟出血漏洞”。
隨著時間的推移,這個問題最終得到了解決。但是,所有使用了 OpenSSL 庫 2011 年至 2014 年間發(fā)布版本的用戶都必須通過升級到最新版本來消除這一安全隱患。據(jù)估計,2014 年時,高達(dá)三分之二的活躍網(wǎng)站依賴 OpenSSL,尤其是因為它在 Apache 和 Nginx 等流行的開源網(wǎng)絡(luò)服務(wù)器上得到了廣泛應(yīng)用。
我們能從“心臟滴血”漏洞中學(xué)到什么教訓(xùn)?
對于現(xiàn)代程序員而言,使用開源庫是日常工作的一部分。如果對每個問題都從零開發(fā),忽視了現(xiàn)成的解決方案,編程效率將會大大降低。然而,每當(dāng)向系統(tǒng)中引入一個新的庫時,都應(yīng)該保持警惕,不能僅僅因為某個庫廣受歡迎就認(rèn)為它絕對安全。
GitLab備份事件
GitLab 是一個深受歡迎的軟件開發(fā)協(xié)作平臺,用戶約有 3000 萬。
2017 年 1 月,GitLab 的工程師發(fā)現(xiàn)數(shù)據(jù)庫負(fù)載突然激增。在嘗試診斷問題并恢復(fù)正常運行的過程中,他們遇到了一系列困難。因此,決定手動同步部分?jǐn)?shù)據(jù)庫。然而在操作過程中,他們發(fā)現(xiàn)一個錯誤,盡管幾秒內(nèi)就停止了操作,卻仍然導(dǎo)致了 300 GB 用戶數(shù)據(jù)的丟失。
面臨這一挑戰(zhàn)時,GitLab 所依賴的備份系統(tǒng)發(fā)揮了關(guān)鍵作用。然而,當(dāng)他們嘗試使用備份恢復(fù)數(shù)據(jù)時,發(fā)現(xiàn)恢復(fù)過程存在缺陷,且沒有可用的最新備份。最終,他們只能使用六小時前轉(zhuǎn)移到測試環(huán)境中的數(shù)據(jù)進(jìn)行恢復(fù)。對于一個擁有 3000 萬用戶的平臺來說,六小時的數(shù)據(jù)丟失絕非小事,這種情況暴露了緊急情況下備份和數(shù)據(jù)恢復(fù)流程的不足,這無疑令人感到沮喪。
我們能從這個事件中學(xué)到什么?
作為開發(fā)者,我們都知道處理數(shù)據(jù)時備份的重要性。這起事件警示我們,僅僅擁有備份遠(yuǎn)遠(yuǎn)不夠,備份策略需要根據(jù)系統(tǒng)的實際情況、數(shù)據(jù)類型以及用戶需求來制定。此外,定期測試備份及恢復(fù)流程的有效性也是至關(guān)重要的。
從這些編程錯誤中,我們能得出什么結(jié)論?
有哲學(xué)家說過:“不從歷史中汲取教訓(xùn)的人,注定會重蹈覆轍?!毙疫\的是,對于我們程序員而言,需要重點關(guān)注的歷史始自 20 世紀(jì),回顧起來也不算繁瑣。這既是優(yōu)勢,也伴隨著挑戰(zhàn),因為哪怕是微不足道的錯誤——例如在游戲中引入一種魔法病毒——也可能讓你在 18 年后依然被人銘記。
在你讀到某公司出錯的案例時,我建議你想一想自己如果處在那種境地,會如何應(yīng)對,并且從他們的錯誤中學(xué)到教訓(xùn)。
譯者介紹
劉汪洋,51CTO社區(qū)編輯,昵稱:明明如月,一個擁有 5 年開發(fā)經(jīng)驗的某大廠高級 Java 工程師,擁有多個主流技術(shù)博客平臺博客專家稱號。
原文標(biāo)題: Famous Programming Errors That Everyone Should Learn From,作者:Rino Kova?evi?