一點項目經驗的總結


  前幾個月開發了一個服務器網關程序,測試過程中發現了一些問題,問題的查找與修改花了一個月的時間。好在經過一個月的修改與完善終於解決了所有問題。現在網關服務器在局域網內吞吐量最高可以達到120MB/S,穩定運行7*24小時,性能和穩定性都讓人滿意。 然而其中遇到的問題也不少,現在回想起解決過程是痛並快樂着,以此文作一個經驗教訓的總結。

  先說下項目背景,項目要求開發一個高性能的跨平台的網關服務器,主要性能指標是吞吐量達到70-80MB/s,並發連接數要到3000以上。現有的服務器性能達不到要求,最多不過30-40MB/s。這個項目也是我到新部門之后的首秀,也受到領導的高度關注。作為自己的首秀當然希望能充分發揮自己的能力,高質量的完成這個項目。項目開始就受到領導的高度關注和期待,我也是信心滿滿。先是技術選型,因為要考慮跨平台和高性能,最終決定采用asio作為底層通信庫。然后業務分析、需求分析、系統設計。剛開始我的設計目標是做得通用靈活易擴展,不僅僅是為了當前的項目,還要在今后能在其他項目中復用,成為一個平台,因此靈活性、通用性和可擴展性很重要,設計時就引入了AOP、IOC、插件、消息總線等基礎框架。具體設計上是分層,將底層通信與業務邏輯分開,業務邏輯采用插件方式,可以方便的實現業務邏輯的擴展和替換;業務類可以用IOC框架動態配置,以滿足不同的應用場景;通過AOP將非核心邏輯和核心邏輯分離開,提高可維護性;通過消息總線統一管理所有對象之間的通信,避免對象間復雜的調用關系。設計中充斥了大量的設計模式,初步數了一下,大概用到了十二三種設計模式。這對於一個平台級規模的軟件來說也不算復雜,設計充分考慮到了可擴展性、可維護性和靈活性。

  在完成設計后的概要設計評審會上,當我向參與評審的十幾位領導同事侃侃而談時,下面一片沉默,因為很多人都沒聽說過AOP、IOC等概念,也幾乎沒有提出設計上有什么不足。最后大領導問要多少人多長時間做完。我說4、5個人三四個月吧。結果領導說了一句讓我大跌眼鏡的話,只能給我兩個人,一個半月能不能做個基本的東西出來,后面再繼續完善,因為另外一個項目等着用。這是典型的中國式項目開發,只想着盡快完成,至於設計是否巧妙、質量是否高不是很關心。我只得無奈的說如果要一個多月的話,那很多東西就要從設計中砍掉,也不能考慮那么多質量屬性了,比如平台化、擴擴展性和靈活性等等。領導說沒關系,先做個基本的出來。沒辦法,這種事我也見怪不怪了,只是沒想到時間那么緊。項目開工了還是抓點緊干吧。先是學習asio,接着開發需要用到的各種基礎框架和基礎庫,雖然項目給的時間很短,但我還是想按照我的設計去開發,時間不夠就加下班了,做就做好,不要打折,我不想做一個自己不滿意的產品出來。等asio會用了、基礎框架搭好了、基礎庫完成了都過了一個月了。剩下的半個月趕緊將具體業務邏輯引入進來。中間還要感謝我的直接領導給我爭取了一些時間,最終是兩個多月把項目做完了。網關服務器的各項性能都達到要求了,吞吐量都遠高於設計目標,任務算是初步完成了。下圖是吞吐量和連接數測試過程中的截圖。

  接着一個任務又來了,算是網關服務器的一個具體應用:多個客戶端往服務器發送解碼數據,服務器將數據轉發到其他解碼的客戶端,解碼完之后再回發到服務器,服務器再將解碼之后的數據轉發到最終的客戶端那里。這個應用稍微有點復雜,涉及的方面比較多,有客戶端發送方,有解碼方,還有接收方。果然在開發過程遇到各種問題,先是解碼方經常掛掉,服務器有時候也掛掉,一時間問題集中出現,把人搞得焦頭爛額。經過問題排查,服務器掛了有兩個原因,一個是服務器收到錯誤包時異常處理不足導致的,另外一個是客戶端斷開連接后,服務器的asio異步操作還不知道,結果異步回調回來的時候對象本身已經析構,就報錯了。這個問題花了不少時間解決,其實解決辦法比較簡單,就是保持對象的生命周期就行了,通過shared_from_this解決。解決服務器的問題之后,又要解決解碼客戶端掛了的問題,發現是解碼器有問題,然后又是各種改,終於沒有掛掉的現象出現了,松了一口氣。不久發現又出現一個詭異的問題:系統運行一段時間后,所有的進程都阻塞住了,也不收數據也不發數據,也沒有報錯。真是詭異,然后又是各種查,懷疑是解碼器的問題,干脆就不解碼,將數據直接回發到服務器,但仍然出現這個問題。然后各種可能的地方都打印調試,還是沒解決問題,這時是最讓人沮喪的時候,我開始懷疑之前所有的代碼,因為覺得設計得有點復雜,調試起來還挺麻煩,感覺自己被自己坑了,干嘛搞得這么復雜,甚至有了重寫的念頭,就這樣毫無頭緒的過了一周還是沒有進展。最后冷靜的分析了一下,覺得有可能是socket死鎖造成的,當一個連接雙工通信時,即這個連接又發數據又收數據,並且數據量很大時,造成對方的接收緩沖區滿了,這時兩邊的發送都阻塞在發送了,死鎖發生了!想清楚這個問題之后,就改成單工通信了,雙工通信的話就建兩個連接, 一個收一個發,這樣就不會出現死鎖問題了。又可以松口氣了。

  不料又出現一個詭異的問題,解碼開啟之后又出現所有進程阻塞的情況。剛開始以為是解碼器內部多線程死鎖導致的,然后各種改,保證線程安全不死鎖,情況仍然出現,按理說也不應該是socket死鎖,因為已經改為兩個連接分別去收發了,而且將回發的數據發到另外一個服務器就不會出現阻塞。又是各種查,還是沒找到原因,這時甚至開始懷疑asio是不是有bug。最后決定調試到asio內部去看看到底死在哪里了,結果讓人很意外的發現兩邊還是阻塞在發送那里了,還是出現了socket死鎖!天,兩個連接都會產生socket死鎖,簡直要讓人抓狂了。為什么兩個連接還會出現死鎖?其實原因還是在解碼。因為解碼比較耗時導致服務器發數據會被阻塞,而客戶端解碼完之后回發時,io_service因為還在阻塞着,導致接收緩沖區慢慢的填滿了,在某個時刻兩邊的接收緩沖區都滿了,就都又阻塞在發送了,死鎖再一次發生了!本質上是因為一個io_service時兩個連接依然會產生一個環,形成死鎖。解決辦法是啟用兩個io_service,消除這個環就能避免死鎖了。這時出現一個比較囧的問題,由於對象間通信都是通過消息總線,而消息總線是一個單例,使用兩個含io_service對象時會導致重復注冊,這時消息總線的副作用出現了,雖然它能很好的解耦對象間復雜的關系,但也帶來了調試不直觀,相同對象出現重復注冊的情況,這也是當初設計時始料未及的問題,好在問題不大,最后通過兩個io_service來消除環,解決死鎖問題,現在系統運行得很穩定。 再回過頭來看,讓人感慨寫一個高性能又穩定的服務器程序真是不容易啊。中間解決問題的過程一波三折,充滿挑戰,從開始的煩惱、懷疑到后面的從容與自信,最后終於解決所有問題,覺得還是挺有成就感的,呵呵。

