1. 程式人生 > >彈力設計總結

彈力設計總結

容錯設計又叫彈力設計,其中著眼於分散式系統的各種“容忍”能力,包括容錯能力(服務 隔離、非同步呼叫、請求冪等性)、可伸縮性(有 / 無狀態的服務)、一致性(補償事務、重 試)、應對大流量的能力(熔斷、降級)。可以看到,在確保系統正確性的前提下,系統的可用性是彈力設計保障的重點。

隔離設計
what:
顧名思義,它是指將系統按照一定的原則劃分為若干個服務模組,各個模組之間相對獨立,無強依賴。當有故障發生時,能將問題和影響隔離在某個模組內部,而不擴散風險,不波及其它模組,不影響整體的系統服務。它是借鑑於造船行業。

why:
任何軟體系統,故障是不可避免的,並且大多數還是不可預測的,因此,我們只能在系統的設計之初就充分的考慮好應對措施,如何在故障發生時,去盡最大可能的止損和減少故障範圍。
沒有人敢說他的系統是百分百可用,我們能做的就是,使用一切方法去減少故障的影響面,儘可能的去提高系統的整體可用率。
而把系統分離成子服務,將子服務進行一定程度隔離的做法,能保證在有不可預測的故障發生時,縮小故障範圍的最佳手段。
我們需要定義好隔離業務的大小和粒度,過大和過小都不好。這需要認真地做業務上的需求 和系統分析。

how:
1.按服務/功能做隔離
如果我們要設計個電商平臺,可以將其中的使用者系統、訂單系統、支付系統、倉儲系統都分別進行獨立隔離,這樣做就是從服務層面實現了故障的隔離效果

2.按使用者分類隔離:
先部署多套一模一樣的業務服務,然後將使用者根據一定的特徵去做分類,讓不同分類的使用者去訪問不同的業務例項,達到分流和隔離的效果。

3.在程式碼層面也可以做隔離設計,比如不同業務使用不同的執行緒池,防止某個業務故障直接影響到其他的業務

非同步通訊設計:

what:
通訊一般來說分同步和非同步兩種。同步通訊就像打電話,需要實時響應,而非同步通訊就像發郵 件,不需要馬上回復。

why:
1.整個同步呼叫鏈的效能會由慢的那個服務所決定
2.同步呼叫會導致呼叫方一直在等待被呼叫方完成,如果一層接一層地同步呼叫下去,所有的 參與方會有相同的等待時間。這會非常消耗呼叫方的資源。

3.同步呼叫不好的是,如果被呼叫方有問題,那麼其呼叫方就會跟著出問題,會出現多 米諾骨牌效應。

所以,非同步通訊相對於同步通訊來說,除了可以增加系統的吞吐量之外,大的一個好處是其可 以讓服務間的解耦更為徹底,系統的呼叫方和被呼叫方可以按照自己的速率而不是步調一致,各個服務間的效能不受干擾相對獨立。從 而可以更好地保護系統,讓系統更有彈力。

how:
有幾種方式
1.請求響應式 在這種情況下,傳送方(sender)會直接請求接收方(receiver),被請求方接收到請求後,直 接返回——收到請求,正在處理。
對於返回結果,有兩種方法,一種是傳送方時不時地去輪詢一下,問一下乾沒幹完。另一種方式 是傳送方註冊一個回撥方法,也就是接收方處理完後回撥請求方。這種架構模型在以前的網上支 付中比較常見,頁面先從商家跳轉到支付寶或銀行,商家會把回撥的 URL 傳給支付頁面,支付 完後,再跳轉回商家的 URL。
很明顯,這種情況下還是有一定耦合的。是傳送方依賴於接收方,並且要把自己的回調發送給接 收方,處理完後回撥。

2.釋出訂閱的方式
通過訊息中介軟體(Broker),傳送方(sender)和接收方(receiver)都互相看不到對方, 它們看得到的是一個 Broker,傳送方向 Broker 傳送訊息,接收方向 Broker 訂閱訊息。

