程序員和工程師的民間傳說(第 1 部分)

程序員和工程師的民間傳說(第 1 部分)

這是來自互聯網的精選故事,講述了錯誤有時如何具有完全令人難以置信的表現。 也許你也有話要說。

汽車對香草冰淇淋過敏

這是一個工程師的故事,他們明白顯而易見的並不總是答案,無論事實看起來多麼牽強,它們仍然是事實。 通用汽車公司龐蒂亞克分部收到投訴:

這是我第二次給你寫信,我不怪你沒有回信,因為這聽起來很瘋狂。 我們家有每天晚上晚飯後吃冰淇淋的傳統。 每次冰淇淋的種類都會改變,吃完晚飯,全家人都會選擇要買哪種冰淇淋,然後我就去商店。 我最近買了一輛新的龐蒂亞克,從那以後我去買冰淇淋就成了一個問題。 你看,每次我買香草冰淇淋從商店回來,車子就發動不起來。 如果我帶任何其他冰淇淋,汽車啟動就沒有任何問題。 我想問一個嚴肅的問題,不管聽起來多麼愚蠢:“龐蒂亞克是什麼原因讓它在我帶香草冰淇淋時不啟動,但在我帶另一種口味的冰淇淋時卻很容易啟動?”

正如你可以想像的那樣,部門總裁對這封信持懷疑態度。 不過,為了以防萬一,我還是派了工程師去檢查。 他很驚訝自己遇到了一位住在美麗地區、富有、受過良好教育的男人。 他們約好晚飯後立即見面,這樣兩人就可以去商店買冰淇淋。 那天晚上是香草味的,當他們回到車上時,車就發動不起來了。

工程師又來了三個晚上。 第一次吃巧克力冰淇淋。 車子啟動了。 第二次有草莓冰淇淋。 車子啟動了。 第三天晚上,他要求吃香草。 汽車沒有啟動。

理性推理,工程師不肯相信這輛車對香草冰淇淋過敏。 因此,我同意車主的意見,讓他繼續拜訪,直到找到問題的解決辦法。 一路上,他開始做筆記:他記下了所有信息,一天中的時間,汽油類型,到達商店和從商店返回的時間等等。

工程師很快意識到車主花在購買香草冰淇淋上的時間更少了。 原因就在於店內商品的佈局。 香草冰淇淋是最受歡迎的,並且被保存在商店前面的單獨冰櫃中,以便更容易找到。 所有其他品種都在商店後面,找到合適的品種並付款需要花費更多時間。

現在的問題是工程師:如果從發動機關閉那一刻起經過的時間較短,為什麼汽車還沒有啟動? 由於問題在於時間,而不是香草冰淇淋,工程師很快找到了答案:這是一個氣鎖。 這種情況每天晚上都會發生,但當車主花更多時間尋找冰淇淋時,發動機設法冷卻到足夠的溫度並輕鬆啟動。 當該男子購買香草冰淇淋時,發動機仍然太熱,氣鎖沒有時間溶解。

寓意:即使是完全瘋狂的問題有時也是真實存在的。

古惑狼

經歷這些是很痛苦的。 作為一名程序員,你習慣於首先、第二、第三地責怪你的代碼……而在萬分之一的地方你會責怪編譯器。 再往下看,你已經把責任歸咎於設備了。

這是我關於硬件錯誤的故事。

對於遊戲古惑狼,我編寫了加載並保存到存儲卡的代碼。 對於這樣一個自鳴得意的遊戲開發者來說,這就像在公園散步一樣:我以為這項工作需要幾天的時間。 然而,我最終調試了六個星期的代碼。 一路上,我解決了其他問題,但每隔幾天我就會回到這段代碼幾個小時。 這是痛苦的。

症狀如下:當您保存遊戲的當前遊戲並訪問存儲卡時,一切幾乎總是正常......但有時讀取或寫入操作會無明顯原因超時。 短時間的錄音通常會損壞存儲卡。 當玩家嘗試保存時,他不僅保存失敗,還破壞了地圖。 廢話。

過了一段時間,我們索尼的製片人康妮·巴斯開始恐慌。 我們無法發布帶有此錯誤的遊戲,六週後我不明白是什麼導致了這個問題。 通過Connie,我們聯繫了其他PS1開發者:有人遇到過類似的事情嗎? 不。 沒有人遇到存儲卡問題。

當你沒有調試的想法時,剩下的唯一方法就是“分而治之”:從有問題的程序中刪除越來越多的代碼,直到剩下一個相對較小的片段仍然會導致問題。 也就是說,你把程序一塊一塊地砍掉,直到剩下包含bug的部分。

但問題是,從視頻遊戲中刪減片段非常困難。 如果刪除了模擬重力的代碼,如何運行它? 還是畫人物?

因此,我們必須用存根替換整個模塊,這些存根假裝做一些有用的事情,但實際上做一些非常簡單且不能包含錯誤的事情。 我們必須編寫這樣的拐杖才能使遊戲至少能夠運行。 這是一個緩慢而痛苦的過程。

簡而言之,我做到了。 我刪除了越來越多的代碼,直到只剩下配置系統運行遊戲、初始化渲染硬件等的初始代碼。 當然,在這個階段我無法創建保存和加載菜單,因為我必須為所有圖形代碼創建一個存根。 但我可以假裝是使用(不可見的)保存和加載屏幕的用戶,並要求保存然後寫入存儲卡。

這給我留下了一小段代碼,仍然存在上述問題 - 但它仍然是隨機發生的! 大多數情況下一切都工作正常,但偶爾也會出現故障。 我刪除了幾乎所有的遊戲代碼,但錯誤仍然存​​在。 這令人費解:剩下的代碼實際上沒有做任何事情。

在某個時刻,大概是凌晨三點左右,我突然想到了一個想法。 讀取和寫入(輸入/輸出)操作涉及精確的執行時間。 當您使用硬盤驅動器、存儲卡或藍牙模塊時,負責讀寫的低級代碼根據時鐘脈衝進行讀寫。

在時鐘的幫助下,未直接連接到處理器的設備與處理器上執行的代碼同步。 時鐘決定波特率——數據傳輸的速度。 如果時序混亂,那麼硬件或軟件或兩者也會混亂。 這是非常糟糕的,因為數據可能會被損壞。

如果我們的代碼中的某些內容混淆了時間怎麼辦? 我檢查了測試程序代碼中與此相關的所有內容,並註意到我們將 PS1 中的可編程定時器設置為 1 kHz(每秒 1000 個滴答聲)。 這是相當多的;默認情況下,當控制台啟動時,它以 100 Hz 運行。 大多數遊戲都使用這個頻率。

遊戲開發者 Andy 將計時器設置為 1 kHz,以便更準確地計算動作。 安迪傾向於做得太過分,如果我們模擬重力,我們會盡可能準確地做到這一點!

但是,如果加速計時器以某種方式影響了程序的整體計時,從而影響了調節存儲卡波特率的時鐘,該怎麼辦?

我註釋掉了定時器代碼。 錯誤沒有再次發生。 但這並不意味著我們修復了它,因為故障是隨機發生的。 如果我只是幸運的話怎麼辦?

幾天后我再次嘗試了測試程序。 該錯誤沒有再次出現。 我返回到完整的遊戲代碼庫並修改了保存和加載代碼,以便可編程計時器在訪問存儲卡之前重置為其原始值(100Hz),然後重置回1kHz。 沒有再發生車禍。

但為什麼會發生這種情況呢?

我又回到了測試程序。 我試圖找到 1 kHz 定時器發生錯誤的一些模式。 最終我注意到當有人使用 PS1 控制器玩遊戲時會出現錯誤。 由於我自己很少這樣做 - 為什麼在測試保存和加載代碼時需要控制器? - 我什至沒有註意到這種依賴性。 但有一天,我們的一位藝術家正在等我完成測試——當時我可能正在咒罵——並緊張地轉動著手中的控制器。 發生了錯誤。 “等等,什麼?!” 好吧,再做一次!”

當我意識到這兩個事件是相互關聯的時,我能夠輕鬆地重現該錯誤:我開始記錄到存儲卡,移動控制器,並損壞了存儲卡。 對我來說,這看起來像是一個硬件錯誤。

我找到康妮,告訴她我的發現。 她將這一信息轉達給了設計 PS1 的一位工程師。 “不可能,”他回答道,“這不可能是硬件問題。” 我請康妮為我們安排一次談話。

工程師打電話給我,我們用他蹩腳的英語和我(極其)蹩腳的日語爭論。 最後我說:“讓我發送我的 30 行測試程序,其中移動控制器會導致錯誤。” 他同意。 說這是浪費時間,而且他正忙於一個新項目,但會屈服,因為我們是索尼非常重要的開發商。 我清理了我的測試程序並將其發送給他。

第二天晚上(我們在洛杉磯,他在東京)他打電話給我,不好意思地道歉。 這是一個硬件問題。

我不知道這個錯誤到底是什麼,但據我在索尼總部聽到的消息,如果你將定時器設置得足夠高,它會干擾主板上定時器晶體附近的組件。 其中之一是存儲卡的波特率控制器,它還設置控制器的波特率。 我不是工程師,所以我可能搞砸了一些事情。

但最重要的是主板上的組件之間存在干擾。 當定時器以 1 kHz 運行時,通過控制器端口和存儲卡端口同時傳輸數據時,會出現位丟失、數據丟失和卡損壞的情況。

壞牛

在 1980 世紀 1800 年代,我的導師 Sergei 為 SM-11(PDP-XNUMX 的蘇聯克隆版)編寫了軟件。 這台微型計算機剛剛安裝在蘇聯重要交通樞紐斯維爾德洛夫斯克附近的一個火車站。 新系統旨在為貨車和貨運提供路線。 但它包含一個惱人的錯誤,導致隨機崩潰和崩潰。 跌倒總是發生在有人晚上回家時。 但儘管第二天進行了徹底的調查,計算機在所有手動和自動測試中都工作正常。 這通常表示在某些條件下發生競爭條件或其他一些競爭性錯誤。 謝爾蓋厭倦了深夜的電話,決定徹查到底,首先了解編組站的哪些情況導致了計算機故障。

首先,他收集了所有不明原因跌倒的統計數據,並按日期和時間創建了圖表。 模式很明顯。 又觀察了幾天,謝爾蓋意識到他可以輕鬆預測未來系統故障的時間。

他很快了解到,只有當車站對從烏克蘭北部和俄羅斯西部運往附近屠宰場的一列牛進行分類時,才會出現干擾。 這本身就很奇怪,因為屠宰場是由距離更近的哈薩克斯坦農場供應的。

1986 年,切爾諾貝利核電站發生爆炸,放射性沉降物使周邊地區無法居住。 烏克蘭北部、白俄羅斯和俄羅斯西部的大片地區受到污染。 謝爾蓋懷疑抵達的車廂輻射水平很高,因此開發了一種方法來測試這一理論。 人們被禁止擁有劑量計,因此謝爾蓋在火車站向幾名軍人登記。 喝了幾杯伏特加後,他設法說服一名士兵測量其中一輛可疑車廂的輻射水平。 事實證明,該水平比正常值高出數倍。

牛不僅發出大量輻射,而且輻射水平太高,導致位於空間站旁邊建築物內的 SM-1800 內存中的位隨機丟失。

