朱曄的網際網路架構實踐心得S1E7:三十種架構設計模式(上)

設計模式是前人通過大量的實踐總結出來的一些經驗總結和最佳實踐。在經過多年的軟件開發實踐之後,回過頭來去看23種設計模式你會發現很多平時寫代碼的套路和OO的套路和設計模式裡總結的類似,這也說明了你悟到的東西和別人悟到的一樣,經過大量實踐總能趨向性得出一些最佳實踐的結論。架構設計也是一樣,這裡結合自己的理解分析一下微軟給出的。話說微軟幹這方面的事情真的很厲害,之前翻譯過的《》寫的也很不錯。有了模式的好處是,技術人員和技術人員之間的對話可以毫不費力的通過幾個模式關鍵詞進行交流,就像現在大家溝通提到職責鏈模式,如果雙方都理解這個模式的意義那麼這五個字替代的可能就是半小時的解釋。廢話不多說,接下去來看一下這些其實已經很熟悉親切的模式。

管理和監控

1、大使模式:創建代表消費者服務或應用程序發送網絡請求的幫助服務

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

進程外的代理服務(之前介紹中間件的時候也提到了,很多框架層面的事情可以以軟件框架的形式寄宿在進程內,也可以以獨立的代理形式做一個網絡中間件)。這裡的大使模式意思就是這麼一個網絡代理進程,用於和遠端的服務進行通訊,完成下面的工作:

· 服務路由

· 服務熔斷

· 服務跟蹤

· 服務監控

· 數據加密

· 日誌記錄

由於是獨立進程的網絡服務,所以這個模式適合於我們有多語言多框架都需要幹同樣的事情,那麼我們的框架中客戶端部分的很多工作可以移出來放到大使服務中去。當然了,多一層網絡調用多一層開銷,大使服務的部署也要考慮到性能不一定可以集中部署,這些都是要考慮的問題。

2、反腐模式:在現代應用程序和遺留系統之間實現裝飾或適配器層

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

使用一層防腐層來作為新老系統通訊的中間人。這樣新系統可以完全使用新的通訊方式和架構方式,老的系統又不用進行特別改造可以暫時保留,等老系統不用之後可以廢棄這個反腐層。這種模式適合新老系統遷移的過渡方案,不屬於永久使用的架構設計模式。

3、外部配置存儲:將應用程序部署包中的配置信息移動到中心化的位置

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

這個模式說的就是可以有一個外部的配置服務來保存配置信息,在之前第五篇文章介紹中間件的時候我詳細說明過配置服務的功能。不管是處於管理運維的角度還是方便安全的角度,具有配置共享配置外存特點的獨立配置服務對於大型的網站來說必不可少。實現的話有很多開源項目提供了配置服務,見之前我的文章。

4、網關聚合模式:使用網關將多個單獨的請求聚合到一個請求中

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

應用程序如果需要和多個服務交互的話,在中間構建起一個聚合網關層,網關併發發出多個請求給後面的服務,然後彙總數據給到應用程序。這種模式有幾個好處:

· 允許併發調用多個服務提高性能,允許只返回部分數據

· 網關裡可以做一些彈性設計方案(熔斷、重試、限流)

· 網關裡可以做一些緩存方案

· 對於外網通訊的時候,可以讓網關作為一個網絡中間層

當然,使用這種模式需要考慮到網關的負載、高可用、高性能(異步IO)等等。

其實這種模式不僅僅用於純後端服務之間的通訊,很多面向前端的API請求都會做一個聚合層,這樣前端可以只發一個請求的情況下任意向後端一次性索取多個API的返回,減少網絡請求次數提高性能。

實現上最簡單的方式可以使用OpenResty或Nginx實現。

5、網關卸壓模式:把共享或特定的服務功能放到網關代理

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

名字有點難以理解,其實這種模式我們可能一直在用。就是用一個代理網關層做一些和業務無關的又麻煩的點,比如SSL,實現上用Nginx實現就很簡單。我們經常會對外啟用HTTPS服務,然後對內服務實際提供的是HTTP接口,通過網關做一下協議轉換。

6、網關路由模式:使用單個端點將請求路由到多個服務

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

這也是很常見的作法,我們對外的接口可能是/cart、/order、/search這樣的API,在其背後其實是不同的服務,通過網關層進行轉發,不僅僅可以做後端服務的負載均衡和故障轉移,在後端服務變更切換對外API路徑(比如版本升級)的時候我們也可以進行靈活的路由,確保了對外接口的一致性。可以使用Nginx來實現,相信大部分公司都是由Nginx這樣的網關來對外的,不會把域名直接解析到底層服務上對外。

