架構設計之六個複雜度來源
但是究竟複雜度有哪些呢?所以今天藉此說說軟體複雜度的六個來源:
1.高效能;
2.高可用;
3.可擴充套件性;
4.低成本;
5.安全;
6.規模;
一、高效能
對效能孜孜不倦的追求是整個人類技術不斷髮展的根本驅動力。例如計算機,從電子管計算機到電晶體計算機再到積體電路計算機,運算效能從每秒幾次提升到每秒幾億次。但伴隨著效能越來越高,相應的方法和系統複雜度也是越來越高。現代的計算機CPU集成了幾億顆電晶體,邏輯複雜度和製造複雜度相比最初的電晶體計算機,根本不可同日而語。
軟體系統也存在同樣的現象,比如以淘寶為例,最早期間可能最多支撐幾千人的訪問,但是,隨著使用者群體的增長和業務規模的擴大,已經遠遠不止幾千人,這時對於效能方面就要求很高,總不可能,使用者買個東西點選搜尋或者檢視商品詳情出現卡頓現象吧。
軟體系統中高效能帶來的複雜度主要體現在兩方面,一方面是單臺計算機內部為了高效能帶來的複雜度;另一方面是多臺計算機叢集為了高效能帶來的複雜度。
(1)單機複雜度
以我公司最早專案來說,剛開始一個伺服器對一個專案,那個時候還好,很少出現伺服器宕機現象,但是當一臺機器上執行好幾個tomcat,而且好幾個tomcat上都分別放置web專案,那個時候就是一臺伺服器,測試和線上都在一個伺服器,最後總是頻繁宕機,雖說,當時靠著shell指令碼時刻監控著,發現宕機,就自動重啟。但是那也無濟於事,後來改變了下,將所有的專案集中在一個tomcat上,將這個tomcat的優化程度做到最高,關於tomcat優化,可以參考我的這篇部落格:ofollow,noindex" target="_blank">Jmeter之tomcat效能測試+效能改進措施
然後在這個基礎上再做了nginx動靜分離和負載均衡。也許有人問什麼是動靜分離?
簡單的說,就是由Nginx處理靜態資源(img,css,js等之類),Tomcat處理動態請求(比如介面或者jsp之類的),Nginx作為高效能Web伺服器,自然對於靜態資源的處理效率高的多。
這個負載均衡做的有點名不符實,僅僅只是在一個tomcat上。不過在做了將tomcat優化配置和Nginx相關優化配置和動靜分離等後,發現網站的效能變的高了,關於網站效能測試可以參考這篇文章:網站線上效能測試分享
這個效能不僅僅指的是網頁效能,同時還包含併發,目前併發仍然不是特別高。
上面我只是提提最初的到後來改變,但是一個伺服器上隨著後面我們慢慢有了私服和jenkins,還有裝上了redis和phpmyadmin等,每個軟體都會有對應的程序執行者,這也會佔用很多記憶體。最後雖然勉強執行起來,但是時不時還是回到了卡頓,最後我只能將一些不必要的去掉(也許有人說,再買個伺服器唄,反正現在的阿里伺服器也很便宜,但是呢,經理說了,目前這個伺服器只是測試伺服器,以後正式上線再買一個好的)。
有點說偏了,最後提一下,單機複雜度的因素主要是程序和執行緒,,比如程序之間通訊、多程序、多執行緒、多執行緒併發等。
(2)叢集的複雜度
叢集的複雜度主要體現在這麼幾個方面?
a.任務分配
比如當伺服器由一臺變成十臺甚至上百臺時,如何保障對應的請求分發獲得對應的響應呢(web的本質基本上還是遵循HTTP協議,同學們,有時間還是得好好回顧回顧一下HTTP,關於HTTP相關的,我覺得朋友們可以參考我的這篇文章:談談HTTP)?
b.任務分解
通過任務分配的方式,我們能夠突破單臺機器處理效能的瓶頸,通過增加更多的機器來滿足業務的效能需求,但如果業務本身也越來越複雜,單純只通過任務分配的方式來擴充套件效能,收益會越來越低。為此,為了解決這個問題,於是就有了任務分解。
任務分解,在此你可以理解,如果一個專案有上百個子系統,可以將子系統分類並放在不同的伺服器上,以此達到一個系統只辦一件事(系統只辦一件事總比系統辦好幾件事情效率要高的)。
考慮到依賴性,如果好幾個系統雜糅一起,不做好合理的業務分離,最後可能演變成這樣,修改了A系統,還得修改B系統,修改了B系統,還得修改C系統,這是一件非常麻煩的事情。相信,無論是專案經理還是開發的小夥伴們都不願意看到這種現象的出現。另外還有一點需要強調的是,分解後,模組變小,有助於更好的發現問題。
針對高效能,摘抄李運華先生提出的一個問題,問題如下:
你所在的業務體系中,高效能的系統採用的是哪種方式?目前是否有改進和提升的空間?
我對此的回答如下:
我們目前開發出的三個系統,由於目前沒有完全上線為客戶服務,對於效能方面要求不是特別高。
不過要問,採取的是上面哪種方式,目前僅僅只是Nginx負載均衡+動靜分離,還僅僅只停留在單體應用上面。也沒有做一些主從複製、讀寫分離之類的。目前用不到。
如果要說未來擴充套件方面,目前,根據我們經理的預測,我談談我的想法,我覺得我們將來更偏向於叢集+任務分解方面。原因很簡單,複雜問題簡單化。不過在此情況下,一定會遵循前面提到的架構設計三原則,對此有興趣的朋友,可以讀讀:架構設計三原則
目前的改進和提升空間,我覺得目前的改進和提升空間不在於架構方面,而在於規範方面。很多規矩都不是特別成型,顯得有點散,不過目前相比之前,還是有很大的進步,為此我也感到高興,我很害怕團隊退步,畢竟一個團隊的氛圍對於一個人影響還是比較大的,不過我覺得當團隊規模逐漸擴大時,這個問題,一定會迎刃而解的,目前,只所以這樣,也考慮到一個重要的因素,那就是人員的流動性(眾所周知,對於創業團隊,特別是有一段時間,如果團隊中有人離職,將會給這個團隊帶來不少的損失)。
二、高可用
什麼是高可用?
系統無中斷地執行其功能的能力,代表系統的可用性程度,是進行系統設計時的準則之一(摘自維基百科)
維基百科說的有點太官方了,沒有百度百科這麼直接。百度百科是怎麼說的呢?如下所示:
“高可用性”(High Availability)通常來描述一個系統經過專門的設計,從而減少停工時間,而保持其服務的高度可用性。
關鍵字在於“可用”,以諾基亞手機來說,諾基亞手機質量確實不錯,用的好幾年覺得還是那麼的好用,說明諾基亞的可用性還是不錯的(雖說功能並沒有目前哦蘋果、安卓手機那麼強大,但是它的質量是沒的說的)。
另外再說一點,高效能和高可用是有根本的區別的:
高效能增加機器的目的在於“擴充套件”處理效能;
高可用增加機器的目的在於“冗餘”處理單元。
通過冗餘增強可用性,但同時也帶來複雜性。
(1)計算高可用
這裡的“計算”,指的是業務的邏輯處理。計算有一個特點就是無論在哪臺機器上進行計算,同樣的演算法和輸入資料,產出的結果都是一樣的,所以將計算從一臺伺服器遷移到另外一臺伺服器,對業務並沒有什麼影響。
可以以單機架構和雙機架構對比,
單機架構:客戶端-服務端
雙機架構:客戶端-任務分配器-服務端(多了這個中間層,不要小看這個中間層,目前的寬頻上網之所以能夠確保使用者上網隱私不被侵犯,很大原因是因為我們是通過路由器來上網,筆記本電腦-路由器-外網,我們只需連線路由器即可,由路由器連線外網上網,對於外網而言,它只知道是路由器,而不知道是我們)。
(2)儲存高可用
對於需要儲存資料的系統來說,整個系統的高可用設計關鍵點和難點在於“儲存高可用”。儲存與計算機相比,有一個本質上的區別:將資料從一臺機器搬到另外一臺機器,需要經過線路進行傳輸。線路傳輸的速度是毫秒級,同一機房內部能夠做到幾毫秒;分佈在不同地方的機房,傳輸耗時需要幾十甚至上百毫秒。
雖然毫秒對於人來說幾乎沒有什麼感覺,或者是說完全就沒有什麼感覺,但是對於高可用系統而言,就是本質上的不同。按照“資料+邏輯=業務”的這個公式來套,資料不一致,即使邏輯一直,但是最終的業務表現就會不一樣,沒有達到預計的效果。
(3)高可用業務決策
無論是計算高可用還是儲存高可用,其基礎都是“狀態決策”,即系統需要能夠判斷當前的狀態是正常還是異常,如果出現了異常就要採取行動保證高可用。如果狀態決策本身是有問題的,那麼後續的處理和行動都是沒有意義的。另外有一點要強調一下,狀態決策本質上就不可能做到完全正確。
a.獨裁式
天下之大,唯我獨尊。所有的都要聽我的。比如,以集中式舉例,SVN是一種集中式的版本控制系統,如果其中央倉庫出了問題,會影響整個專案團隊的研發。關於SVN和Git的對比分析,可以參考我的這篇文章:SVN和Git的比較
b.協商式
常見的表現就是主備,可以聯絡到生活實際,男女交往,有不少男性朋友或者不少女性朋友淪為備胎。也許這個例子有些不恰當,但是往往也可以說明一點,當主機出現問題時,不能正常提供服務,備機替代主機保障正常服務,不受宕機的影響(聯絡到伺服器)。
c.民主式
這個民主式我一時看不太明白,不過我認為前兩種是比較常見的(a,b) 。
摘自李運華先生提出的一個問題,問題如下:
高效能和高可用是系統的核心複雜度,你認為哪個會更復雜一些?理由是什麼?
我的回答如下:
我覺得不管是高效能還是高可用,前提是確保業務正常服務於客戶。
理由如下:要說複雜度,我認為還是取決於業務,比如有的業務它並不需要高效能,比如特定公司的人群定製化使用的辦公軟體,只需要保障客戶使用的時候不會出現一些功能方面的錯誤或者是業務方面的bug。
三、可擴充套件
可擴充套件性指的是應對將來需求變化而提供的一種擴充套件能力,當有新的需求出現時,系統不需要或者僅需要少量修改就可以支援,無需整個系統重構或者重建。
回憶當初我們的PMS系統,為了實現某個功能,牽其一必動其餘,那段心酸加班的苦日子,至今讓我難以忘記。當然了,也許當初的考慮沒有那麼周到,寫的程式碼也不夠嚴謹規範,當然了,最重要的就是對業務不瞭解熟悉(犯了沒有完全理解需求就直接開敲這樣的錯誤,我想每個工作年限不長的人或者工作年限小於兩年的都可能犯這樣的錯誤)。
記得當初我還有一個這樣的想法,幻想著將20多種設計模式全部應用到程式碼中,只要應用到程式碼上,程式碼質量、專案質量都會提高。不過,幸運的是我並沒有那麼做。
原因有這麼幾點?
(1)我自己對設計模式而言,僅僅只是知道它的名字,不知道它在實際的應用場景和怎麼寫的;
(2)由於(1)緣故,在我心中有這麼個理念,沒有把握的事情絕不幹(雖然生活中幹了不少沒有把握的事情,比如曾經對某某一見鍾情,就對其表白,最後的結果只能是以失敗告終,如果結果對方同意,說明了兩點,第一點,的確情投意合;第二點,這是一個陷阱)
(3)當時手裡專案太多,由於之前系統遺留的問題,不停的解決問題,當然了,當解決以後,面對這麼臃腫的專案,我只想說四個字(四個字是什麼我就不說了,有些不文明),不過的確面對一個很蛋疼的專案,要重構的話,不僅僅是時間問題,更重要是勇氣,當然了,還有必不可少的籌備。
扯了一些相關又無關的話,下面進入正題說說。
首先有一個問題,什麼才算是一個可擴充套件性良好的系統?可擴充套件性良好的系統具備哪些條件?
其實這兩個問題可以理解為一個問題,不過我覺得還是分開說好一些,因為更全面。
針對第一個問題,我給出的回答是:
可擴充套件性良好的系統,用一句簡單粗暴的話來說就是,面對產品經理變態的需求,你不需要內心暴打產品經理一百遍,不需要加班加點。一個字,就是“幹”。這個“幹”,不是之前痛苦的“幹”,而是快樂的“幹”。正常來說,之所以程式員對於產品經理抱著仇視的態度,關鍵不在於需求怎麼樣,而在於,實現這個需要需要改很多地方,改完很多地方,有可能又會出現新的問題,這才是最要命的。快樂的實現,在於將需求吃透的前提下,不需要改完這個改那個,只需在此基礎上擴充套件就能達到這個目的。
我想如此,便是一個可擴充套件性良好的系統。
針對第二個問題,我給出的回答是:
一般在設計一個可擴充套件性良好的系統,需要具備這麼兩個基本條件:
(1)正確預測變化;
(2)完美封裝變化;
關於(1),軟體系統與硬體系統或者建築相比,有一個很大的差異:軟體系統在釋出後還可以不斷地修改和演進,這就意味著不斷有新的需求需要實現。如果新的需求能夠不需要改動程式碼或者是少改動程式碼就可以實現,肯定是皆大歡喜的,否則來一個需要就要求系統大改一次,成本會非常高,程式設計師心裡也不爽(改來該去),產品經理也不爽(做的那麼慢),老闆更不爽(那麼多人就只能幹這點事情,要你們何用)。因此作為架構師,我們總是試圖去預測這些變化,然後設計完美的方案來應對,當下一次需求真正來臨時,架構師可以自豪地說:這個我當時就已經預測到了,架構能夠完美地支援,只需幾個小時就可以了(在團隊成員眼裡是多麼牛逼的存在)。
有一句名言叫做:理想很豐滿,現實很骨感。
在IT界中有一句諺語叫做:唯一不變的是變化。
這句話值得細細品味,比如如果架構師在每個設計方案都要考慮可擴充套件性,那麼架構師會不堪重負,有一句話叫做,魚和熊掌不可兼得。
另外關於預測,預測本身就暗示了不可能每次預測都是正確的(如果每次預測都是正確的,你可以去做預言家了)。
關於預測變化的複雜性,主要有這麼幾個體現:
a.不能每個設計點都考慮可擴充套件性;
b.不能不完全考慮可擴充套件性;
c.所有的預測都存在出錯的可能性;
對於架構師來說,如何把握預測的程度和提升預測結果的準確性,是一件很複雜的事情,而且沒有通用的標準可以簡單套用,更多的是靠自己的經驗、直覺,所以架構評審時,經常會出現兩個設計師對某個判斷爭的面紅耳赤的情況,原因在於沒有明確標準,不同的人理解和判斷有偏差,而最終又只能選擇一個判斷。
關於(2),為了應對變化,通常將變化封裝起來。
常見的方案有這麼幾個?
a.將“變化”封裝在一個變化層,將不變封裝在一個獨立的穩定層;
b.提煉出一個抽象層和一個實現層;
關於a,可以用一句話來概括“不同的人辦理不同的事情”,比如在社會生活中有形形色色的人,有的人善於阿諛奉承,有的人踏踏實實,阿諛奉承的人總有一天會因為某些事情而做了嫁衣,而踏踏實實的人卻不受到半點影響,這個社會需要實幹家。在寫程式碼中可以反映出,一個函式只辦一件事情,變化層應對那些時常變化的,穩定層就待在它的穩定層,這樣也解耦,不會有影響變化層有問題而還要改穩定層,不會因為穩定層有問題而改變化層。
關於b,對於抽象層和實現層,對於Java開發很常見,比如介面和實現類就是一個很好的體現。
來自李運華先生的思考題:
你在具體程式碼中使用過哪些可擴充套件的技術?最終的效果如何?
針對這個問題,我對此的回答是:
可擴充套件的技術倒沒有使用太多,主要是從程式碼上擴充套件,比如從簡單方面做起,每次Controller都要列印對應的日誌,我讓AbstractController繼承log4j日誌,這樣每次Controller需要列印日誌不用每次在類裡面宣告一下。還有就是將Controller程式碼簡化,比如之前Controller太過繁雜,以至於業務邏輯全面在Controller裡面,其實最好還是在service中將其寫清楚,Controller方面能簡化則簡化。當然了,還有就是統一程式碼規範,這裡我參考了《阿里巴巴的Java開發手冊》,同時也自定義一些特定元件。
最終的效果是,程式碼耦合性降低,程式碼的可讀性提高,可擴充套件性提高很多。效果雖然沒有達到我預想的那樣,但是已經進步了不少。這也是一件讓人高興的事情。
小結:
這兩天由於合作方的需要編寫了大量文件,這篇文章預計應該昨天就發表的,但是還沒有寫完。今天仍然沒有寫完,因為還有低成本、安全、規模等三個方面還沒有講到,我準備留到下次再講,我覺得高可用、高效能、可擴充套件性等三個方面,大家可以仔細讀讀,多多思考。一定會有你想不到的收穫。