蘇聯出現糧食短缺,當局決定將切爾諾貝利肉類與該國其他地區的肉類混合。 這使得在不損失寶貴資源的情況下降低總體放射性水平成為可能。 得知此事後,謝爾蓋立即填寫了移民文件。 當輻射水平隨著時間的推移而降低時,計算機崩潰會自行停止。

通過管道

曾幾何時,Movietech Solutions 為電影院創建了軟件,專為會計、票務銷售和綜合管理而設計。 該旗艦應用的DOS版本在北美中小型連鎖電影院中頗受歡迎。 因此,當集成了最新觸摸屏和自助服務亭並配備各種報告工具的Windows 95版本發布後,它也迅速流行起來,這並不奇怪。 大多數情況下更新都沒有問題。 當地IT人員安裝了新設備,遷移了數據,業務繼續進行。 除非它沒有持續下去。 當這種情況發生時,公司就會派出綽號“清潔工”的詹姆斯。

雖然這個暱稱暗示著邪惡的類型,但清潔工只是指導員、安裝工和多才多藝的組合。 詹姆斯會花幾天時間在客戶現場將所有組件組裝在一起,然後再花幾天時間教員工如何使用新系統,解決出現的任何硬件問題,並從根本上幫助軟件度過其起步階段。

因此,在這段忙碌的時間裡,詹姆斯早上到達辦公室,還沒走到辦公桌前,就受到了經理的迎接,經理的咖啡因超出了平時的水平,這也就不足為奇了。

“恐怕你需要盡快去新斯科舍省的安納波利斯。” 他們的整個系統癱瘓了,經過一整夜的工程師工作,我們無法弄清楚發生了什麼。 服務器上的網絡似乎出現故障。 但只有在系統運行了幾分鐘之後。

——他們沒有回到舊系統? ——詹姆斯十分認真地回答,不過心裡卻驚訝地睜大了眼睛。

— 確切地說:他們的 IT 專家“改變了優先級”並決定保留舊服務器。 James,他們在六個站點安裝了該系統,並且只支付了高級支持費用,他們的業務現在就像 1950 世紀 XNUMX 年代一樣運行。

詹姆斯微微直起身子。

- 那是另一回事了好的,讓我們開始吧。

當他到達安納波利斯後,他做的第一件事就是找到客戶第一家出現問題的影院。 在機場拍攝的地圖上,一切看起來都不錯,但所需地址周圍的區域看起來很可疑。 不是貧民窟,而是讓人想起黑色電影。 當詹姆斯把車停在市中心的路邊時,一名妓女走近他。 考慮到安納波利斯的規模,它很可能是整個城市中唯一的一個。 她的出現立刻讓人想起大銀幕上那個以性換錢的著名角色。 不,不是關於朱莉婭·羅伯茨,而是關於強·沃特 [暗示電影“午夜牛仔” - 大約。 車道].

送走妓女後,詹姆斯去了電影院。 周圍環境已經好了很多,但還是給人一種破敗的感覺。 並不是說詹姆斯太擔心了。 他以前也去過一些骯髒的地方。 這是加拿大,即使是搶劫犯在拿走你的錢包後也會有禮貌地說“謝謝”。

電影院的側門在一條潮濕的小巷裡。 詹姆斯走到門口敲了敲門。 很快,它吱吱作響,輕輕地打開了。

-你是清潔工嗎? ——裡面傳來沙啞的聲音。

- 是的,是我......我來修復一切。

詹姆斯走進電影院大廳。 顯然別無選擇,工作人員開始向遊客分發紙質門票。 這使得財務報告變得困難,更不用說更有趣的細節了。 但工作人員鬆了口氣迎接了詹姆斯,並立即將他帶到了服務器機房。

乍一看,一切都很好。 詹姆斯登錄服務器並檢查了常見的可疑地方。 沒問題。 然而,出於謹慎考慮,詹姆斯關閉了服務器,更換了網卡,並回滾了系統。 她立即​​開始全力工作。 工作人員又開始售票了。

詹姆斯打電話給馬克,向他通報了情況。 不難想像詹姆斯可能想留下來看看是否會發生什麼意外。 他走下樓梯,開始詢問員工發生了什麼事。 顯然系統已經停止工作了。 他們把它關掉再打開,一切正常。 但10分鐘後系統就掉下來了。

就在這時,類似的事情發生了。 突然,票務系統開始拋出錯誤。 工作人員嘆了口氣,抓起紙質門票,詹姆斯趕緊奔向服務器機房。 服務器一切看起來都很好。

然後一名員工走了進來。

— 系統再次運行。

詹姆斯很困惑,因為他什麼也沒做。 更準確地說,沒有任何東西能讓系統正常工作。 他退出後,拿起手機,撥打了公司的支持熱線。 很快,同一名員工進入了服務器機房。

- 系統宕機了。

詹姆斯看了一眼服務員。 屏幕上舞動著一種有趣而熟悉的多彩形狀圖案——混亂地扭動和纏繞的管道。 我們都曾在某個時候見過這個屏幕保護程序。 它渲染得很漂亮,簡直令人著迷。


詹姆斯按下按鈕,圖案就消失了。 他急忙趕到售票處,半路上遇到了回來找他的工作人員。

— 系統再次運行。

如果你能在心裡捂臉,那就是詹姆斯所做的。 螢幕保護程式. 它使用OpenGL。 因此,在運行過程中,它會消耗服務器處理器的所有資源。 結果,每次對服務器的調用都會超時結束。

詹姆斯回到服務器機房,登錄,然後將屏幕保護程序替換為帶有空白屏幕的漂亮管道。 也就是說,我沒有安裝一個消耗 100% 處理器資源的屏幕保護程序,而是安裝了另一個不消耗資源的屏幕保護程序。 然後我等了10分鐘來驗證我的猜測。