在 Broker 這種模式下,傳送方的服務和接收方的服務大程度地解耦。但是所有人都依賴於一 個匯流排,所以這個匯流排就需要有如下的特性:

必需是高可用的,因為它成了整個系統的關鍵;
必需是高效能而且是可以水平擴充套件的;
必需是可以持久化不丟資料的。

要做到這三條還是比較難的,所以一般下游系統會提供查詢介面給上游呼叫。

注意點:
訊息順序很難保證,業務最好設計成不依賴訊息順序的。

訊息傳遞中,可能有的業務邏輯會有像 TCP 協議那樣的 send 和 ACK 機制。比如:A 服務 發出一個訊息之後,開始等待處理方的 ACK,如果等不到的話,就需要做重傳。此時,需要 處理方有冪等的處理,即同一件訊息無論收到多少次都只處理一次。

關於冪等性設計,現在我們一般是採用先查詢的方式,但是絕大多數請求應該都不會是重新發過來的,所以讓 100% 的請求都到這個儲存裡去查一下,這會導致處理流程可能會很慢。
所以,好是當這個儲存出現衝突的時候會報錯。也就是說,我們收到交易請求後,直接去儲存 裡記錄這個 ID(相對於資料的 Insert 操作),如果出現 ID 衝突了的異常,那麼我們就知道這個之前已經有人發過來了,所以就不用再做了。

補償事務
what:
業務補償主要做兩件事。

  1. 努力地把一個業務流程執行完成。
  2. 如果執行不下去,需要啟動補償機制,回滾業務流程

why:
在很多情況下,我們是無法做到強一致的ACID 的。特別是我們需要跨多個系統的時候。BASE理論強調軟狀態與最終一致性,
如果一個事務失敗了或是超時了,我們需要 不斷地重試,努力地達到最終我們想要的狀態。然後,如果我們不能達到這個我們想要的狀態, 我們需要把整個狀態恢復到之前的狀態。另外,如果有變化的請求,我們需要啟動整個事務的業務更新機制。

how:
因為要把一個業務流程執行完成,需要這個流程中所涉及的服務方支援冪等性。並且在上游 有重試機制
需要小心維護和監控整個過程的狀態

補償的業務邏輯和流程不一定非得是嚴格反向操作。有時候可以並行,有時候,可能會更簡 單。總之,設計業務正向流程的時候,也需要設計業務的反向補償流程

業務補償的業務邏輯是強業務相關的,很難做成通用的

下層的業務方最好提供短期的資源預留機制。就像電商中的把貨品的庫存預先佔住等待使用者 在 15 分鐘內支付。如果沒有收到使用者的支付,則釋放庫存。然後回滾到之前的下單操作,等待使用者重新下單。

重試設計

重試的場景:
呼叫超時、被呼叫端返回了某種可以重試的錯誤(如繁忙中、流控中、維護中、資源不足等)。
而對於一些別的錯誤,則最好不要重試,比如:業務級的錯誤(如沒有許可權、或是非法資料等錯 誤),技術上的錯誤(如:HTTP 的 503 等,這種原因可能是觸發了程式碼的 bug,重試下去沒 有意義)。

重試的策略:
指數級退避策略。在這種情況下,每一次重試所需要的休息時間都會翻倍增加。這種機制主要是用來讓被呼叫方能夠有更多的時間來從容處理我們的請求。類似TCP 的擁塞控制

注意點:
要確定什麼樣的錯誤下需要重試;

重試的時間和重試的次數。這種在不同的情況下要有不同的考量。有時候,而對一些不是很 重要的問題時,我們應該更快失敗而不是重試一段時間若干次。比如一個前端的互動需要用 到後端的服務。這種情況下,在面對錯誤的時候,應該快速度失敗報錯(比如:網路錯誤請 重試)。而面對其它的一些錯誤,比如流控,那麼應該使用指數退避的方式,以避免造成更 多的流量。

如果超過重試次數,或是一段時間,那麼重試就沒有意義了

需要考慮被呼叫方是否有冪等的設計。如果沒有,那麼重試是不安全的,可能會導致 一個相同的操作被執行多次。

熔斷機制
借鑑於我們電閘上的 " 保險絲 ",當電壓有問題時(比如短路),自動跳閘,此時電路 就會斷開,我們的電器就會受到保護。
重試機制,如果錯誤太多, 或是在短時間內得不到修復,那麼我們重試也沒有意義了,此時應該開啟我們的熔斷操作,尤其 是後端太忙的時候,使用熔斷設計可以保護後端不會過載。

熔斷器可以使用狀態機來實現,內部模擬以下幾種狀態。
閉合(Closed)狀態:我們需要一個呼叫失敗的計數器,如果呼叫失敗,則使失敗次數加 1。如果最近失敗次數超過了在給定時間內允許失敗的閾值,則切換到斷開 (Open) 狀態。此 時開啟了一個超時時鐘,當該時鐘超過了該時間,則切換到半斷開(Half-Open)狀態。該 超時時間的設定是給了系統一次機會來修正導致呼叫失敗的錯誤,以回到正常工作的狀態。

在 Closed 狀態下,錯誤計數器是基於時間的。在特定的時間間隔內會自動重置。這能夠防 止由於某次的偶然錯誤導致熔斷器進入斷開狀態。也可以基於連續失敗的次數。

斷開 (Open) 狀態:在該狀態下,對應用程式的請求會立即返回錯誤響應,而不呼叫後端的 服務。這樣也許比較粗暴,有些時候,我們可以 cache 住上次成功請求,直接返回快取(當 然,這個快取放在本地記憶體就好了),如果沒有快取再返回錯誤(快取的機制最好用在全站 一樣的資料,而不是用在不同的使用者間不同的資料,因為後者需要快取的資料有可能會很 多)。

半開(Half-Open)狀態:允許應用程式一定數量的請求去呼叫服務。如果這些請求對服務 的呼叫成功,那麼可以認為之前導致呼叫失敗的錯誤已經修正,此時熔斷器切換到閉合狀態 (並且將錯誤計數器重置)。
如果這一定數量的請求有呼叫失敗的情況,則認為導致之前呼叫失敗的問題仍然存在,熔斷器切 回到斷開狀態,然後重置計時器來給系統一定的時間來修正錯誤。半斷開狀態能夠有效防止正在 恢復中的服務被突然而來的大量請求再次拖垮。

熔斷設計的重點:
錯誤的型別。需要注意的是請求失敗的原因會有很多種。需要根據不同的錯誤情況來調整相 應的策略。所以,熔斷和重試一樣,需要對返回的錯誤進行識別。一些錯誤先走重試的策略 (比如限流,或是超時),重試幾次後再開啟熔斷。一些錯誤是遠端服務掛掉,恢復時間比 較長;這種錯誤不必走重試,可以直接開啟熔斷策略。

日誌監控。熔斷器應該能夠記錄所有失敗的請求,以及一些可能會嘗試成功的請求,使得管 理員能夠監控使用熔斷器保護的服務的執行情況。

測試服務是否可用。在斷開狀態下,熔斷器可以採用定期地 ping 一下遠端的服務的健康檢查 介面,來判斷服務是否恢復,而不是使用計時器來自動切換到半開狀態。這樣做的一個好處 是,在服務恢復的情況下,不需要真實的使用者流量就可以把狀態從半開狀態切回關閉狀態。 否則在半開狀態下,即便服務已恢復了,也需要使用者真實的請求來恢復,這會影響使用者的真 實請求。

手動重置。在系統中對於失敗操作的恢復時間是很難確定的,提供一個手動重置功能能夠使 得管理員可以手動地強制將熔斷器切換到閉合狀態。同樣的,如果受熔斷器保護的服務暫時 不可用的話,管理員能夠強制將熔斷器設定為斷開狀態。

併發問題。相同的熔斷器有可能被大量併發請求同時訪問。熔斷器的實現不應該阻塞併發的 請求或者增加每次請求呼叫的負擔。尤其是其中的對呼叫結果的統計,一般來說會成為一個 共享的資料結構,這個會導致有鎖的情況。在這種情況下,最好使用一些無鎖的資料結構, 或是 atomic 的原子操作。這樣會帶來更好的效能。

資源分割槽。有時候,我們會把資源分佈在不同的分割槽上。比如,資料庫的分庫分表,某個分 區可能出現問題,而其它分割槽還可用。在這種情況下,單一的熔斷器會把所有的分割槽訪問給 混為一談,從而,一旦開始熔斷,那麼所有的分割槽都會受到熔斷影響。或是出現一會兒熔斷 一會兒又好,來來回回的情況。所以,熔斷器需要考慮這樣的問題,只對有問題的分割槽進行 熔斷,而不是整體。

限流設計
保護系統不會在過載的情況下導致問題,那麼,我們就需要限流,熔斷機制也是限流的一種。

限流的策略:

  1. 拒絕服務

  2. 服務降級

限流的實現方式:
計數器方式

佇列演算法

漏斗演算法

令牌桶演算法

降級設計

本質是為了解決資源不足和訪問量過大的問題

降級需要犧牲掉的東西有:
降低一致性。從強一致性變成最終一致性

停止次要功能。停止訪問不重要的功能,從而釋放出更多的資源。

簡化功能。把一些功能簡化掉,比如,簡化業務流程,或是不再返回全量資料,只返回部分 資料

要點:
對於降級,一般來說是要犧牲業務功能或是流程,以及一致性的。所以,我們需要對業務做非常 仔細的梳理和分析。我們很難通過不侵入業務的方式來做到功能降級。
在設計降級的時候,需要清楚地定義好降級的關鍵條件,比如,吞吐量過大、響應時間過慢、失 敗次數過多,有網路或是服務故障,等等,然後做好相應的應急預案。這些預案最好是寫成程式碼 可以快速地自動化或半自動化執行的。
功能降級需要梳理業務的功能,哪些是 must-have 的功能,哪些是 nice-to-have 的功能;哪 些是必需要死保的功能,哪些是可以犧牲的功能。而且需要在事前設計好可以簡化的或是用來應 急的業務流程。當系統出問題的時候,就需要走簡化應急流程。
降級的時候,需要犧牲掉一致性,或是一些業務流程:對於讀操作來說,使用快取來解決,對於 寫操作來說,需要非同步呼叫來解決。並且,我們需要以流水賬的方式記錄下來,這樣方便對賬, 以免漏掉或是和正常的流程混淆。
降級的功能的開關可以是一個系統的配置開關。做成配置時,你需要在要降級的時候推送相應的 配置。另一種方式是,在對外服務的 API 上有所區分(方法簽名或是開關引數),這樣可以由 上游呼叫者來驅動。
比如:一個閘道器在限流時,在協議頭中加入了一個限流程度的引數,讓後端服務能知道限流在發 生中。當限流程度達到某個值時,或是限流時間超過某個值時,就自動開始降級,直到限流好
轉。
對於資料方面的降級,需要前端程式的配合。一般來說,前端的程式可以根據後端傳來的資料來 決定展示哪些介面模組。比如,當前端收不到商品評論時,就不展示。為了區分本來就沒有數 據,還是因為降級了沒有資料的兩種情況,在協議頭中也應該加上降級的標籤。
因為降級的功能平時不會總是會發生,屬於應急的情況,所以,降級的這些業務流程和功能有可 能長期不用而出現 bug 或問題,對此,需要在平時做一些演練。