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