當詹姆斯到達下一家電影院時,他正在考慮如何向經理解釋他剛剛飛了 800 公里來關閉屏幕保護程序。

在月相的某個階段發生墜機

真實的故事。 有一天,出現了一個與月相有關的軟件錯誤。 麻省理工學院的各種程序中常用一個小子程序來計算月球真實相位的近似值。 GLS 將此例程構建到 LISP 程序中,該程序在寫入文件時會輸出帶有近 80 個字符長的時間戳的行。 消息的第一行最終會變得太長並導致下一行的情況非常罕見。 當程序後來讀取這個文件時,它咒罵了。 第一行的長度取決於確切的日期和時間,以及打印時間戳時階段規範的長度。 也就是說,這個錯誤實際上取決於月相!

第一紙質版 行話文件 (Steele-1983) 包含一個導致所描述的錯誤的行的示例,但排字機“修復”了它。 此後這被描述為“月相錯誤”。

然而,要小心假設。 幾年前,歐洲核子研究中心(CERN)的工程師在大型正負電子對撞機進行的實驗中遇到了錯誤。 由於計算機在向科學家展示結果之前會主動處理該設備生成的大量數據,因此許多人推測該軟件在某種程度上對月相敏感。 幾位絕望的工程師查出了真相。 該誤差的產生是由於月球經過期間地球變形導致27公里長的環的幾何形狀發生了微小變化! 這個故事已作為“牛頓對粒子物理學的複仇”進入物理學民間傳說,也是最簡單、最古老的物理定律與最先進的科學概念之間聯繫的一個例子。

沖廁所讓火車停下來

我聽說過的最好的硬件錯誤是在法國的高速列車上。 該漏洞導致火車緊急制動,但前提是車上有乘客。 在每次此類情況下,列車都被停止運行並進行檢查,但什麼也沒發現。 然後他被送迴線上,立刻就停了下來。

在一次檢查中,一名乘坐火車的工程師去了廁所。 很快他就被沖走了,轟! 緊急停車。

工程師聯繫了司機詢問:

— 剎車前你在做什麼?

- 好吧,我在下降時放慢了速度......

這很奇怪,因為在正常運行期間,火車在下坡時會減速數十次。 火車繼續前行,在下一個下坡處,司機警告道:

- 我要放慢速度。

什麼都沒發生。

— 上次剎車時你做了什麼? - 司機問道。

- 嗯...我在廁所...

- 好吧,那就去廁所,做我們再下去時做的事!

工程師去了廁所,當司機警告:“我要減速”時,他沖掉了水。 當然,火車立刻停了下來。

現在他們可以重現問題並需要找到原因。

兩分鐘後,他們注意到發動機制動遙控電纜(火車兩端各有一個發動機)與電氣櫃壁斷開,並躺在控制馬桶插頭電磁閥的繼電器上......當繼電器打開時,它會對製動拉索產生干擾,系統針對故障的保護僅包括緊急制動。

討厭 FORTRAN 的網關

幾個月前,我們注意到大陸(在夏威夷)的網絡連接變得非常非常慢。 這種情況可能會持續 10-15 分鐘,然後突然再次發生。 過了一段時間,我的同事向我抱怨大陸的網絡連接 一般來說 不起作用。 他有一些 FORTRAN 代碼需要復製到大陸的一台機器上,但無法複製,因為“網絡支持的時間不夠長,無法完成 FTP 上傳”。

是的,原來是同事試圖將FORTRAN源代碼的文件FTP到大陸的機器上時出現網絡故障。 我們嘗試對文件進行歸檔:然後就順利複製了(但是目標機沒有解包器,所以問題沒有解決)。 最後,我們將 FORTRAN 代碼“拆分”成非常小的片段,並一次發送一個。 大部分片段複製沒有問題,但有少數片段沒有通過,或者之後通過 很多的 嘗試。

當我們檢查有問題的段落時,我們發現它們有一些共同點:它們都包含以大寫 C 組成的行開始和結束的註釋塊(正如一位同事更喜歡在 FORTRAN 中進行註釋)。 我們給大陸的網絡專家發了郵件尋求幫助。 當然,他們想查看我們無法通過 FTP 傳輸的文件樣本……但我們的信件沒有到達他們手中。 最後我們想出了一個簡單的方法 描述不可傳輸的文件是什麼樣的。 它有效:) [我敢在這裡添加一個有問題的 FORTRAN 註釋之一的示例嗎? 可能不值得!]

最終我們設法弄清楚了。 最近在我們校園和大陸網絡之間安裝了一個新的網關。 它在傳輸包含重複的大寫 C 位的數據包時遇到了巨大的困難! 這些數據包中的一小部分就可能佔用所有網關資源並阻止大多數其他數據包通過。 我們向網關製造商投訴……他們回答說:“哦,是的,你面臨著重複 C 的錯誤! 我們已經了解他了。” 我們最終通過從另一家製造商購買新網關解決了這個問題(在前者的辯護中,無法傳輸 FORTRAN 程序對某些人來說可能是一個優勢!)。

困難時期

幾年前,當我致力於用 Perl 創建 ETL 系統以降低 40 期臨床試驗的成本時,我需要處理大約 000 個日期。 其中兩人沒有通過測試。 這並沒有讓我太困擾,因為這些日期取自客戶提供的數據,我們可以說,這些數據常常令人驚訝。 但當我查看原始數據時,結果發現這些日期是1年2011月1日和2007年30月XNUMX日。我以為這個bug包含在我剛剛寫的程序中,結果發現已經有XNUMX年了老的。 對於那些不熟悉軟件生態系統的人來說,這可能聽起來很神秘。 由於另一家公司長期以來決定賺錢,我的客戶付錢給我修復一個公司無意引入的錯誤,而另一家公司故意引入的錯誤。 為了讓您理解我在說什麼,我需要談談添加了最終成為錯誤的功能的公司,以及導致我修復的神秘錯誤的其他一些有趣的事件。

