朱晔的互联网架构实践心得S1E7:三十种架构设计模式(上)
設(shè)計模式是前人通過大量的實踐總結(jié)出來的一些經(jīng)驗總結(jié)和最佳實踐。在經(jīng)過多年的軟件開發(fā)實踐之后,回過頭來去看23種設(shè)計模式你會發(fā)現(xiàn)很多平時寫代碼的套路和OO的套路和設(shè)計模式里總結(jié)的類似,這也說明了你悟到的東西和別人悟到的一樣,經(jīng)過大量實踐總能趨向性得出一些最佳實踐的結(jié)論。架構(gòu)設(shè)計也是一樣,這里結(jié)合自己的理解分析一下微軟給出的云架構(gòu)的一些模式。話說微軟干這方面的事情真的很厲害,之前翻譯過的《微軟應(yīng)用架構(gòu)指南》寫的也很不錯。有了模式的好處是,技術(shù)人員和技術(shù)人員之間的對話可以毫不費力的通過幾個模式關(guān)鍵詞進(jìn)行交流,就像現(xiàn)在大家溝通提到職責(zé)鏈模式,如果雙方都理解這個模式的意義那么這五個字替代的可能就是半小時的解釋。廢話不多說,接下去來看一下這些其實已經(jīng)很熟悉親切的模式。
管理和監(jiān)控
1、大使模式:創(chuàng)建代表消費者服務(wù)或應(yīng)用程序發(fā)送網(wǎng)絡(luò)請求的幫助服務(wù)
進(jìn)程外的代理服務(wù)(之前介紹中間件的時候也提到了,很多框架層面的事情可以以軟件框架的形式寄宿在進(jìn)程內(nèi),也可以以獨立的代理形式做一個網(wǎng)絡(luò)中間件)。這里的大使模式意思就是這么一個網(wǎng)絡(luò)代理進(jìn)程,用于和遠(yuǎn)端的服務(wù)進(jìn)行通訊,完成下面的工作:
· 服務(wù)路由
· 服務(wù)熔斷
· 服務(wù)跟蹤
· 服務(wù)監(jiān)控
· 服務(wù)授權(quán)
· 數(shù)據(jù)加密
· 日志記錄
由于是獨立進(jìn)程的網(wǎng)絡(luò)服務(wù),所以這個模式適合于我們有多語言多框架都需要干同樣的事情,那么我們的框架中客戶端部分的很多工作可以移出來放到大使服務(wù)中去。當(dāng)然了,多一層網(wǎng)絡(luò)調(diào)用多一層開銷,大使服務(wù)的部署也要考慮到性能不一定可以集中部署,這些都是要考慮的問題。
2、反腐模式:在現(xiàn)代應(yīng)用程序和遺留系統(tǒng)之間實現(xiàn)裝飾或適配器層
使用一層防腐層來作為新老系統(tǒng)通訊的中間人。這樣新系統(tǒng)可以完全使用新的通訊方式和架構(gòu)方式,老的系統(tǒng)又不用進(jìn)行特別改造可以暫時保留,等老系統(tǒng)不用之后可以廢棄這個反腐層。這種模式適合新老系統(tǒng)遷移的過渡方案,不屬于永久使用的架構(gòu)設(shè)計模式。
3、外部配置存儲:將應(yīng)用程序部署包中的配置信息移動到中心化的位置
這個模式說的就是可以有一個外部的配置服務(wù)來保存配置信息,在之前第五篇文章介紹中間件的時候我詳細(xì)說明過配置服務(wù)的功能。不管是處于管理運維的角度還是方便安全的角度,具有配置共享配置外存特點的獨立配置服務(wù)對于大型的網(wǎng)站來說必不可少。實現(xiàn)的話有很多開源項目提供了配置服務(wù),見之前我的文章。
4、網(wǎng)關(guān)聚合模式:使用網(wǎng)關(guān)將多個單獨的請求聚合到一個請求中
應(yīng)用程序如果需要和多個服務(wù)交互的話,在中間構(gòu)建起一個聚合網(wǎng)關(guān)層,網(wǎng)關(guān)并發(fā)發(fā)出多個請求給后面的服務(wù),然后匯總數(shù)據(jù)給到應(yīng)用程序。這種模式有幾個好處:
· 允許并發(fā)調(diào)用多個服務(wù)提高性能,允許只返回部分?jǐn)?shù)據(jù)
· 網(wǎng)關(guān)里可以做一些彈性設(shè)計方案(熔斷、重試、限流)
· 網(wǎng)關(guān)里可以做一些緩存方案
· 對于外網(wǎng)通訊的時候,可以讓網(wǎng)關(guān)作為一個網(wǎng)絡(luò)中間層
當(dāng)然,使用這種模式需要考慮到網(wǎng)關(guān)的負(fù)載、高可用、高性能(異步IO)等等。
其實這種模式不僅僅用于純后端服務(wù)之間的通訊,很多面向前端的API請求都會做一個聚合層,這樣前端可以只發(fā)一個請求的情況下任意向后端一次性索取多個API的返回,減少網(wǎng)絡(luò)請求次數(shù)提高性能。
實現(xiàn)上最簡單的方式可以使用OpenResty或Nginx實現(xiàn)。
5、網(wǎng)關(guān)卸壓模式:把共享或特定的服務(wù)功能放到網(wǎng)關(guān)代理
名字有點難以理解,其實這種模式我們可能一直在用。就是用一個代理網(wǎng)關(guān)層做一些和業(yè)務(wù)無關(guān)的又麻煩的點,比如SSL,實現(xiàn)上用Nginx實現(xiàn)就很簡單。我們經(jīng)常會對外啟用HTTPS服務(wù),然后對內(nèi)服務(wù)實際提供的是HTTP接口,通過網(wǎng)關(guān)做一下協(xié)議轉(zhuǎn)換。
6、網(wǎng)關(guān)路由模式:使用單個端點將請求路由到多個服務(wù)
這也是很常見的作法,我們對外的接口可能是/cart、/order、/search這樣的API,在其背后其實是不同的服務(wù),通過網(wǎng)關(guān)層進(jìn)行轉(zhuǎn)發(fā),不僅僅可以做后端服務(wù)的負(fù)載均衡和故障轉(zhuǎn)移,在后端服務(wù)變更切換對外API路徑(比如版本升級)的時候我們也可以進(jìn)行靈活的路由,確保了對外接口的一致性。可以使用Nginx來實現(xiàn),相信大部分公司都是由Nginx這樣的網(wǎng)關(guān)來對外的,不會把域名直接解析到底層服務(wù)上對外。
7、健康端點監(jiān)控模式:在應(yīng)用程序中執(zhí)行功能檢查,外部工具可以定期通過暴露的端點訪問
這個模式其實是挺重要的一點,有幾個點需要注意:
· 需要暴露哪些信息?不僅僅是服務(wù)本身或框架本身是否啟動成功,盡可能暴露出服務(wù)依賴的外部存儲或系統(tǒng)是否可用,原因是網(wǎng)絡(luò)通訊是復(fù)雜的,從外部看到某個服務(wù)可用不代表我們的網(wǎng)站就可以成功連接,如果底層的數(shù)據(jù)庫都無法連接,即使這個網(wǎng)站本身啟動成功,那么我們應(yīng)該認(rèn)為這個服務(wù)是不健康的。外部存儲即使對于A節(jié)點是可以連通對于B節(jié)點不能連通也是有可能的,可能是因為網(wǎng)絡(luò)問題或權(quán)限問題,還可能因為負(fù)載問題,有的時候?qū)τ陂L連接的請求A節(jié)點因為始終連著存儲不會有問題,新的B節(jié)點要求連接的時候因為超出最大連接限制無法連接。如果有可能的話還暴露一些服務(wù)內(nèi)部各種線程池、連接池和隊列的信息吧(對象數(shù),隊列長度等),這些指標(biāo)很關(guān)鍵,但是因為在程序內(nèi)部所以外圍很難感知到,有了一些關(guān)鍵指標(biāo)的外露對于排查性能問題會方便很多。
· 不只是網(wǎng)站,服務(wù)也應(yīng)該暴露出健康信息,一來我們可以在外部收集這些信息進(jìn)行監(jiān)控匯總,二來我們的負(fù)載均衡器或發(fā)布系統(tǒng)需要有一個方式來判斷服務(wù)是否可用,不可用的時候進(jìn)行重啟或故障轉(zhuǎn)移。
· 對外的服務(wù)注意health端口的授權(quán),這里可能會有一些敏感信息,不宜讓匿名用戶看到。
實現(xiàn)上,我們應(yīng)當(dāng)把health端口作為插件形式集成到系統(tǒng),配置一下即可啟用,用不著每一個系統(tǒng)都自己開發(fā)一套。如果使用SpringBoot的話可以直接使用Actuator模塊實現(xiàn)。
8、絞殺者模式:通過使用新的應(yīng)用程序和服務(wù)逐漸替換特定功能部件來逐步遷移舊系統(tǒng)
名字挺嚇人,這個模式說的是如何做遷移。通過建立一個門面來作為后端新老服務(wù)的路由,慢慢把服務(wù)替換為新服務(wù),最后當(dāng)所有的服務(wù)都是新服務(wù)后刪除這個門面即可。這樣對于消費者感知不到這個遷移的過程。在上一篇文章中我們提到的換引擎的方式其實說的是保留原有的門面,也是通過這個門面做底層引擎的替換。其實我覺得對于減少外圍影響這種模式是完全可以理所當(dāng)然想到的,真正難的過程還是之前說的數(shù)據(jù)遷移和底層服務(wù)實現(xiàn)的過程。
性能和可擴(kuò)展性
9、緩存輔助模式:按需將數(shù)據(jù)從數(shù)據(jù)存儲加載到緩存中
這個模式說的不是廣義上的緩存使用,而是其中的一種使用方式。我們對于緩存的使用一般有這么幾種方式:
· 查緩存,不存在查庫,然后更新緩存
· 直接維護(hù)一大塊“全量”數(shù)據(jù),盡量和數(shù)據(jù)庫同步
這個模式說的是后一種方式,對于數(shù)據(jù)變動不大,這種模式是性能最好的,幾乎實現(xiàn)了100%的命中率,而且如果數(shù)據(jù)量不大可以容納進(jìn)進(jìn)程的話不需要跨進(jìn)程通訊。往細(xì)致一點去想,這里還有一層性能優(yōu)化的點,因為我們在內(nèi)存中維護(hù)了一套復(fù)雜的全量數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu),內(nèi)存中對象的引用只是指針引用,內(nèi)存中的數(shù)據(jù)搜索可以很快,對于數(shù)據(jù)量不大但是關(guān)系復(fù)雜的數(shù)據(jù),這個搜索效率可以是數(shù)據(jù)庫的幾百倍。實現(xiàn)上一般會在應(yīng)用程序啟動的時候把數(shù)據(jù)完全加入內(nèi)存,在后續(xù)通過一些策略進(jìn)行數(shù)據(jù)更新:
· 定時更新同步數(shù)據(jù),不同數(shù)據(jù)可以有不同的更新頻率由后臺線程來更新
· 數(shù)據(jù)具有不同的過期時間,過期后由請求觸發(fā)主動更新或回調(diào)方式被動更新
· 數(shù)據(jù)修改后同步修改緩存和數(shù)據(jù)庫中的數(shù)據(jù)
10、命令和查詢責(zé)任分離模式:通過使用單獨的接口來分離讀取數(shù)據(jù)和更新數(shù)據(jù)的操作
英文縮寫是CQRS,看到這個關(guān)鍵字你可能會覺得有點熟悉了。CQRS原來說的是我們可以有兩套數(shù)據(jù)模型分別用于讀和寫。好處是,我們可以讓讀和寫具有完全不同的數(shù)據(jù)結(jié)構(gòu),減少相互的干擾,減少權(quán)限控制的復(fù)雜度。這里說的不一定是指架構(gòu)層面我們可以這么做,也指在程序內(nèi)部,我們可以有兩套命令模型來處理讀寫這兩個事情,分別進(jìn)行優(yōu)化和定制。
現(xiàn)在一般的做法是類似于上圖的做法,為讀寫配置兩套獨立的數(shù)據(jù)源,并且和事件溯源的方式結(jié)合起來做(見后面一節(jié))。我們來說說讀寫兩套模型在存儲上分離這個事情,在《相輔相成的存儲五件套》一文中我們的架構(gòu)圖其實就有這方面的意思。對于讀寫這兩個事情,我們完全可以不用一套數(shù)據(jù)源,為讀建立專門的物化視圖,可以針對讀進(jìn)行優(yōu)化,避免在讀的時候做很多Join的工作,可以把性能做到極致(后面會有物化視圖模式的介紹)。事件溯源+CQRS+物化視圖三者一般會結(jié)合起來使用。
11、事件溯源模式:使用僅追加存儲去記錄描述對域中的數(shù)據(jù)采取的操作的完整系列事件
事件溯源(ES)是一種有趣的模式,說的是我們記錄的不是數(shù)據(jù)的當(dāng)前狀態(tài)而是疊加的數(shù)據(jù)變化序列(是不是想到了區(qū)塊鏈的數(shù)據(jù)記錄方式)。傳統(tǒng)的CRUD方式因為有更新這個操作,所以會產(chǎn)生性能并發(fā)方面的局限性,而且我們還需要配備額外的日志來做審計,否則就產(chǎn)生了信息丟失。而事件溯源模式記錄的是事件而不是當(dāng)前狀態(tài),所以有下面的特點:
· 事件不可變,只是追加新的事件,沒有沖突,性能高
· 以事件驅(qū)動做外部處理,耦合低
· 保留第一手原始信息,信息沒有損耗
其實有一些業(yè)務(wù)場景下這種模式會比CRUD存儲更適合:
· 業(yè)務(wù)更看重數(shù)據(jù)的意圖和目的而不是當(dāng)前的狀態(tài),注重審計、回滾、歷史方面的功能
· 希望避免數(shù)據(jù)更新的沖突,希望數(shù)據(jù)的產(chǎn)生能有較高性能,又能接受數(shù)據(jù)狀態(tài)的最終一致性
· 整個系統(tǒng)中本身就是以事件在驅(qū)動的(我們可以想一下在真實的世界中,物體和物體之間相互影響,通過事件來影響,每個物體觀察到其它物體發(fā)出的事件來做出自己的反映,這是最自然的,而不是觀察到別的物體屬性的變化來調(diào)整自己的屬性)
反過來說,業(yè)務(wù)邏輯很簡單的系統(tǒng),需要強一致性的系統(tǒng),數(shù)據(jù)很少更新的系統(tǒng)不適合這種模式。不知你所了解到的采用ES模式的業(yè)務(wù)場景有哪些?大家一起交流一下。
12、物化視圖模式:針對所需的查詢操作,當(dāng)數(shù)據(jù)沒有理想地格式化時,在一個或多個數(shù)據(jù)存儲中的數(shù)據(jù)上生成預(yù)填充視圖
我們在使用數(shù)據(jù)存儲的時候往往會更多考慮存儲而不是讀取。我們使用各種數(shù)據(jù)庫范式來設(shè)計數(shù)據(jù)庫,在讀取數(shù)據(jù)的時候我們需要做大量的關(guān)聯(lián)查詢以輸出符合需要的查詢結(jié)果。這個時候性能往往會成為瓶頸,物化視圖是一種空間換時間的做法。與其在查詢的時候做關(guān)聯(lián),倒不如提前保存一份面向于查詢和輸出的數(shù)據(jù)格式。因此,物化視圖適合下面的場景:
· 經(jīng)過復(fù)雜的計算才能查詢出數(shù)據(jù)
· 背后存儲可能會有不穩(wěn)定的情況
· 需要連接多個不同類型的存儲才能查詢到結(jié)果
但是因為需要考慮到物化視圖計算保存的開銷,所以也不太適合于數(shù)據(jù)變化太頻繁的情況,因為數(shù)據(jù)加工需要時間,所以不適合需要數(shù)據(jù)強一致性的場景。
實現(xiàn)上一般是基于消息監(jiān)聽做額外維護(hù)一套物化視圖的數(shù)據(jù)源和主流程解耦。惠普的Vertica是一款高性能的列式分析數(shù)據(jù)庫,它的一個特性就是物化視圖,通過事先提供SQL語句直接緩存面向于統(tǒng)計的查詢結(jié)果,極大程度提高了性能,也是空間換時間的思想。
13、基于隊列的負(fù)載均衡模式:使用一個隊列作為任務(wù)和服務(wù)之間的緩沖區(qū),平滑間歇性重負(fù)載
消息隊列我們太熟悉了,之前我們也反復(fù)提高過好多次,甚至我說這是架構(gòu)三馬車之一。這個模式在這里強調(diào)的是削峰的優(yōu)勢。這里我還想提幾點:
· 引入消息隊列不會提高處理能力,而是會降低性能,只是我們把耦合解開了允許每一個部件單獨有自己的彈性,對于不能負(fù)荷的部分在隊列中進(jìn)行緩沖,緩沖不是存儲不意味無限制
· 隊列看的是處理速度和入隊速度的比例,一般而言,我們需要預(yù)先做評估確保處理TPS超過2倍的最高峰的入隊TPS,確保留出一半的富裕,這樣在業(yè)務(wù)邏輯有修改的時候處理TPS哪怕下降了30%,還能抗住壓力
14、優(yōu)先級隊列模式:確定發(fā)送到服務(wù)的請求的優(yōu)先級,使得具有較高優(yōu)先級的請求更快地被接收和處理
區(qū)別于FIFO結(jié)構(gòu)的隊列,優(yōu)先級隊列允許消息標(biāo)識處理優(yōu)先級。這里實現(xiàn)上如上面兩個圖有兩種方式:
· 消息優(yōu)先級方式。在隊列中進(jìn)行實時位置重排,永遠(yuǎn)優(yōu)先處理級別較高的消息。
· 不同的處理池方式。我們可以針對不同的處理級別配備專門的處理池來處理這些消息,高級別的消息具有更多的處理資源,更好的硬件來處理,這樣勢必會有較高的處理能力。
在方案選擇和實現(xiàn)上要考慮消息優(yōu)先級是否需要絕對按照優(yōu)先級來處理,還是說相對優(yōu)先處理即可,如果需要絕對優(yōu)先那么除了消息位置重排還需要有搶占處理。還有,如果我們采用第二種多池的方式來處理的話可能會發(fā)生低級別的消息處理時間比高級別的消息更快的可能性(如果兩者處理業(yè)務(wù)邏輯是完全不同的話)。
實現(xiàn)上的話RabbitMQ 3.5以上版本支持了消息優(yōu)先級,實現(xiàn)的是第一種方式,在消息有緩沖的堆積的時候進(jìn)行消息重排,消費端可以先看到先處理優(yōu)先級高的消息,這種方式在消費速度大于產(chǎn)出速度的場景下是無法實現(xiàn)高級別消息優(yōu)先處理的。
補充一點,對于隊列中的消息,還有一種需要特別考慮的就是一直停留在隊列的消息應(yīng)當(dāng)視為低優(yōu)先級或是死信消息來處理,最好是有單獨的消費者來處理,避免此類消息影響了整個隊列的處理,見過很多個事故是由于隊列中被廢棄消息卡死導(dǎo)致徹底喪失處理能力的。
15、限流模式:控制應(yīng)用程序,個人租戶或整個服務(wù)的實例消耗的資源
在做壓力測試的時候我們會發(fā)現(xiàn),隨著壓力的上升系統(tǒng)的吞吐慢慢變大而且這個時候響應(yīng)時間可以基本保持可控(1秒內(nèi)),當(dāng)壓力突破一個邊界后,響應(yīng)時間一下子會不可控,隨之系統(tǒng)的吞吐就會下降,最后會徹底崩潰。任何系統(tǒng)對于壓力的負(fù)荷是有邊界的,超過這個邊界之后系統(tǒng)的SLA肯定無法滿足標(biāo)準(zhǔn),導(dǎo)致大家都無法好好用這個服務(wù)。因為系統(tǒng)的擴(kuò)展往往不是秒級可以做到的,所以這個時候最快的手段就是限流,只有限流了才能保護(hù)現(xiàn)在的系統(tǒng)不至于突破這個邊界徹底崩潰。對于業(yè)務(wù)量超大的系統(tǒng)搞活動,對關(guān)鍵服務(wù)甚至入口層面做限流是必然的,別無它法,淘寶雙11凌晨0點那一刻也能看到一定比例的下單被限流了。
常見的限流算法有這么幾種:
· 計數(shù)器算法。最簡單的算法,資源使用加一,釋放減一,達(dá)到一定的計數(shù)拒絕服務(wù)。
· 令牌桶算法。按照固定速率往桶里加令牌,桶里最多存放n個令牌,填滿丟棄。處理的時候需要獲取令牌,獲取不到則拒絕請求。
· 漏桶算法。一個固定容量的漏洞,按照一定的速度流出水滴(任務(wù))。可以以任意速度流入水滴(任務(wù)),滿了則溢出丟棄。
令牌桶算法限制的是平均流入速度,允許一定程度的突發(fā)請求,漏桶算法限制的是常量的流出速率用于平滑流入的速度。實現(xiàn)上,常用的一些開源類庫都會有相關(guān)的實現(xiàn),比如google的Guava提供的RateLimiter就是令牌桶算法。
限流模式有下面的一些注意事項:
· 限流需要快速執(zhí)行,任何一個超出流量控制的請求不允許放行,否則沒有意義。
· 限流需要提前執(zhí)行,最好在系統(tǒng)能力達(dá)到80%的時候進(jìn)行限流,越晚限流風(fēng)險越大。
· 可以返回特定的限流控制錯誤代碼給客戶端,讓用戶知道這不是錯誤是限流,可以稍后再試。
· 因為我們的系統(tǒng)很多地方都會做限流,在監(jiān)控圖上我們最好對這類限流的曲線有敏感,限流后的曲線是一下子失去了增長的梯度變?yōu)榱似椒€(wěn)的狀態(tài),如果監(jiān)控圖看的時間范圍過小的話會誤判這是一個正常的請求量。
· 限流可以在邊緣節(jié)點做。我們來考慮秒殺的場景,如果一秒有100萬個請求,這100萬個請求全部打到我們的應(yīng)用服務(wù)器沒有意義,我們可以在邊緣節(jié)點(CDN)甚至上做簡單的邊緣計算,讓這100萬個請求采用命中注定的方式直接隨機(jī)放棄其中的99.9%留下1000個請求,最終可以進(jìn)入我們的業(yè)務(wù)服務(wù),這樣TPS在1000一般是沒有問題的。所以很多時候我們參與秒殺系統(tǒng)會在極端的時間內(nèi)毫無思考告知你活動已結(jié)束,說明你已經(jīng)是被選中的命中注定的無法進(jìn)入后端系統(tǒng)來參與秒殺的那些人。
在下篇中我們將會繼續(xù)介紹數(shù)據(jù)、安全、消息、彈性方面的一些架構(gòu)模式。
總結(jié)
以上是生活随笔為你收集整理的朱晔的互联网架构实践心得S1E7:三十种架构设计模式(上)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AWD-LSTM为什么这么棒?
- 下一篇: NPM报错终极大法