教訓之一:不要過渡設計,不要玩太多技巧;

  由於大量的引入了一些框架和設計模式,導致調試的時候不方便,查問題的時候也要繞半天,最后發現其實現實沒有那么多變化,不需要這么復雜的設計。靈活性和高可擴展性是有代價的,程序在滿足當前需求且夠用的前提下,應該盡可能的簡潔明了,不要玩那么多技巧,所謂“重劍無鋒,大巧不工”,認真考慮一下一個系統是否有那么多變化,不要考慮“誇誇其談的未來性”,夠用就好,簡單就好,這樣好處很多,一來方便調試,二來方便維護,三來減少犯錯誤的可能。我在后面的代碼重構過程中摒棄了很多模式和框架,只要求簡單夠用,如果將來真的有變化的時候我再重構到模式、應用一些框架也不遲。再重溫大師Kent Beck提出的編程中的價值觀:溝通,簡單,靈活。其中簡單和靈活是排在前面的,這是很有道理的。

教訓之二:要注意asio的一些坑;

  一個io_service時,雙工通信或者多個socket發送,通過io_service形成環可能會造成死鎖,需要注意。解決辦法是消除環。

經驗之一:保持代碼的高質量;

  我對自己的代碼還是滿意的,雖然時間那么緊,但我仍然堅持我的編碼原則,並且在有空時用代碼檢查工具檢查我的代碼,發現有問題不合原則的代碼馬上重構。在此也分享一下自己的編碼原則:

1、過長的函數(不能超過12行)

2、重復代碼(3行重復的代碼)

3、類過大(不能超過500行);

4、過長的邏輯表達式(表達式不超過兩個邏輯體;表達式文本長度不超過40個字符);

5、不能有魔法數;

6、圈復雜度不能超過5;

7、函數參數列表過多(參數列表不要超過5個);

8、switch語句,盡量不用;

9、過多的注釋;

  最后代碼檢查,都符合上述原則,這也是我比較滿意的地方,不要以時間緊為借口,編寫高質量的代碼要成為一種習慣、一種責任,要為自己的代碼負責。

經驗之二:不要為自己的懶惰找借口,一定要重構。

  當嗅到代碼的壞味道的時候,一定要重構,不要怕麻煩,代碼是會慢慢腐化的,壞味道的代碼就是技術債務,必須定期清理,不處理的話,腐化會越來越快。重構是為了讓事情做得更好,通過重構可以改善設計、提高代碼可讀性、減少錯誤,提高軟件質量。哪怕當前的代碼可以工作,也要經常重構,保持對代碼壞味道的警惕性。

 

c++11 boost技術交流群:296561497,歡迎大家來交流技術。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM