16種設計思想 - Design for failure

一直在說互聯網系統應該是design for failure,今天看到的這篇文介紹的雖是簡單幾句話,但妥妥的設計思想,還是蠻契合SRE精髓。作爲一名designer或者developer,應該要對墨菲定律心存敬畏,以下講一下我對這16中設計思想的一個大致看法吧。
 

1、防禦性設計(Defensive Design)

所謂的防禦性設計實際上就是“防呆”,英文叫Idiot Proofing。說白了就是用戶有時候會不自覺的做一些蠢事,我們在設計的時候要儘量考慮到一些不規範的交互行爲,如果你的用戶是一隻猴子,你要寫包單保證系統不被玩壞。例如,在Android開發中使用到的Monkey Test就是用於這樣的目的。
 

2、邊界情況(Edge Case)

這個設計思想在測試領域比較常見,就是我們在設計我們的設計案例的時候有沒有充分考慮在邊界情況下的系統行爲。比較常見的例如,閏年情況、跨日情況等邊界。想起剛入行我leader跟我說你的程序在你腦袋有沒有跑過一遍所有能想到的情況,沒有的話重做。
 

3、防誤措施(Mistake Proofing)

怎麼保證不會發生錯誤。例如在人機交互環節,能不能進行輸入校驗?
 

4、解耦(Decoupling)

設計的時候,哪怕是最基礎的代碼也應該符合開閉原則。遠的不說,就單單Spring的IOC就是爲了把對象創建及維護從原來的由引用類負責這種強耦合模式轉成通過spring容器負責。且解耦一般的做法是通過把內部邏輯封裝起來,暴露對外統一API接口,調用方不需要了解被調用方的內部邏輯實現,只需要知道提供什麼功能即可。這樣再引申一下,解耦的作用就在於複用,把所有的高內聚功能獨立成一個個模塊,然後就可以像樂高積木一樣根據調用方的實際需求進行組裝。
 
宏觀的系統設計就更是如此,例如微服務中的Eureka。首先,Eureka客戶端通過把自己註冊到Eureka服務端(如IP、端口),然後其他服務在調用前通過Eureka獲取被調用方的信息,然後再去調用被調用方,然後他們的調用關係就是這樣解耦的。
 
 
熔斷本質上就是一種防禦性設計或者策略。假設一個微服務體系下的系統,其中A服務調用B服務。系統的QPS是千級別,當時如果B服務掛掉的話A的線程絕對在短時間內佔滿耗盡而導致假死,從而形成大量A請求積壓而導致情況惡化,最終形成雪崩。
 
在SpringCloud技術體系中,熔斷就是Hystrix所體現的另外一種思想。Hystrix可以通過監控一段事件內的異常次數和響應速度來判斷當前服務的健康狀況,若服務健康狀況不佳則進行熔斷,熔斷之後新的請求將不會調用實際的業務,而是通過快速失敗或優雅降級的方式來快速給用戶進行響應。
 
具體斷路器可以參考以下文章:

5、 艙壁模式(Bulkhead)

在分佈式系統的設計中有一種艙壁模式。目前比較火的微服務架構我理解實際上就是隔板模式的一個體現。這種模式把系統中的各個功能模塊實體進行進程、資源上的隔離,使得系統不會因爲某個功能模塊試題(即微服務)的局部失敗而導致全局失敗。
 
1)資源隔離:
微服務裏面的Hystrix則是遵守了該模式,通過爲每個單獨的服務提供獨立的線程池而進行資源隔離。在Hystrix中實際上通過兩種方式進行資源隔離:
 
信號量隔離策略( ExecutionIsolationStrategy.SEMAPHORE
Semaphores 隔離就是利用了java.util.concurrent.Semaphore 的功能,從信號量獲取到許可才允許執行,否則不允許執行,執行完成後要釋放之前獲取到的許可。同樣的每一個服務依賴都有一個自己的信號量,當該信號量的許可被獲取完之後,再有線程要進行依賴調用,發現已經沒有可用的信號量,這時候就會被拒絕調用。信號量隔離始終都是運行在 Container 線程內的。它的優勢就在於造成的開銷更低。
 
線程隔離策略( ExecutionIsolationStrategy.THREAD
所謂的線程隔離,實際上就是每一個依賴調用都有自己的線程池來負責處理,依賴調用都運行在自己線程池中的線程上,當同一個依賴調用使用的線程池中的 Queue  size 達到設置的闕值時就會拒絕進行依賴的調用。
 
 
具體用法可以通過繼承@HystrixCommand實現線程隔離或者交易隔離。
 
2)數據隔離
上面講的都是線程隔離,當然數據隔離這個一般的做法有庫隔離、表隔離、按字段區分這三種租戶隔離的方法。
 

6、冗餘(Redundancy)

所謂的冗餘指的通過重複配置關鍵組件或部件,保證在關鍵組件失效的情況下還有備份組件運作以便保證系統可以繼續提供服務。生活中的例子請參與飛機的雙引擎設計。
 
主從模式就是冗餘的體現。在正常情況下,主實例負責提供全部的服務,從實例在主實例整體或部分不可用的情況下,完全替代主實例整體或局部而對外提供服務。
 

7、重試(Retry)

重試是在分佈式系統下處理瞬態故障的一個基本手段,簡單有效(當然重試的前提是要求冪等)。但是重試也是可以很危險的,它能夠引起把一個局部小時間迅速升級爲一個系統重大故障,嚴重者導致系統假死。
 
 
舉個簡單例子:如果我們的鏈路類似上圖,這裏會發生什麼問題?在極端情況下,重試次數達到5*5*5*5=625次。當鏈路中的其中一個服務故障率異常的時候,那重試風暴便開啓了,因爲重試爲服務器帶來額外的開銷和線程的佔用,然後其他新來的請求又形成排隊,這樣的話就形成了類似的DDos惡性事件。根據我們平時的項目經驗,
- 相對較好的是選用3次;
- 且重試的時間一定要設定一定的時間間隔(因爲很多時候的瞬態故障更多是網絡抖動)
- 儘量只在應用層做重試。
 

8、撤銷(Undo)

這個沒什麼好說,撤銷這個功能應該是標配吧。
 

9、冷備(Cold Standby)

冷備實際上也是冗餘設計的其中一種體現,只是它會更側重於“冷”,意思是當系統發生宕機時,這個系統是需要手動啓動用於替換下線的主實例,它是跟熱備是不一樣,熱備更多體現在自動切換。
 

10、熔斷(Derating)

 
 
 
熔斷本質上就是一種防禦性設計或者策略。假設一個微服務體系下的系統,其中A服務調用B服務。系統的QPS是千級別,當時如果B服務掛掉的話A的線程絕對在短時間內佔滿耗盡而導致假死,從而形成大量A請求積壓而導致情況惡化,最終形成雪崩。
 
在SpringCloud技術體系中,熔斷就是Hystrix所體現的另外一種思想。Hystrix可以通過監控一段事件內的異常次數和響應速度來判斷當前服務的健康狀況,若服務健康狀況不佳則進行熔斷,熔斷之後新的請求將不會調用實際的業務,而是通過快速失敗或優雅降級的方式來快速給用戶進行響應。
 
具體斷路器可以參考以下文章:
Martin Fowler的Circuit Breaker
 

11、容錯(Error Tolerance)

狹義的容錯泛指人機交互界面的時候需要對用戶輸入進行輸入校驗,保證數據準確性。
 
廣義的容錯應該是兩個具有明確邊界的事物(如服務間,系統間)交互時候針對可能發生的一切主客觀異常情況的防禦性手段。常見的容錯機制有failsafe、failback、failover、failfast。
 
failfast更多指的是快速失敗。當系統遭遇一定概率的故障時,可預見這不是偶發性故障,然後就要開啓類似斷路器開關,讓後續打進來的流量直接失敗快速返回,避免線程積壓導致系統滾雪球式崩潰。
 
failover指的是失效轉移。請參考我的上一篇redis的文章《玩轉Redis高可用-哨兵模式》,裏面的主庫崩掉後通過選舉重新選定新主庫的情況就是失效轉移。
 
failsafe指的是失效安全,具體參考以下第12點。
 
failback指的是失效自動恢復,具體是指主實例發生故障而導致系統切換到備實例,在主實例恢復後自動轉移回主實例上。這種容錯在Hystrix的自恢復能力可以得到體現(詳看下圖)。
 
 
 
上圖斷路器的原理具體看以下三個關鍵參數,大體邏輯如下:
 
// 這個用於設定斷路器觸發的異常比例閾值,正常情況下斷路器處於關閉狀態。假設閾值爲50%,當在一定時間內(如1分鐘),異常調用次數/總調用次數>50%的話,斷路器打開,後續所有的請求全部調用getFallback()進行failfast.
HystrixCommandProperties.Setter().withCircuitBreakerErrorThresholdPercentage(50%)

// 上面說的一定的採樣週期內的流量至少要達到100,Hystrix纔會進行採樣統計並計算異常比例,再跟上面設定的異常比例閾值進行比較。
HystrixCommandProperties.Setter().withCircuitBreakerRequestVolumeThreshold(100)

// 當斷路器狀態爲打開後,在下面預設的6000毫秒時間內所有請求被快速failfast;當時間一過,Hystrix會試探性允許一個請求進來,這個時候斷路器處於半開狀態;如果調用成功,斷路器自動關閉,然後應用恢復正常狀態。
HystrixCommandProperties.Setter().withCircuitBreakerSleepWindowInMilliseconds(6000)

 

12、失效安全(Fail safe)

所謂的失效安全,就是指在特定失效的情況下,一個系統或者服務也不會對業務造成損害。實際上,我們使用token進行安全登錄也是一種失效安全的體現,如果token失效了(如時間過期),用戶是無法登錄的,因爲正常登錄需要token有一種約束因素,這種因素就是時間。如果時間過了,代表這種約束因素不存在或者不再有效了,登錄功能就不能正常工作了,這個是一個極好的設計理念。
 
有點抽象?跟你介紹一個生活的例子。電梯之所以可以正常升降,是因爲在通電的過程中,正常工作的約束因素(brake)是關閉的;如果某個特殊情況(如沒電),這個約束因素不存在或不再有效了,brake是打開的,因此電梯是不會因爲沒電而下墜的。這個可以理解了吧?
 

13、優雅降級(Graceful Degradation)

服務降級跟熔斷還是挺像的,只是降級來得更加溫和和優雅一點。熔斷是直接斷掉防止異常進一步擴大而導致雪崩,但是我們的終極目標是提供儘可能多的服務,這個就是優雅降級的理念。在一些異常情況或者秒殺場景下,爲了保證核心服務(如商品下單、支付)的正常可用,會放棄掉一些非核心服務(如歷史賬單查詢),這就是所謂的服務降級。
 
在微服務框架中,一般會使用Hystrix的@HystrixCommand或Feign的@FeignClient對服務進行聲明,然後爲每個服務配置相應的fallback類,最終結合起來進行服務降級。
 

14、監控(Monitoring)

我們的系統有哪幾個緯度的監控,估計最多就是常規的硬件狀態監控。當然這裏的監控我理解除了技術指標監控,還更應該有業務指標監控,否則我們都在裸泳,等海水退下去後就一覽無遺。
 
監控實際上是爲了更好的主動防禦,下圖展示一下本人前司的一個運維監控與開發協同的機制(從每個序號順序往下走)。大家可以看出,一套完善的告警監控系統,能夠快速通知開發與運維,開發側能夠完成緊急修復並能夠協同運維進行快速部署。在筆者前司經歷中,正是有着完善的監控告警系統,大部分故障基本可以在業務發現問題得到有效解決(麻蛋一般在晚上爆問題,那段時間太美好了)。
 
 

15、耐用性(Durability)

這裏我理解的是系統或數據的耐受性。例如數據,爲什麼我們一定要持久化到數據庫,因爲就是要藉助數據庫硬件各種維度的耐受性。
 

16、回彈性(Resilience)

這裏我看到網上有一個更恰當的翻譯,叫“韌性”,就是說我們的設計應該在一些特殊情況下還能通過一系列的手段繼續提供儘可能多的服務,你也可以理解爲“可靠性”。實際上,我的理解是上面說到的基本上都是回彈性的範疇之內。
 
  • 服務降級
  • 限流
  • 重試
  • 艙壁模式
  • 防禦性設計
 
當然,現在爲了提升系統服務的回彈性,部分頭部公司也會使用一些故障注入的辦法進行混沌工程式訓練,如Netflix的ChaosMonkey,阿里的ChaosBlade等。
 
參考:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章