我最近犯了5個極愚蠢的錯誤
譯文【51CTO.com快譯】我們大家都時不時地犯錯誤……而且,有時我們會一次犯很多錯誤!Grzegorz Ziemonski以一種令人愉快的方式和坦誠的態度與我們一起回顧了他在一次關鍵性Java應用程序發行調試過程中一路走來所犯的錯誤。
來自于DZone團隊的Michael Tharrington最近向我建議,我應該寫一寫作為一個開發者我所犯過的一些錯誤和從中取得的教訓。好吧,現在做這件事實在是最恰當不過——我的團隊剛剛上線一款關鍵的應用程序,而一切可能出錯的地方都出現錯誤了!當然,并非下文提供的每一個解決方案都完全是由我編寫的——其中一些是我們在敏捷開發中結對編程時所產生的,而一切都通過一個嚴格的代碼審查過程——也正是這一點使得問題更加可笑。
問題產生的背景
我們正在開發一款簡單的Web應用程序,此程序能夠暴露一個REST API以便與一個外部供應者程序進行通信并能夠把一些結果***地存儲起來;這樣的話,我們就不需要做太多的調用,因為我們不得不為每一次API調用付費。該應用程序主要依賴于Spring Boot這個Web框架及數據功能。該應用程序的兩個實例將部署在公司的支持負載平衡器的服務器上。
- 錯誤1—進程并發錯誤
對外部提供者程序的調用需要花費很長的時間;自然,我們的客戶端應用程序都不愿意等待這種類型的調用。正因為如此,我們都同意通過后臺處理方式加以改進——***次調用時,我們返回一條消息“DONT_KNOW_YET_COME_BACK_LATER...”。下次調用時,我們返回一個持久性的結果。于是,自然就出現這樣一個問題:如果我們在足夠短的時間內進行兩次相同的調用,那么會發生什么情況呢?靈感突然出現:我們必須以某種方式保護自己避免使用相同的數據兩次調用外部提供者程序。下面給出我的初步的編程方案:
- private Set<Long> currentlyProcessedIds = ConcurrentHashMap.newKeySet();
- // further down in the code:
- if (currentlyProcessedIds.add(id) {
- doTheJob(id);
- currentlyProcessedIds.remove(id);
- }
你能找到這段代碼有什么毛病嗎?如果你馬上發現不了我的愚蠢錯誤,慢慢來就是。
當然,如果doTheJob部分拋出異常的話,整段程序會中斷!這是典型的進程并發錯誤,導致明顯的內存破壞!遺憾的是,四個人盯著這段代碼卻沒人注意到,其實我們可以阻止id再次被正確處理。下面是更正后的代碼的正確版本:
- if (currentlyProcessedIds.add(id) {
- try {
- doTheJob(id);
- } finally {
- currentlyProcessedIds.remove(id);
- }
- }
- 錯誤2—負載已平衡,但問題仍存在
如果你足夠聰明,你可能已經注意到哪里出現錯誤了——上面的這段代碼仍然存在問題!我們在一臺負載平衡器上部署了應用程序的兩個獨立的實例。這意味著,問題并不只是并發方面的,還與分布式有關系!
目前,我們還沒有實現一個分布式的解決方案,但我們明顯需要實現兩個應用程序實例間對當前處理過的數據集進行同步。
后臺處理的想法來自于我觀察舊式解決方案的結果。這種老式解決方案嚴重依賴于數據庫的性能,但即使在最糟糕的情況下,也比外部調用要快。一開始,我們建議負責客戶端應用程序的程序員可以允許***次(長)調用超時,然后稍后再試。但是,實際運行情況表明:當整個系統負載很重時這種方案還很不理想。我負責測算我們的應用程序響應一個時間幀需要多長時間。我找到舊解決方案中所有的數據庫查詢并一個接一個地針對我們的數據庫進行測試。我收集這些結果,作出一些計算,***制作成一個不錯的表并把它放在我們公司的Wiki上。
到底哪里出錯了?好,我一邊喝著早餐咖啡一邊手工運行查詢,結果發現這次的系統性能大大不同于在重負荷下運行所有系統的性能。這意味著,我的計算以前過于樂觀,而在產品上線之后我們看到的***件事成為客戶端應用程序超時的“一面墻壁”。
- 錯誤4—使用過少的測試數據
現在來看,系統負載只是我們高估了系統性能表現的原因之一。第二個原因更令人尷尬!我們的應用程序和客戶端程序之間的所有測試都基于一組相當有限的數據集——大約1000條記錄。結果看起來非常成功,因為測試過程中沒有出現過一點警告標志。
其實,我們忽略了一個小小的細節,那就是:系統上線前,我們使用從舊解決方案中遷移的數據填充數據庫——這些數據大約有100萬條記錄!數據集比原來增加了三個數量級!我們這才發現自己忘記了設置關鍵的數據庫索引——這是一款關鍵應用程序上線前你必須且心甘情愿要做的事情!
- 錯誤5—度量并非統計結果
商界人士要求我們準備一份有關我們向外部提供者程序發出請求的報告。因為我們做了大量的調用,而且這些調用都很昂貴,所以我們想給他們提供一種方式來計算預期成本和驗證我們從供應商那里得到的發票。當然,業務人員不懂SQL,所以他們要求通過電子郵件發送給他們一個Excel報表。哦,我的娘……要求我們生成Excel文件并發送電子郵件?如今都是2016年了。實在是沒有辦法!最終,我們還是提供了一個漂亮的界面來實現統計度量。以后我們還將添加一個針對發出請求次數的計數器!
因為我們有了如上文所述的那些最初的問題,所以我們發出一些修補程序并再次發布應用程序。經過重新部署,顯示有計數器的儀表板看起來很不正常。請求的次數發生了什么事?情況是:度量數據保存在應用程序端,并每隔固定的時間間隔發送到度量服務端。這意味著,每次我們重新啟動應用程序,計數器的值都將消失了。簡單是廢話!
幸運的是,我們有一種方法能夠提供有關請求的正確信息,而沒有使用儀表板上的漂亮計數器,但我們仍然在試圖實現一個真正的計數器,以保持業務的統計數字。
總結
就在我們的產品最終上線之前,我曾經告訴我的同事說:我有些擔心,因為從項目開始到測試階段都不曾遇到過重大的問題,一切顯得那么順利,沒有出現過重大障礙。事實證明我是正確的,錯誤的東西注定會發生!雖然我在這篇文章中提到的錯誤現在看起來都是很明顯的,但是當我們趕時間進行開發和測試時它們卻不是那么明顯?,F在,寫出關于我們的項目開發中的這些羞于啟齒的事是有點兒尷尬;但是,我希望作為讀者的您至少能夠從中獲得一些樂趣,并且以后不會再犯和我們同樣的錯誤!
【51CTO譯稿,合作站點轉載請注明原文譯者和出處為51CTO.com】