程序開發者去世,代碼沒人懂,一個bug導致千萬損失

系統出故障了。當年負責寫這個程序的開發者早在十五年前就去世了,現在已經沒有人能讀得懂他的代碼了…

現在一些關鍵系統的運行仍依賴於過時的軟件,但編寫他們的人要麼離職要麼已經去世。中間也缺少維護或更新,導致現在幾乎沒人能理解它們,而且一旦出現 Bug 就會給企業造成不可挽回的損失。

而現實中的這種例子,遠比你想象中的要多。

一個令人深思的故事

我的一位客戶負責數項世界排名前一百的養老基金,該公司在前幾個月成功的將程序搬到了雲端。作爲項目的主任架構師,前兩天我很意外地直接收到了 CIO 的短信:“抱歉打擾,我們出 S1X 級的大問題了。你能下午飛過來嗎?”

“S1X”是他們對“比最嚴重級別還要糟,級聯影響到業務其它非直接相關部分”問題的定義。

事情看起來十萬火急,當天晚上我就飛到現場進行了診斷,發現是該客戶的系統中一個批處理任務發生了崩潰。

該任務每天晚上執行一次,通過寫一個 CSV 文件爲某些養老金計算繳費率,再將計算的結果輸出到另一個收益(benefit)分配程序。原先收益分配程序設定爲在繳費(contribution)低於預測(projection)時會向客戶發出報警。由於上一個處理任務已發生崩潰,不再產生輸出,因此程序認爲“所有繳費爲零”。

所以系統立即向該養老基金的經理們發出大量警報郵件,基金經理們被嚇得趕緊從該項目裏撤離了。

起初沒有人找得出問題發生在哪裏,在大家的記憶和工作記錄中,這個批處理任務也從未崩潰過。編寫該批處理程序的人已經在 15 年前離世了,並且數十年前就不再是該企業的員工了。儘管該批處理的程序代碼規模不大,但非常難讀。因爲在編程時主要考慮的是提高計算效率,並未考慮如何適合他人閱讀。當然,程序也沒有留下任何測試。

事故發生的前一天,腳本在運行環境中的編排發生了一次更改。這被認爲是導致事故的罪魁禍首。幸運的是,這個更改做了版本 push,工程部門據此回溯到先前的版本。但不幸的是,這隻使事情變得更糟。

最後我們通過提供熱修補腳本的方式解決了這個問題。但實質上這次崩潰已經給該基金造成了 170 萬美元(約 1203 萬人民幣)的直接損失。

“數據潰爛”(Bit Rot)問題

事實上類似的故事並非孤例。我在 2012 年離開英特爾加入 Sun 公司,切身體會到他們的 SPARC 產品線做得是多麼的糟糕。Sun 在互聯網泡沫時代的殺雞取卵行爲,導致此後 SPARC 遠遠落後於英特爾的至強產品線。

我的經理都和我說,要在英特爾至強服務器上運行模擬程序,因爲 SPARC 服務器“非常慢”。更嚴重的問題在於,英特爾不僅 CPU 性能更好,而且具有製造優勢,這意味着 CPU 的製造成本也大大降低。

隨之而來的問題很明顯:既然遠遠落後於競爭對手,那麼爲什麼客戶還是會購買我們的 SPARC 芯片?一位高級架構師向我給出了令人震驚的答案。那是因爲我們的客戶的軟件系統過於僵化,只能在 SPARC/Solaris 系統上運行。而遷移到 x86/Linux 對客戶而言是一項艱鉅的任務。許多客戶甚至丟失了源代碼,無法重新編譯應用。

他們能做的最好選擇,就是升級到最新一代的 SPARC 處理器,無論這樣的處理器性能多慢,價格多麼昂貴。

這就是問題所在。我們整個部門的業務模式,完全圍繞着這個國家中那些潰爛的軟件系統。

維持運轉的代價

我加入 Amazon 後,發現自己面對正是這樣一個爲遺留系統而構建適用的原型。該系統是另一個團隊基於大量技術債務開發的,並且團隊早已解散。之後該項目的所有權就移交給我們的團隊……事實操明,這並非攬下一件好事。所以開發人員陸陸續續跳槽到其他的團隊。我加入時的十多名團隊成員中,一年後一位都沒有留下。

該系統從表面上看並非一無是處。它使用了現代編程語言和技術棧(Java 8)編寫,由拿着六位數收入的開發人員所組成的團隊進行着日常維護,並不斷更新以修復錯誤和添加新功能。儘管如此,測試修改(turnover)依然是拖累整個系統的顯著負擔。所有權變更和團隊更替導致整體代碼設計、端到端功能、最佳實踐和調試技術等大量實操知識的丟失。儘管我們一直努力保持項目的推進,但感覺就像進入了一片沼澤地,四處亡羊補牢,深陷戰爭迷霧(Fog of War)中。

設想一下,一個運行在第一版 Java 上的項目,開發活動幾乎爲零,沒有開發人員負責。還有比這更糟的項目嗎?

如何預防發生災難性故障

