前幾個月開發了一個服務器網關程序,測試過程當中發現了一些問題,問題的查找與修改花了一個月的時間。好在通過一個月的修改與完善終於解決了全部問題。如今網關服務器在局域網內吞吐量最高能夠達到120MB/S,穩定運行7*24小時,性能和穩定性都讓人滿意。 然而其中遇到的問題也很多,如今回想起解決過程是痛並快樂着,以此文做一個經驗教訓的總結。c++
先說下項目背景,項目要求開發一個高性能的跨平臺的網關服務器,主要性能指標是吞吐量達到70-80MB/s,併發鏈接數要到3000以上。現有的服務器性能達不到要求,最多不過30-40MB/s。這個項目也是我到新部門以後的首秀,也受到領導的高度關注。做爲本身的首秀固然但願能充分發揮本身的能力,高質量的完成這個項目。項目開始就受到領導的高度關注和期待,我也是信心滿滿。先是技術選型,由於要考慮跨平臺和高性能,最終決定採用asio做爲底層通訊庫。而後業務分析、需求分析、系統設計。剛開始個人設計目標是作得通用靈活易擴展,不只僅是爲了當前的項目,還要在從此能在其餘項目中複用,成爲一個平臺,所以靈活性、通用性和可擴展性很重要,設計時就引入了AOP、IOC、插件、消息總線等基礎框架。具體設計上是分層,將底層通訊與業務邏輯分開,業務邏輯採用插件方式,能夠方便的實現業務邏輯的擴展和替換;業務類能夠用IOC框架動態配置,以知足不一樣的應用場景;經過AOP將非核心邏輯和核心邏輯分離開,提升可維護性;經過消息總線統一管理全部對象之間的通訊,避免對象間複雜的調用關係。設計中充斥了大量的設計模式,初步數了一下,大概用到了十二三種設計模式。這對於一個平臺級規模的軟件來講也不算複雜,設計充分考慮到了可擴展性、可維護性和靈活性。編程
在完成設計後的概要設計評審會上,當我向參與評審的十幾位領導同事侃侃而談時,下面一片沉默,由於不少人都沒據說過AOP、IOC等概念,也幾乎沒有提出設計上有什麼不足。最後大領導問要多少人多長時間作完。我說四、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提出的編程中的價值觀:溝通,簡單,靈活。其中簡單和靈活是排在前面的,這是頗有道理的。多線程
一個io_service時,雙工通訊或者多個socket發送,經過io_service造成的環可能會形成死鎖,須要注意。解決辦法是消除環。併發
我對本身的代碼仍是滿意的,雖然時間那麼緊,但我仍然堅持個人編碼原則,而且在有空時用代碼檢查工具檢查個人代碼,發現有問題不合原則的代碼立刻重構。在此也分享一下本身的編碼原則:框架
一、過長的函數(不能超過12行)異步
二、重複代碼(3行重複的代碼)socket
三、類過大(不能超過500行);
四、過長的邏輯表達式(表達式不超過兩個邏輯體;表達式文本長度不超過40個字符);
五、不能有魔法數;
六、圈複雜度不能超過5;
七、函數參數列表過多(參數列表不要超過5個);
八、switch語句,儘可能不用;
九、過多的註釋;
最後代碼檢查,都符合上述原則,這也是我比較滿意的地方,不要以時間緊爲藉口,編寫高質量的代碼要成爲一種習慣、一種責任,要爲本身的代碼負責。
當嗅到代碼的壞味道的時候,必定要重構,不要怕麻煩,代碼是會慢慢腐化的,壞味道的代碼就是技術債務,必須按期清理,不處理的話,腐化會愈來愈快。重構是爲了讓事情作得更好,經過重構能夠改善設計、提升代碼可讀性、減小錯誤,提升軟件質量。哪怕當前的代碼能夠工做,也要常常重構,保持對代碼壞味道的警戒性。
c++11 boost技術交流羣:296561497,歡迎你們來交流技術。