7、健康端點監控模式:在應用程序中執行功能檢查,外部工具可以定期通過暴露的端點訪問

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

這個模式其實是挺重要的一點,有幾個點需要注意:

· 需要暴露哪些信息?不僅僅是服務本身或框架本身是否啟動成功,儘可能暴露出服務依賴的外部存儲或系統是否可用,原因是網絡通訊是複雜的,從外部看到某個服務可用不代表我們的網站就可以成功連接,如果底層的數據庫都無法連接,即使這個網站本身啟動成功,那麼我們應該認為這個服務是不健康的。外部存儲即使對於A節點是可以連通對於B節點不能連通也是有可能的,可能是因為網絡問題或權限問題,還可能因為負載問題,有的時候對於長連接的請求A節點因為始終連著存儲不會有問題,新的B節點要求連接的時候因為超出最大連接限制無法連接。如果有可能的話還暴露一些服務內部各種線程池、連接池和隊列的信息吧(對象數,隊列長度等),這些指標很關鍵,但是因為在程序內部所以外圍很難感知到,有了一些關鍵指標的外露對於排查性能問題會方便很多。

· 不只是網站,服務也應該暴露出健康信息,一來我們可以在外部收集這些信息進行監控彙總,二來我們的負載均衡器或發佈系統需要有一個方式來判斷服務是否可用,不可用的時候進行重啟或故障轉移。

· 對外的服務注意health端口的授權,這裡可能會有一些敏感信息,不宜讓匿名用戶看到。

實現上,我們應當把health端口作為插件形式集成到系統,配置一下即可啟用,用不著每一個系統都自己開發一套。如果使用SpringBoot的話可以直接使用Actuator模塊實現。

8、絞殺者模式:通過使用新的應用程序和服務逐漸替換特定功能部件來逐步遷移舊系統

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

名字挺嚇人,這個模式說的是如何做遷移。通過建立一個門面來作為後端新老服務的路由,慢慢把服務替換為新服務,最後當所有的服務都是新服務後刪除這個門面即可。這樣對於消費者感知不到這個遷移的過程。在上一篇文章中我們提到的換引擎的方式其實說的是保留原有的門面,也是通過這個門面做底層引擎的替換。其實我覺得對於減少外圍影響這種模式是完全可以理所當然想到的,真正難的過程還是之前說的數據遷移和底層服務實現的過程。

性能和可擴展性

9、緩存輔助模式:按需將數據從數據存儲加載到緩存中

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

這個模式說的不是廣義上的緩存使用,而是其中的一種使用方式。我們對於緩存的使用一般有這麼幾種方式:

· 查緩存,不存在查庫,然後更新緩存

· 直接維護一大塊"全量"數據,儘量和數據庫同步

這個模式說的是後一種方式,對於數據變動不大,這種模式是性能最好的,幾乎實現了100%的命中率,而且如果數據量不大可以容納進進程的話不需要跨進程通訊。往細緻一點去想,這裡還有一層性能優化的點,因為我們在內存中維護了一套複雜的全量數據的數據結構,內存中對象的引用只是指針引用,內存中的數據搜索可以很快,對於數據量不大但是關係複雜的數據,這個搜索效率可以是數據庫的幾百倍。實現上一般會在應用程序啟動的時候把數據完全加入內存,在後續通過一些策略進行數據更新:

· 定時更新同步數據,不同數據可以有不同的更新頻率由後臺線程來更新

· 數據具有不同的過期時間,過期後由請求觸發主動更新或回調方式被動更新

· 數據修改後同步修改緩存和數據庫中的數據

10、命令和查詢責任分離模式:通過使用單獨的接口來分離讀取數據和更新數據的操作

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

英文縮寫是CQRS,看到這個關鍵字你可能會覺得有點熟悉了。CQRS原來說的是我們可以有兩套數據模型分別用於讀和寫。好處是,我們可以讓讀和寫具有完全不同的數據結構,減少相互的干擾,減少權限控制的複雜度。這裡說的不一定是指架構層面我們可以這麼做,也指在程序內部,我們可以有兩套命令模型來處理讀寫這兩個事情,分別進行優化和定製。