在過去的美好時光,蘋果電腦有時會自發地將日期重置為 1 年 1904 月 1 日。 原因很簡單:它使用電池供電的“系統時鐘”來跟踪日期和時間。 電池沒電後發生了什麼? 計算機開始按照紀元開始以來的秒數來跟踪日期。 我們所說的紀元指的是參考原始日期,對於 Macintosh 來說,它是 1904 年 XNUMX 月 XNUMX 日。電池耗儘後,當前日期被重置為指定的日期。 但為什麼會發生這種情況呢?

此前,Apple 使用 32 位來存儲自原始日期以來的秒數。 一位可以存儲兩個值之一 - 1 或 0。兩位可以存儲四個值之一:00、01、10、11。三位 - 八個值之一:000、001、010、011、100 、101、110、111 等32可以存儲232個值之一,即4秒。 對於 Apple 日期,這相當於大約 294 年,因此較舊的 Mac 無法處理 967 年之後的日期。 如果系統電池耗盡,日期將重置為自紀元開始以來的 296 秒,並且您必須在每次打開計算機時手動設置日期(或直到您購買新電池)。

然而,Apple 決定將日期存儲為自紀元以來的秒數,這意味著我們無法處理紀元之前的日期,這會產生深遠的影響,正如我們將看到的。 蘋果引入了一項功能,而不是一個錯誤。 除其他外,這意味著 Macintosh 操作系統不受“千年蟲”的影響(對於許多擁有自己的日期系統來規避限制的 Mac 應用程序來說,這是不能說的)。

前進。 我們使用了 Lotus 1-2-3,IBM 的“殺手級應用程序”,它幫助發起了 PC 革命,儘管 Apple 計算機擁有 VisiCalc,它使個人計算機取得了成功。 平心而論,如果1-2-3沒有出現,個人電腦就很難騰飛,個人電腦的歷史也可能有截然不同的發展。 Lotus 1-2-3 錯誤地將 1900 年視為閏年。 當微軟發布第一個電子表格 Multiplan 時,它佔據了一小部分市場份額。 當他們啟動 Excel 項目時,他們決定不僅複製 Lotus 1-2-3 的行和列命名方案,而且還故意將 1900 年視為閏年,以確保錯誤兼容性。 這個問題今天仍然存在。 因此,在1-2-3 中這是一個錯誤,但在Excel 中,這是一個有意識的決定,以確保所有1-2-3 用戶都可以將其表導入Excel 而不更改數據,即使它是錯誤的。

但還有另一個問題。 首先,微軟發布了適用於 Macintosh 的 Excel,它不識別 1 年 1904 月 1 日之前的日期。而在 Excel 中,1900 年 XNUMX 月 XNUMX 日被認為是時代的開始。 因此,開發人員進行了更改,使他們的程序能夠識別時代類型並根據所需的時代在其內部存儲數據。 微軟甚至為此寫了一篇解釋文章。 這個決定導致了我的錯誤。

我的 ETL 系統從客戶那裡收到了在 Windows 上創建的 Excel 電子表格,但也可以在 Mac 上創建。 因此,表中紀元的開始可能是 1 年 1900 月 1 日,也可能是 1904 年 XNUMX 月 XNUMX 日。 如何找出? Excel 文件格式顯示了必要的信息,但我使用的解析器沒有顯示它(現在顯示了),並假設您知道特定表的紀元。 我可能可以花更多的時間來理解 Excel 二進制格式並向解析器作者發送補丁,但我還有很多工作要做,所以我很快編寫了一個啟發式方法來確定紀元。 她很簡單。

在Excel 中,日期5 年1998 月07 日可以用以下格式表示:“05-98-5”(無用的美國系統)、“Jul 98, 5”、“July 1998, 5”、“98-Jul- 8601”或一些其他格式,另一種無用的格式(諷刺的是,我的 Excel 版本不提供的格式之一是 ISO 35981)。 但是,在表中,未格式化的日期存儲為 epoch-1900 的“34519”或 epoch-1904 的“4”(數字表示自該紀元以來的天數)。 我只是使用一個簡單的解析器從格式化日期中提取年份,然後使用 Excel 解析器從未格式化日期中提取年份。 如果兩個值相差 1904 年,那麼我就知道我使用的是 epoch-XNUMX 的系統。

為什麼我不直接使用格式化日期? 因為 5 年 1998 月 98 日可以被格式化為“July, XNUMX”,並丟失該月的日期。 我們收到了來自許多公司的表格,這些公司以多種不同的方式創建它們,因此由我們(在本例中為我)來計算日期。 此外,如果 Excel 做得對,那麼我們也應該如此!

同時我遇到了 39082。讓我提醒您,Lotus 1-2-3 認為 1900 是閏年,並且在 Excel 中忠實地重複了這一點。 由於這會在 1900 年基礎上增加一天,因此許多日期計算函數在這一天可能會出現錯誤。 也就是說,39082 可能是 1 年 2011 月 31 日(在 Mac 上)或 2006 年 2011 月 1900 日(在 Windows 上)。 如果我的“年份解析器”從格式化值中提取出 2006 年,那麼一切都很好。 但由於 Excel 解析器不知道正在使用哪個紀元,因此它默認為 epoch-5,返回 XNUMX 年。 我的應用程序發現差異為 XNUMX 年,認為這是一個錯誤,將其記錄下來,並返回一個未格式化的值。