軟件開發人員致力於構建健壯、無錯誤的系統,無需過多人工維護就能正常運行多年。據此標準,上面所說的養老金腳本無疑是非常成功的項目。

然而現實很嚴峻,再好的項目有發生崩潰的一天。最終,所有內容都需要做更新。導致原因可能是:

  • 系統運行所基於的硬件系統停產了。

  • 系統的依賴關係不再可用。

  • 依賴關係中出現了嚴重安全漏洞,而唯一可用的安全補丁僅適用於並不後向兼容的版本。

  • 應用開發基於一些已不再成立的假設。

  • 甚至是整個世界發生了改變,軟件必需因勢而變。

無論出於何種原因,變更都是不可避免的。唯一的問題是,當最終需要變更時,它的代價有多大。

對於一個活躍維護的系統,變更就不會那麼痛苦。但是,對於一個已有幾年甚至數十年沒有維護的系統,那麼很多因素都可能會導致災難性的錯誤。例如:

  • 構建系統的開發人員已經離職。

  • 源碼丟失。

  • 開發人員不瞭解如何正確地編譯源碼,並構建可執行文件。

  • 開發人員不瞭解如何部署系統。

  • 開發人員不瞭解如何正確地配置運行可執行文件。

  • 開發人員對代碼的架構和實現一頭霧水。

  • 開發人員不瞭解使代碼功能正常運作所依賴的常量和隱含假設。

  • 開發人員不瞭解如何運行自動化測試。

  • 開發人員不瞭解如何調試測試問題。

  • 開發人員不瞭解如何調試生產故障。

  • 開發人員不瞭解如何獲取生產日誌和指標度量。

一種解決方案是對上述問題做盡量詳細的記錄。但文檔並非最優的解決方案,因爲其中難免會有遺漏。再全面的文檔,也比不上自己親自動手操作。

理想的做法

一個好的開端,就是企業指定專門的開發人員全面負責上述所有問題。但這還不夠。

如果僅僅反覆“閱讀文檔”,那麼就會產生厭倦。人們並不能從中獲得實踐經驗,進而解決實際問題。

如果加上“績效審覈”,那麼人們更傾向於新的出彩項目,很有可能會簡單地抹去並掩蓋舊的問題,甚至直接從中剔除問題。如果沒有真正面對的可交付成果或挑戰,許多人自然會選擇一條最輕鬆的道路。

真正要想避免軟件發生潰爛,唯一的方法是確保項目的持續推進,即使看起來毫無必要,或是存在風險。建立、維護和驗證實操知識和能力的最佳方法,就是不斷做出變更,並測試這些變更是否能成功地執行。一旦項目停止推進,那麼相關實操知識就會過時和消解。

即使原地打轉聽上去很可笑,但這對疏於維護而言仍是一種進步。事實上,維護人員總是可以做一些事情實現向前推進,雖然步伐可能很小。

一種做法是使用所有依賴關係的最新版本去更新開發環境,例如:

  • 從 JDK 8 遷移到 11。

  • 更新 JVM,使用 G1 垃圾回收機制替代原先的 CMS。

  • 將 GCC 編譯器從版本 5 更新到 7。

  • 將數據庫從 Postgres 9.5 更新到 Postgres 11。

  • 將 AWS SDK 從版本 1.10 更新到 1.11。

  • 在生產環境中安裝最新的 Linux 發行版。

在一些情況下,依賴關係會過時。這時就需要考慮整體遷移到新的架構。例如:

  • 從 SPARC 遷移到 x86;

  • 從 Solaris 遷移到 Linux。

同樣,保持開發人員的戰鬥力,可對應用做如下更新:

  • 修復頑疾和一些邊緣用例。

  • 加固自動測試套件。

  • 清理技術債務。

  • 做性能優化。

  • 實現新特性。

  • 增量重構代碼庫,改進可讀性。

上述變化勢必會帶來暫時性風險,併產生看似“不必要的”支出。開發人員難免會犯一些錯誤,甚至引入新的錯誤。面對這些代價時,人們很容易退而求其次,認爲“如果還沒有破裂,就不要修復。”

對於一個業務價值不大的系統,這可能是合理的做法。但對於任何關鍵任務系統而言,忽略問題將僅會掩蓋較小的瞬態風險,而沒有解決永久的災難性風險。一旦有一天需要緊急調試或更新系統,那麼企業將無所適從。

對於任何關鍵任務系統,至關重要的就是維持實操知識和能力。做到這一點的唯一方法,就是不斷地開展實操。企業的大腦和肌肉一樣,不使用,就會失效。

作者介紹:

Rajiv Prabhakar,畢業於密歇根大學和斯坦福大學,之後曾在 Intel 和 Sun Microsystem 公司工作五年,參與了 Jaketown、Skylake 和 SPARC 設計團隊。之後轉向軟件相關工作,先後任職於 Amazon、Google、Engineers Gate,並數次自己組建創業公司。

原文鏈接:

https://software.rajivprab.com/2020/04/25/preventing-software-rot/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章