現在一般的做法是類似於上圖的做法,為讀寫配置兩套獨立的數據源,並且和事件溯源的方式結合起來做(見後面一節)。我們來說說讀寫兩套模型在存儲上分離這個事情,在《相輔相成的存儲五件套》一文中我們的架構圖其實就有這方面的意思。對於讀寫這兩個事情,我們完全可以不用一套數據源,為讀建立專門的物化視圖,可以針對讀進行優化,避免在讀的時候做很多Join的工作,可以把性能做到極致(後面會有物化視圖模式的介紹)。事件溯源+CQRS+物化視圖三者一般會結合起來使用。

11、事件溯源模式:使用僅追加存儲去記錄描述對域中的數據採取的操作的完整系列事件

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

事件溯源(ES)是一種有趣的模式,說的是我們記錄的不是數據的當前狀態而是疊加的數據變化序列(是不是想到了區塊鏈的數據記錄方式)。傳統的CRUD方式因為有更新這個操作,所以會產生性能併發方面的侷限性,而且我們還需要配備額外的日誌來做審計,否則就產生了信息丟失。而事件溯源模式記錄的是事件而不是當前狀態,所以有下面的特點:

· 事件不可變,只是追加新的事件,沒有衝突,性能高

· 以事件驅動做外部處理,耦合低

· 保留第一手原始信息,信息沒有損耗

其實有一些業務場景下這種模式會比CRUD存儲更適合:

· 業務更看重數據的意圖和目的而不是當前的狀態,注重審計、回滾、歷史方面的功能

· 希望避免數據更新的衝突,希望數據的產生能有較高性能,又能接受數據狀態的最終一致性

· 整個系統中本身就是以事件在驅動的(我們可以想一下在真實的世界中,物體和物體之間相互影響,通過事件來影響,每個物體觀察到其它物體發出的事件來做出自己的反映,這是最自然的,而不是觀察到別的物體屬性的變化來調整自己的屬性)

反過來說,業務邏輯很簡單的系統,需要強一致性的系統,數據很少更新的系統不適合這種模式。不知你所瞭解到的採用ES模式的業務場景有哪些?大家一起交流一下。

12、物化視圖模式:針對所需的查詢操作,當數據沒有理想地格式化時,在一個或多個數據存儲中的數據上生成預填充視圖

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

我們在使用數據存儲的時候往往會更多考慮存儲而不是讀取。我們使用各種數據庫範式來設計數據庫,在讀取數據的時候我們需要做大量的關聯查詢以輸出符合需要的查詢結果。這個時候性能往往會成為瓶頸,物化視圖是一種空間換時間的做法。與其在查詢的時候做關聯,倒不如提前保存一份面向於查詢和輸出的數據格式。因此,物化視圖適合下面的場景:

· 經過複雜的計算才能查詢出數據

· 背後存儲可能會有不穩定的情況

· 需要連接多個不同類型的存儲才能查詢到結果

但是因為需要考慮到物化視圖計算保存的開銷,所以也不太適合於數據變化太頻繁的情況,因為數據加工需要時間,所以不適合需要數據強一致性的場景。

實現上一般是基於消息監聽做額外維護一套物化視圖的數據源和主流程解耦。惠普的Vertica是一款高性能的列式分析數據庫,它的一個特性就是物化視圖,通過事先提供SQL語句直接緩存面向於統計的查詢結果,極大程度提高了性能,也是空間換時間的思想。

13、基於隊列的負載均衡模式:使用一個隊列作為任務和服務之間的緩衝區,平滑間歇性重負載

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

消息隊列我們太熟悉了,之前我們也反覆提高過好多次,甚至我說這是架構三馬車之一。這個模式在這裡強調的是削峰的優勢。這裡我還想提幾點:

· 引入消息隊列不會提高處理能力,而是會降低性能,只是我們把耦合解開了允許每一個部件單獨有自己的彈性,對於不能負荷的部分在隊列中進行緩衝,緩衝不是存儲不意味無限制

· 隊列看的是處理速度和入隊速度的比例,一般而言,我們需要預先做評估確保處理TPS超過2倍的最高峰的入隊TPS,確保留出一半的富裕,這樣在業務邏輯有修改的時候處理TPS哪怕下降了30%,還能抗住壓力

14、優先級隊列模式:確定發送到服務的請求的優先級,使得具有較高優先級的請求更快地被接收和處理

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

朱曄的互聯網架構實踐心得S1E7:三十種架構設計模式(上)

區別於FIFO結構的隊列,優先級隊列允許消息標識處理優先級。這裡實現上如上面兩個圖有兩種方式:

· 消息優先級方式。在隊列中進行實時位置重排,永遠優先處理級別較高的消息。

· 不同的處理池方式。我們可以針對不同的處理級別配備專門的處理池來處理這些消息,高級別的消息具有更多的處理資源,更好的硬件來處理,這樣勢必會有較高的處理能力。

在方案選擇和實現上要考慮消息優先級是否需要絕對按照優先級來處理,還是說相對優先處理即可,如果需要絕對優先那麼除了消息位置重排還需要有搶佔處理。還有,如果我們採用第二種多池的方式來處理的話可能會發生低級別的消息處理時間比高級別的消息更快的可能性(如果兩者處理業務邏輯是完全不同的話)。

實現上的話RabbitMQ 3.5以上版本支持了消息優先級,實現的是第一種方式,在消息有緩衝的堆積的時候進行消息重排,消費端可以先看到先處理優先級高的消息,這種方式在消費速度大於產出速度的場景下是無法實現高級別消息優先處理的。

補充一點,對於隊列中的消息,還有一種需要特別考慮的就是一直停留在隊列的消息應當視為低優先級或是死信消息來處理,最好是有單獨的消費者來處理,避免此類消息影響了整個隊列的處理,見過很多個事故是由於隊列中被廢棄消息卡死導致徹底喪失處理能力的。

15、限流模式:控制應用程序,個人租戶或整個服務的實例消耗的資源

在做壓力測試的時候我們會發現,隨著壓力的上升系統的吞吐慢慢變大而且這個時候響應時間可以基本保持可控(1秒內),當壓力突破一個邊界後,響應時間一下子會不可控,隨之系統的吞吐就會下降,最後會徹底崩潰。任何系統對於壓力的負荷是有邊界的,超過這個邊界之後系統的SLA肯定無法滿足標準,導致大家都無法好好用這個服務。因為系統的擴展往往不是秒級可以做到的,所以這個時候最快的手段就是限流,只有限流了才能保護現在的系統不至於突破這個邊界徹底崩潰。對於業務量超大的系統搞活動,對關鍵服務甚至入口層面做限流是必然的,別無它法,淘寶雙11凌晨0點那一刻也能看到一定比例的下單被限流了。

常見的限流算法有這麼幾種:

· 計數器算法。最簡單的算法,資源使用加一,釋放減一,達到一定的計數拒絕服務。

· 令牌桶算法。按照固定速率往桶裡加令牌,桶裡最多存放n個令牌,填滿丟棄。處理的時候需要獲取令牌,獲取不到則拒絕請求。

· 漏桶算法。一個固定容量的漏洞,按照一定的速度流出水滴(任務)。可以以任意速度流入水滴(任務),滿了則溢出丟棄。

令牌桶算法限制的是平均流入速度,允許一定程度的突發請求,漏桶算法限制的是常量的流出速率用於平滑流入的速度。實現上,常用的一些開源類庫都會有相關的實現,比如google的Guava提供的RateLimiter就是令牌桶算法。

限流模式有下面的一些注意事項:

· 限流需要快速執行,任何一個超出流量控制的請求不允許放行,否則沒有意義。

· 限流需要提前執行,最好在系統能力達到80%的時候進行限流,越晚限流風險越大。

· 可以返回特定的限流控制錯誤代碼給客戶端,讓用戶知道這不是錯誤是限流,可以稍後再試。

· 因為我們的系統很多地方都會做限流,在監控圖上我們最好對這類限流的曲線有敏感,限流後的曲線是一下子失去了增長的梯度變為了平穩的狀態,如果監控圖看的時間範圍過小的話會誤判這是一個正常的請求量。

· 限流可以在邊緣節點做。我們來考慮秒殺的場景,如果一秒有100萬個請求,這100萬個請求全部打到我們的應用服務器沒有意義,我們可以在邊緣節點(CDN)甚至上做簡單的邊緣計算,讓這100萬個請求採用命中註定的方式直接隨機放棄其中的99.9%留下1000個請求,最終可以進入我們的業務服務,這樣TPS在1000一般是沒有問題的。所以很多時候我們參與秒殺系統會在極端的時間內毫無思考告知你活動已結束,說明你已經是被選中的命中註定的無法進入後端系統來參與秒殺的那些人。

在下篇中我們將會繼續介紹數據、安全、消息、彈性方面的一些架構模式。


分享到:


相關文章: