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