為了解決這個問題,我寫了這個(偽代碼):

diff = formatted_year - parsed_year
if 0 == diff
    assume 1900 date system
if 4 == diff
    assume 1904 date system
if 5 == diff and month is December and day is 31
    assume 1904 date system

然後所有 40 個日期都被正確解析。

在大型打印作業中

1980 世紀 XNUMX 年代初,我父親在存儲技術公司工作,該部門現已解散,該部門生產磁帶驅動器和用於高速磁帶輸送的氣動系統。

他們重新設計了驅動器,以便可以將一個中央“A”驅動器連接到七個“B”驅動器,而控制“A”驅動器的 RAM 中的小型操作系統可以將讀寫操作委託給所有“B”驅動器。

每次啟動驅動器“A”時,都需要將軟盤插入到連接到“A”的外圍驅動器中,以便將操作系統加載到其內存中。 它非常原始:計算能力由 8 位微控制器提供。

此類設備的目標受眾是擁有大型數據倉庫的公司(銀行、零售連鎖店等),這些公司需要打印大量地址標籤或銀行對賬單。

一位客戶遇到了問題。 在打印作業進行過程中,一個特定的驅動器“A”可能會停止工作,從而導致整個作業停止。 為了恢復驅動器的運行,工作人員必須重新啟動一切。 如果這種情況發生在一個六小時任務的中間,那麼就會浪費大量昂貴的計算機時間,並且整個操作的時間表會被打亂。

技術人員是從存儲技術公司派來的。 但儘管他們盡了最大努力,他們仍無法在測試條件下重現該錯誤:它似乎發生在大型打印作業的中間。 問題不在於硬件,他們更換了所有可以更換的東西:RAM、微控制器、軟盤驅動器、磁帶驅動器的每個可能的部分 - 問題仍然存在。

隨後技術人員給總部打電話,給專家打電話。

這位專家抓起一把椅子和一杯咖啡,坐在計算機房裡(當時有專門的計算機房間),看著工作人員排隊等待一份大型打印作業。 專家一直在等待故障發生——結果確實發生了。 所有人都看向高手,但他不明白為什麼會出現這種情況。 於是他下令工作重新排隊,所有工作人員和技術人員都返回工作崗位。

專家再次坐在椅子上,開始等待失敗。 大約六個小時過去了,故障發生了。 專家再次毫無頭緒,只是一切都發生在一個擠滿了人的房間裡。 他命令任務重新開始,然後坐下來等待。

第三次失敗時,專家注意到了一些事情。 當工作人員更換外部驅動器中的磁帶時,發生了故障。 而且,當一名員工走過地板上的某一塊瓷磚時,故障就發生了。

高架地板由鋁磚製成,鋪設高度為 6 至 8 英寸。 許多計算機電線都鋪設在高架地板下,以防止任何人意外踩到重要電纜。 瓷磚鋪得非常緊,以防止碎片進入高架地板下面。

專家意識到其中一塊瓷磚變形了。 當員工踩到瓷磚的角落時,瓷磚的邊緣會與相鄰的瓷磚發生摩擦。 連接瓷磚的塑料部件也會與瓷磚發生摩擦,從而引起靜電微放電,從而產生射頻干擾。

如今,RAM 得到了更好的保護,免受射頻干擾。 但在那些年,情況並非如此。 專家意識到這種干擾會擾亂內存,進而擾亂操作系統的運行。 他致電支持服務,訂購了新瓷磚,並親自安裝,問題就消失了。

漲潮了!

故事發生在朴茨茅斯(我認為)碼頭區一間辦公室四樓或五樓的服務器機房裡。

有一天,帶有主數據庫的 Unix 服務器崩潰了。 他們重新啟動了他,但他高興地繼續一次又一次跌倒。 我們決定給支持服務人員打電話。

支持人員...我認為他的名字是馬克,但這並不重要...我認為我不認識他。 真的沒關係。 我們還是和馬克一起吧,好嗎? 偉大的。

因此,幾個小時後,馬克到達(從利茲到朴茨茅斯並不遠,你知道),打開服務器,一切正常。 典型的該死的支持,客戶對此感到非常沮喪。 馬克查看了日誌文件,沒有發現任何異常情況。 於是馬克回到火車上(或者他乘坐的任何交通工具,據我所知,這可能是一頭跛腳牛……無論如何,這並不重要,好嗎?)然後返回利茲,浪費了時間那天。

當天晚上服務器再次崩潰。 故事是一樣的...服務器不升。 馬克嘗試遠程提供幫助,但客戶端無法啟動服務器。

另一趟火車,公共汽車,檸檬酥皮或其他一些垃圾,馬克回到了朴茨茅斯。 看,服務器啟動沒有任何問題! 奇蹟。 馬克花了幾個小時檢查操作系統或軟件是否一切正常,然後出發前往利茲。

中午左右,服務器崩潰了(別著急!)。 這次引入硬件支持人員來更換服務器似乎是合理的。 但不,大約10小時後它也會下降。

這種情況連續幾天重複出現。 服務器正常工作,大約 10 小時後崩潰,並且在接下來的 2 小時內無法啟動。 他們檢查了冷卻、內存洩漏,他們檢查了一切,但什麼也沒發現。 然後崩潰就停止了。

這一周無憂無慮地過去了……每個人都很開心。 快樂直到一切重新開始。 圖片是一樣的。 工作10小時,停機2-3小時...

然後有人(我想他們告訴我這個人與 IT 無關)說:

“是潮水了!”

這聲驚呼引起了茫然的目光,有人的手可能在安全呼叫按鈕上猶豫了。

“它不再隨著潮汐而起作用。”

對於 IT 支持人員來說,這似乎是一個完全陌生的概念,他們不太可能坐下來喝咖啡時閱讀《潮汐年鑑》。 他們解釋說,這與潮汐沒有任何關係,因為服務器已經工作一周,沒有出現任何故障。

“上週潮水很低,但這週潮水很高。”

為那些沒有遊艇執照的人提供的一些術語。 潮汐取決於月球週期。 隨著地球自轉,太陽和月球的引力每 12,5 小時就會產生一次潮汐波。 在12,5小時的周期開始時有漲潮,在周期的中間有退潮,在周期結束時再次漲潮。 但隨著月球軌道的變化,低潮和高潮之間的差異也會發生變化。 當月球位於太陽和地球之間或位於地球的另一側(滿月或無月)時,我們會得到 Syzygyn 潮汐 - 最高的高潮和最低的低潮。 在半月時,我們會得到正交潮汐——最低潮汐。 兩個極端之間的差異大大減小。 月球週期持續 28 天: 日月 - 正交 - 日月 - 正交。

當技術人員了解到潮汐力的本質時,他們立即想到需要報警。 而且很符合邏輯。 但事實證明,這傢伙是對的。 兩週前,一艘驅逐艦停泊在離辦​​公室不遠的地方。 每當潮水將其提升到一定高度時,船上的雷達站就會與服務器室地板齊平。 雷達(或電子戰設備,或其他一些軍事玩具)在計算機中造成了混亂。

火箭的飛行任務

我的任務是將一個大型(約 400 萬行)火箭發射控制和監視系統移植到新版本的操作系統、編譯器和語言。 更準確地說,從 Solaris 2.5.1 到 Solaris 7,從使用 Ada 83 編寫的 Verdix Ada 開發系統 (VADS) 到使用 Ada 95 編寫的 Rational Apex Ada 系統。VADS 被 Rational 購買,其產品是儘管Rational 嘗試實現VADS 特定包的兼容版本以簡化向Apex 編譯器的過渡,但已過時。

三個人幫助我乾淨地編譯了代碼。 花了兩週時間。 然後我自己努力讓這個系統運轉起來。 總之,這是我遇到過的最糟糕的軟件系統架構和實現,所以又花了兩個月的時間才完成移植。 然後該系統被提交進行測試,這又花了幾個月的時間。 我立即糾正了測試中發現的bug,但數量很快就減少了(源代碼是生產系統,所以它的功能運行得相當可靠,我只需要刪除適應新編譯器時出現的bug)。 最終,當一切都按預期進行時,我被轉移到另一個項目。

感恩節前的星期五,電話響了。

火箭發射原本應該在大約三週內進行測試,但在倒計時的實驗室測試期間,命令序列被阻止。 在現實生活中,這會中止測試,如果在啟動發動機的幾秒鐘內發生堵塞,輔助系統中就會發生一些不可逆轉的動作,這需要火箭進行長期且昂貴的準備。 它不會開始,但很多人會對時間和大量金錢的損失感到非常沮喪。 不要讓任何人告訴你國防部花錢魯莽——我從來沒有遇到過一個合同經理不把預算放在第一位或第二位,然後才是進度表。

在過去的幾個月裡,這個倒計時挑戰已經以多種形式運行了數百次,只出現了一些小問題。 因此,這種情況發生的可能性非常低,但其後果卻非常嚴重。 將這兩個因素相乘,您就會明白,該新聞預測我和數十名工程師和經理的假期週將被毀。

作為移植系統的人,我也受到了關注。

與大多數安全關鍵系統一樣,會記錄大量參數,因此很容易識別系統崩潰之前執行的幾行代碼。 當然,它們絕對沒有任何異常;在同一次運行中,相同的表達式實際上已經成功執行了數千次。

我們將 Apex 的人員召集到 Rational,因為他們是開發編譯器的人,並且他們開發的一些例程在可疑代碼中被調用。 他們(以及其他所有人)對有必要找到具有國家重要性的問題的根源感到印象深刻。

由於期刊中沒有任何有趣的內容,我們決定嘗試在當地實驗室重現該問題。 這不是一件容易的事,因為該事件大約每 1000 次運行發生一次。 一個可疑的原因是調用了供應商開發的互斥函數(VADS 遷移包的一部分) Unlock 並沒有導致解鎖。 調用該函數的處理線程處理心跳消息,該消息名義上每秒到達。 我們將頻率提高到10赫茲,即每秒10次,然後開始跑步。 大約一個小時後,系統自行鎖定。 在日誌中,我們看到記錄消息的順序與失敗測試期間的順序相同。 我們又進行了幾次運行,系統在啟動後 45-90 分鐘內一直被阻塞,並且每次日誌都包含相同的路線。 儘管我們在技術上運行不同的代碼 - 消息頻率不同 - 系統的行為是相同的,所以我們確信這種​​負載場景會導致相同的問題。

現在我們需要弄清楚表達式序列中阻塞發生的具體位置。

該系統的實現使用了 Ada 任務系統,但使用得非常糟糕。 任務是 Ada 中的高級並發可執行構造,類似於執行線程,僅內置於語言本身中。 當兩個任務需要通信時,它們“設置集合點”,交換必要的數據,然後停止集合點並返回各自的獨立執行。 然而,該系統的實施方式有所不同。 目標任務會合後,該目標任務會與另一個任務會合,然後該任務會與第三個任務會合,依此類推,直到完成某些處理。 在此之後,所有的交會都完成了,每個任務都必須返回執行。 也就是說,我們正在處理世界上最昂貴的函數調用系統,它在處理部分輸入數據時停止了整個“多任務”過程。 而在此之前並沒有導致問題只是因為吞吐量非常低。

我描述這個任務機制是因為當請求或期望完成集合時,可能會發生“任務切換”。 也就是說,處理器可以開始處理另一個準備執行的任務。 事實證明,當一個任務準備好與另一個任務交匯時,可以開始執行完全不同的任務,並最終控制權返回到第一個交匯點。 並且可能會發生其他事件導致任務切換; 其中一個事件是對系統函數的調用,例如打印或執行互斥鎖。

為了了解哪一行代碼導致了問題,我需要找到一種方法來記錄一系列語句的進度,而不觸發任務切換,這將防止發生崩潰。 所以我無法利用 Put_Line()以避免執行 I/O 操作。 我可以設置一個計數器變量或類似的東西,但是如果我無法在屏幕上顯示它,我如何才能看到它的值呢?

另外,在檢查日誌時發現,儘管心跳消息的處理被凍結,從而阻塞了進程的所有 I/O 操作並阻止了其他處理的執行,但其他獨立任務仍在繼續執行。 也就是說,工作並未完全被阻止,只是(關鍵)任務鏈被阻止。

這是評估阻塞表達式所需的線索。

我製作了一個 Ada 包,其中包含一個任務、一個枚舉類型和該類型的全局變量。 可枚舉文字被綁定到有問題的序列的特定表達式(例如 Incrementing_Buffer_Index, Locking_Mutex, Mutex_Unlocked),然後在其中插入賦值表達式,將相應的枚舉分配給全局變量。 由於所有這些的目標代碼只是在內存中存儲一​​個常量,因此其執行導致的任務切換是極不可能的。 我們主要懷疑可以切換任務的表達式,因為阻塞發生在執行時,而不是在切換回任務時返回(出於多種原因)。

跟踪任務只是循環運行並定期檢查全局變量的值是否已更改。 每次更改後,該值都會保存到文件中。 然後短暫的等待和一張新的支票。 我將變量寫入文件是因為只有在問題區域切換任務時系統選擇執行該任務時才會執行該任務。 該任務中發生的任何事情都不會影響其他不相關的被阻止任務。

預計當系統到達執行有問題的代碼時,全局變量將在移動到下一個表達式時重置。 然後會發生一些事情導致任務切換,由於其執行頻率(10 Hz)低於監控任務,監控器可以捕獲全局變量的值並將其寫入。 在正常情況下,我可以獲得枚舉子集的重複序列:任務切換時變量的最後值。 掛起時,全局變量不應再更改,最後寫入的值將指示哪個表達式未完成。

我通過跟踪運行了代碼。 他愣住了。 監控工作就像發條一樣運轉。

日誌包含預期的序列,該序列被一個指示互斥體已被調用的值中斷 Unlock,並且任務尚未完成 - 就像之前數千個調用的情況一樣。

Apex 工程師此時正在狂熱地分析他們的代碼,並在互斥鎖中找到了理論上可能發生鎖定的位置。 但其概率非常低,因為只有在特定時間發生的特定事件序列才會導致阻塞。 墨菲定律,伙計們,這就是墨菲定律。

為了保護我需要的代碼片段,我用一個小的本機 Ada 互斥包替換了互斥函數調用(構建在操作系統互斥功能之上),以控制對該片段的互斥訪問。

我將其插入代碼並運行測試。 七個小時後,代碼仍然有效。

我的代碼被提交給 Rational,他們在那裡對其進行編譯、反彙編,並檢查它是否使用了有問題的互斥函數中所使用的相同方法。

這是我職業生涯中最擁擠的代碼審查 🙂 房間里大約有 20 名工程師和經理和我在一起,另外 XNUMX 個人正在參加電話會議 - 他們都檢查了大約 XNUMX 行代碼。

代碼被審查,新的可執行文件被組裝並提交以進行正式的回歸測試。 幾週後,倒計時測試成功,火箭起飛。

好吧,這一切都很好,但是這個故事的重點是什麼?

這絕對是一個令人噁心的問題。 數十萬行代碼、並行執行、十多個交互進程、糟糕的架構和糟糕的實現、嵌入式系統的接口以及數百萬美元的花費。 沒有壓力,對吧。

儘管我在進行移植時處於聚光燈下,但我並不是唯一一個致力於解決此問題的人。 但即使我做到了,這並不意味著我理解了所有數十萬行代碼,甚至瀏覽了它們。 全國各地的工程師都對代碼和日誌進行了分析,但當他們告訴我他們對失敗原因的假設時,我只花了半分鐘就反駁了他們。 當我被要求分析理論時,我會把它傳遞給其他人,因為對我來說很明顯這些工程師走錯了路。 聽起來很自以為是嗎? 是的,這是事實,但我出於另一個原因拒絕了這些假設和要求。

我了解問題的本質。 我不知道具體發生在哪里或為什麼發生,但我知道發生了什麼。

多年來,我積累了很多知識和經驗。 我是使用 Ada 的先驅之一,了解它的優點和缺點。 我知道 Ada 運行時庫如何處理任務並處理並行執行。 我了解內存、寄存器和彙編器級別的低級編程。 換句話說,我在自己的領域有很深的知識。 我用它們來查找問題的原因。 我不僅僅解決了這個錯誤,我還了解如何在非常敏感的運行時環境中找到它。

對於那些不熟悉這種鬥爭的特徵和條件的人來說,這種與代碼鬥爭的故事並不是很有趣。 但這些故事可以幫助我們了解如何解決真正困難的問題。

要解決真正困難的問題,您需要的不僅僅是一名程序員。 您需要了解代碼的“命運”、它如何與其環境交互以及環境本身如何工作。

然後你的假期週就會被毀掉。

要繼續進行下去。

來源: www.habr.com

添加評論