哦哇資訊網

分散式架構之彈力設計

由 網際網路技術集中營 發表于 美食2023-01-03

前言

在微服務體系架構中,由於拆解的服務數變多了,服務發生故障的地方也會相應的增加,因此如何保證服務架構健壯是一個值得深思的問題。微服務容錯機制正是這樣一種穩定性解決方案,可以理解微微服務架構的保險絲,透過它可以對業務平臺形成一種有效的保護機制。在發生平臺異常時候,容錯機制是平臺穩定執行的最後一道屏障。

隨著微服務的不斷升級進化,服務和服務之間的穩定性變得越來越重要,分散式系統之所以複雜,主要原因是分散式系統需要考慮到網路的延時和不可靠,微服務很重要的一個特質就是需要保證微服務架構的彈力設計。

什麼是彈力設計

我們在進行架構設計時,不僅需要滿足業務要求,同時也需要面向失敗進行設計,意思就是說當外部條件發生變化或者內部出現異常時,平臺的架構能夠將這種異常的影響降到最低,強大的容錯能力是優秀架構的關鍵指標。

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

彈力設計

隔離

概念

那什麼是「服務隔離」呢?顧名思義,它是指將系統按照一定的原則劃分為若干個服務模組,各個模組之間相對獨立,無強依賴。當有故障發生時,能將問題和影響隔離在某個模組內部,而不擴散風險,不波及其它模組,不影響整體的系統服務。

其實隔離設計並非軟體行業獨創,它是借鑑於造船行業。

如上圖,造船行業有一個專業術語叫做「艙壁隔離」。利用艙壁將不同的船艙隔離起來,如果某一個船艙進了水,那麼就可以立即封閉艙門,形成艙壁隔離,只損失那一個船艙,其他船艙不受影響,整個船隻還是可以正常航行。

設計

任何軟體系統,故障是不可避免的,並且大多數還是不可預測的,因此,我們只能在系統的設計之初就充分的考慮好應對措施,如何在故障發生時,去盡最大可能的止損和減少故障範圍。

沒有人敢說他的系統是百分百可用,我們能做的就是,使用一切方法去減少故障的影響面,儘可能的去提高系統的整體可用率,而隔離是一種非常不錯的設計方案,能保證在有不可預測的故障發生時,縮小故障範圍的最佳手段。

常見的隔離設計方案有如下幾種:

按服務/功能做隔離:微服務的服務隔離

按使用者分類隔離:SaaS多租戶隔離

服務叢集隔離:配置不同的請求訪問不同的服務叢集,我們透過服務別名來區分,比如秒殺叢集隔離和正常應用叢集隔離

資料儲存隔離:包括資料庫隔離、快取叢集隔離。資料庫隔離一般透過分庫分表,讀寫分離。快取隔離如熱點資料隔離、冷熱快取資料隔離。

執行緒池/訊號量隔離:通常基於hystrix、sentinel實現此種隔離。

這裡我們只講服務/功能做隔離、使用者分類隔離、執行緒池/訊號量隔離三種。

服務/功能做隔離

系統分成了使用者、商品、社群三個版塊。使用不同的域名、伺服器和資料庫,從接入層到應用層再到資料層三層完全隔離。每個服務都有自己的一個數據庫,儲存相關業務的資料和相應的處理狀態。每個服務對外暴露。微服務所推薦的架構方式。

服務隔離存在的問題

1。

呼叫多個服務

(同時獲得多個板塊資料),會降低效能。效能指的是響應時間,而不是吞吐量(這種架構下,吞吐量可以得到提高)。所以不要在一個頁面上獲得所有的資料,好在手機頁面小。

2。

增加了資料合併的複雜度

。需要一個框架、間件來對資料進行相應的抽取。

3。

導致整體業務故障

,如果業務流程跨版塊的話,所以業務流程Step-by-Step 的方式,互動的每一步都可以儲存,以便故障恢復後繼續執行,而不是從頭執行。

4。

跨版塊複雜

。高可用並持久化的訊息中介軟體(類似Pub/Sub),打通各個版塊的資料和資訊交換。

5。

多版塊分散式事務問題

。採用“二階段提交”方案。亞馬遜使用的是 Plan – Reserve – Commit/Cancel 模式。也就是說,先做一個 plan 的 API 呼叫,各個子系統 reserve 住相應的資源,成功Commit;一個失敗體 Cancel。很像阿里的 TCC – try confirm/cancel。引入大量的非同步處理模型。

使用者請求隔離

這樣系統掛掉時,隻影響一部分。

“多租戶”模式:大客戶,設定專門獨立服務例項,或是服務叢集與其他客戶隔離開來,小使用者,共享一個服務例項。

“多租戶”架構來引入複雜度。完全隔離,資源浪費,共享,程式設計複雜。

多租戶的做法有三種:

(1)完全獨立的設計。每個租戶有自己完全獨立的服務和資料。

(2)獨立的資料分割槽,共享的服務。多租戶的服務是共享的,但資料是分開隔離的。

(3)共享的服務,共享的資料分割槽。每個租戶的資料和服務都是共享的,本質上屬於邏輯隔離。

這三種方案各有優缺點,如圖所示。

在虛擬化技術非常成熟的今天,我們完全可以使用“

完全獨立

”(完全隔離)的方案,透過底層的虛擬化技術(Hypervisor 的技術,如 KVM,或是 Linux Container 的技術,如

Docker

)來實現物理資源的共享和成本的節約。

執行緒/訊號量隔離

我們可以透過執行緒池隔離的方式來實現資源的隔離,不同的請求使用相應的執行緒池來處理,即便出現請求資源time out的情況,最多影響當前執行緒池的資源,而不會影響整個服務的執行緒資源。類似船艙中的隔離區域。

訊號量主要是用來控制執行緒數的,規定好一些呼叫最大的併發量,超過指定的訊號量後,可以將請求丟棄或者延時處理,防止執行緒的不斷增長導致的服務異常。

基於hystrix、sentinel能夠很好的實現訊號量隔離,這種隔離技術在一定程度上能夠保障服務的可用性,在阿里雲期間曾經採用了該隔離技術,域名在搶注環節時,系統的併發量相對比較高,某些域名註冊局的QPS不高,如果放到一個執行緒池請求,會導致其他其他註冊局的請求比較慢。該隔離技術如果應用本身觸發一些FGC、CPU負載比較高的情況下,會導致應用全域性響應比較慢。

隔離注意事項

我們在做服務隔離的時候,還是有一些原則和事項需要注意的:

不可越界

:能在隔離模組內完成的邏輯,就儘量不要跨模組呼叫,減少依賴。

不可共享

:資料和資源能獨享的就儘量不要共享,不然很容易造成隔離失效。

考慮效率

:設計隔離模組的時候,要根據業務情況而定,充分的考慮到未來的拓補結構,減少呼叫效率的損失。

考慮顆粒度

:隔離模組設計的大小問題,過大和過小都不合適,需充分考慮。

服務的全面監控

:既然服務或使用者進行隔離了,那麼系統的複雜度肯定是比之前要高了,那麼針對多服務的全鏈路監控是必不可少的。

服務隔離的設計模式能降低依賴服務對整個系統的影響,保護有限的資源不被耗盡,提高了整個系統的可用性。

非同步通訊

通訊一般來說分同步和非同步兩種。同步通訊就像打電話,需要實時響應,而非同步通訊就像發郵件,不需要馬上回復。各有千秋,我們很難說誰比誰好。但是在面對超高吐吞量的場景下,非同步處理就比同步處理有比較大的優勢了,這就好像一個人不可能同時接打很多電話,但是他可以同時接收很多的電子郵件一樣。

同步呼叫雖然讓系統間只耦合於介面,而且實時性也會比非同步呼叫要高,但是我們也需要知道同步呼叫所帶來如下的問題:

同步呼叫需要被呼叫方的吞吐不低於呼叫方的吞吐。否則會導致被呼叫方因為效能不足而拖死呼叫方。換句話說,整個同步呼叫鏈的效能會由最慢的那個服務所決定。

同步呼叫會導致呼叫方一直在等待被呼叫方完成,如果一層接一層地同步呼叫下去,所有的參與方會有相同的等待時間。這會非常消耗呼叫方的資源(因為呼叫方需要儲存現場(Context)等待遠端返回,所以對於併發比較高的場景來說,這樣的等待可能會極度消耗資源)。

同步呼叫只能是一對一的,很難做到一對多的通訊方式。

同步呼叫最不好的是,如果被呼叫方有問題,那麼其呼叫方就會跟著出問題,於是會出現多米諾骨牌效應,故障一下就蔓延開來。

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

非同步通訊通常來說有三種方式:

1。請求響應式

在這種情況下,傳送方(sender)會直接請求接收方(receiver),被請求方接收到請求後,直接返回——收到請求,正在處理。

對於返回結果,有兩種方法,一種是傳送方時不時地去輪詢一下,問一下乾沒幹完。另一種方式是傳送方註冊一個回撥方法,也就是接收方處理完後回撥請求方。這種架構模型在以前的網上支付中比較常見,頁面先從商家跳轉到支付寶或銀行,商家會把回撥的 URL 傳給支付頁面,支付完後,再跳轉回商家的 URL。

很明顯,這種情況下還是有一定耦合的。是傳送方依賴於接收方,並且要把自己的回調發送給接收方,處理完後回撥。

2。透過訂閱的方式

接收方(receiver)會來訂閱傳送方(sender)的訊息,傳送方會把相關的訊息或資料放到接收方所訂閱的佇列中,而接收方會從佇列中獲取資料。傳送方並不關心訂閱方的處理結果,它只是告訴訂閱方有事要幹,收完訊息後給個ACK 就好了,你幹成啥樣我不關心。

這就好像下訂單的時候,一旦使用者支付完成了,需要把這個事件通知給訂單處理以及物流,訂單處理變更狀態,物流服務需要從倉庫服務分配相應的庫存並準備配送,後續這些處理的結果無需告訴支付服務。

為什麼要做成這樣?好了,重點來了!前面那種請求響應的方式就像函式呼叫一樣,這種方式有資料有狀態的往來(也就是說需要有請求資料、返回資料,服務裡面還可能需要儲存呼叫的狀態),所以服務是有狀態的。如果我們把服務的狀態給去掉(透過第三方的狀態服務來保證),那麼服務間的依賴就只有事件了。接收方依賴於傳送方。還是有一定的耦合。

3。透過 Broker 的方式

所謂 Broker,就是一箇中間人,傳送方(sender)和接收方(receiver)都互相看不到對方,它們看得到的是一個 Broker,傳送方向 Broker 傳送訊息,接收方向 Broker 訂閱訊息。如下圖所示。

完全的解耦。依賴於一箇中間件 Broker。這個 Broker是一個像資料匯流排一樣的東西,所有的服務要接收資料和傳送資料都發到這個總線上,就像協議一樣,讓服務間的通訊變得標準和可控。所有人都依賴於一個匯流排,需要有如下的特性:

(1)高可用的(整個系統的關鍵);

(2)高效能(水平擴充套件)的;

(3)持久化不丟資料的。

要做到這三條還是比較難的。當然,好在現在開源軟體或雲平臺上 Broker 的軟體是非常成熟的,所以節省了我們很多的精力。

事件驅動

上述的第二種和第三種方式就是比較著名的事件驅動架構(EDA – Event DrivenArchitecture)。正如前面所說,事件驅動最好是使用 Broker 方式,服務間透過交換訊息來完成交流和整個流程的驅動。

如下圖所示,這是一個訂單處理流程。下單服務通知訂單服務有訂單要處理,而訂單服務生成訂單後發出通知,庫存服務和支付服務得到通知後,一邊是佔住庫存,另一邊是讓使用者支付,等待使用者支付完成後通知配送服務進行商品配送。

每個服務都是“自包含”的。所謂“自包含”也就是沒有和別人產生依賴。而要把整個流程給串聯起來,我們需要一系列的“訊息通道(Channel)”。各個服務做完自己的事後,發出相應的事件,而又有一些服務在訂閱著某些事件來聯動。

好處:

服務間依賴沒有了,服務間是平等的,每個服務都是高度可重用並可被替換的。

服務的開發、測試、運維,以及故障處理都是高度隔離的。

服務間透過事件關聯,所以服務間是不會相互 block 的。

在服務間增加一些 Adapter(如日誌、認證、版本、限流、降級、熔斷等)相當容易。

服務間的吞吐也被解開了,各個服務可以按照自己的處理速度處理。

不好:

業務流程不再那麼明顯和好管理。整個架構變複雜。解決這個問題需要有一些視覺化的工具來呈現整體業務流程。

事件可能會亂序。這會帶來非常 Bug 的事。解決這個問題需要很好地管理一個狀態機的控制。

事務處理變得複雜。需要使用兩階段提交來做強一致性,或是退縮到最終一致性。

非同步通訊設計要點

為什麼需要非同步通訊?

解耦服務間依賴,Broker是最佳的解耦方式

服務隔離,故障不會擴散

非同步通訊架構具有更大的吞吐量,服務間效能不影響

Broker或者佇列的方式,能夠“削峰”,使吞吐均勻

服務獨立,部署,擴容,運維更容易

非同步通訊帶來的問題:

Broker需要高可用,而且分散式的Broker無法保證順序,設計最好不依賴順序

業務流程不直觀,需要一定的訊息追蹤機制,便於除錯

服務間只通過訊息互動,業務狀態需要一個總控方管理,常見於銀行的對賬系統,來確保資料是對的

處理方需要冪等的設計,能夠處理重複的訊息,ack丟失,傳送方重傳的情況

冪等

在分散式系統設計中冪等性設計中十分重要的,尤其在複雜的微服務中一套系統中包含了多個子系統服務,而一個子系統服務往往會去呼叫另一個服務,而服務呼叫服務無非就是使用 RPC 通訊或者 restful ,分散式系統中的網路延時或中斷是避免不了的,通常會導致服務的呼叫層觸發重試。具有這一性質的介面在設計時總是秉持這樣的一種理念:呼叫介面發生異常並且重複嘗試時,總是會造成系統所無法承受的損失,所以必須阻止這種現象的發生。

冪等定義

先看下百度百科對於冪等的定義:

在程式設計中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函式,或冪等方法,是指可以使用相同引數重複執行,並能獲得相同結果的函式。這些函式不會影響系統狀態,也不用擔心重複執行會對系統造成改變。

百度百科

簡單理解即:多次呼叫對系統的產生的影響是一樣的,即對資源的作用是一樣的。

冪等性強調的是外界透過介面對系統內部的影響, 只要一次或多次呼叫對某一個資源應該具有同樣的副作用就行。

冪等通常會有兩個維度:

1。 空間維度上的冪等,即冪等物件的範圍,是個人還是機構,是某一次交易還是某種型別的交易。

2。 時間維度上的冪等,即冪等的保證時間,是幾個小時、幾天還是永久性的。

冪等場景

根據上面對冪等性的定義我們得知:產生重複資料或資料不一致,這個絕大部分是由於發生了重複請求。

這裡的重複請求是指同一個請求在一些情況下被多次發起。

導致這個情況會有哪些場景呢?

微服務架構下,不同微服務間會有大量的基於 http,rpc 或者 mq 訊息的網路通訊,會有第三個情況【未知】,也就是超時。如果超時了,微服務框架會進行重試。

使用者互動的時候多次點選,無意地觸發多筆交易。

MQ 訊息中介軟體,訊息重複消費

第三方平臺的介面(如:支付成功回撥介面),因為異常也會導致多次非同步回撥

其他中介軟體/應用服務根據自身的特性,也有可能進行重試。

冪等設計

解決冪等問題,需要從整個請求鏈路進行分析:

1。控制重複請求

控制動作觸發源頭,即前端做冪等性控制實現

相對不太可靠,沒有從根本上解決問題,僅算作輔助解決方案。

主要解決方案:

控制操作次數,例如:提交按鈕僅可操作一次(提交動作後按鈕置灰)

及時重定向,例如:下單/支付成功後跳轉到成功提示頁面,這樣消除了瀏覽器前進或後退造成的重複提交問題。

2。過濾重複動作

控制過濾重複動作,是指在動作流轉過程中控制有效請求數量。

1)分散式鎖

利用 Redis 記錄當前處理的業務標識,當檢測到沒有此任務在處理中,就進入處理,否則判為重複請求,可做過濾處理。

例如訂單支付:

訂單發起支付請求,支付系統會去 Redis 快取中查詢是否存在該訂單號的 Key,如果不存在,則向 Redis 增加 Key 為訂單號。查詢訂單支付已經支付,如果沒有則進行支付,支付完成後刪除該訂單號的 Key。透過 Redis 做到了分散式鎖,只有這次訂單訂單支付請求完成,下次請求才能進來。

分散式鎖相比去重表,將放併發做到了快取中,較為高效。思路相同,同一時間只能完成一次支付請求。

2)token 令牌

應用流程如下:

1)服務端提供了傳送 token 的介面。執行業務前先去獲取 token,同時服務端會把 token 儲存到 redis 中;

2)然後業務端發起業務請求時,把 token 一起攜帶過去,一般放在請求頭部;

3)伺服器判斷 token 是否存在 redis 中,存在即第一次請求,可繼續執行業務,執行業務完成後將 token 從 redis 中刪除;

4)如果判斷 token 不存在 redis 中,就表示是重複操作,直接返回重複標記給 client,這樣就保證了業務程式碼不被重複執行。

3)緩衝佇列

把所有請求都快速地接下來,對接入緩衝管道。後續使用非同步任務處理管道中的資料,過濾掉重複的請求資料。

優點:同步轉非同步,實現高吞吐。

缺點:不能及時返回處理結果,需要後續監聽處理結果的非同步返回資料。

3。解決重複寫

實現冪等性常見的方式有:悲觀鎖(for update)、樂觀鎖、唯一約束。

1)悲觀鎖(Pessimistic Lock)

簡單理解就是:假設每一次拿資料,都有認為會被修改,所以給資料庫的行或表上鎖。

當資料庫執行 select for update 時會獲取被 select 中的資料行的行鎖,因此其他併發執行的 select for update 如果試圖選中同一行則會發生排斥(需要等待行鎖被釋放),因此達到鎖的效果。

select for update 獲取的行鎖會在當前事務結束時自動釋放,因此必須在事務中使用。(注意 for update 要用在索引上,不然會鎖表)

START TRANSACTION; # 開啟事務SELETE * FROM users WHERE id=1 FOR UPDATE;UPDATE users SET name= ‘xiaoming’ WHERE id = 1;COMMIT; # 提交事務

2)樂觀鎖(Optimistic Lock)

簡單理解就是:就是很樂觀,每次去拿資料的時候都認為別人不會修改。更新時如果 version 變化了,更新不會成功。

不過,樂觀鎖存在失效的情況,就是常說的 ABA 問題,不過如果 version 版本一直是自增的就不會出現 ABA 的情況。

UPDATE users SET name=‘xiaoxiao’, version=(version+1) WHERE id=1 AND version=version;

缺點:就是在操作業務前,需要先查詢出當前的 version 版本

另外,還存在一種:狀態機控制

例如:支付狀態流轉流程:待支付->支付中->已支付

具有一定要的前置要求的,嚴格來講,也屬於樂觀鎖的一種。

3)唯一約束

常見的就是利用資料庫唯一索引或者全域性業務唯一標識(如:source+序列號等)。

這個機制是利用了資料庫的主鍵唯一約束的特性,解決了在 insert 場景時冪等問題。但主鍵的要求不是自增的主鍵,這樣就需要業務生成全域性唯一的主鍵。

全域性 ID 生成方案:

UUID:結合機器的網絡卡、當地時間、一個隨記數來生成 UUID;

資料庫自增 ID:使用資料庫的 id 自增策略,如 MySQL 的 auto_increment。Redis 實現:透過提供像 INCR 和 INCRBY 這樣的自增原子命令,保證生成的 ID 肯定是唯一有序的。

雪花演算法-Snowflake:由 Twitter 開源的分散式 ID 生成演算法,以劃分名稱空間的方式將 64-bit 位分割成多個部分,每個部分代表不同的含義。

小結:按照應用上的最優收益,推薦排序為:樂觀鎖 > 唯一約束 > 悲觀鎖。

具體設計方案可參考我之前的設計架構圖,已在阿里團隊大規模使用:

核心設計要點如下:

1)冪等 key 提取能力:獲取唯一冪等 key,冪等key保證全域性唯一

2)分散式鎖服務能力:提供全域性加鎖、解鎖的能力,基於redis實現

3)高效能的寫入、查詢能力:針對冪等結果查詢與儲存,支援二級儲存,可以基於redis和leveldb。

4)高可用的冪等寫入、查詢能力:冪等儲存出現異常,不影響業務正常流程,增加容錯

服務狀態

狀態處理在微服務架構中很關鍵的設計,應用被微服務化後,如果一個功能對應於一個服務的話很難對大流量進行抗壓,所以單一功能可能是由一組相同的服務共同抗壓的。這種架構設計需要解決的就是上下文狀態的處理,或者幾個服務組合的業務邏輯狀態。

這種狀態稱之為服務狀態,如果做到無狀態服務,該服務對於運維執行水平擴容的時候,不必擔心出現邏輯混亂的情況。

例如,一個使用者請求一個活動頁,流量請求到M-A服務,並且完成了task,M-A服務記錄下該狀態,由於流量太大一個服務低擋不住,運維進行了擴容,新擴容一個相同服務M-B,此時該使用者再次請求活動頁準備領獎,因為負載均衡策略這次請求到了M-B,但是使用者卻發現task沒有完成。

如果該活動頁是無狀態的,就不會出現這種第一次請求與第二次請求不一樣的情況。

無狀態的服務 Stateless

在函數語言程式設計中很重要的設計思想就是無狀態的,函式只根據輸入描述邏輯與演算法,不儲存資料,不修改輸入資料,根據邏輯返回對應數值。無狀態服務的設計也可以依據該規則,

只根據請求引數,返回對應的資料,服務本身不儲存資料,

不記錄上下文

不儲存依賴服務的組合結果。

想法很美好,但是在服務設計中不可能不存在狀態。就像活動頁上,每個使用者對應的任務狀態,任務進度,甚至是每天的任務形式都可能不一樣。為了設計為設計為無狀態服務,就需要將這些狀態依賴外部儲存做持久化處理。服務根據userid獲取對應的狀態資訊。

其實就是將服務狀態轉移到外部儲存,使服務變成無狀態。外部儲存可以是Mysql、Redis或者想Etcd這種強一致性的儲存。

這種設計帶來的缺點就是需要依賴外部儲存,增加網路開銷(網路開銷對於一些業務場景來說也可以透過快取來解決,但是因為快取帶來的的缺點也是快取可能在多臺機器上,造成資源的浪費),增加處理時長。但是帶來的優勢對於運維管理來說擴充套件性非常容易,隨意做擴充或減少節點,因為服務是無狀態的,基本上不會產生異常。

總結來看,無狀態服務設計是微服務架構中的最佳實踐,微服務架構本身相對於單體應用提升了系統的複雜性,無狀態服務可以有效降低系統複雜度。由於無狀態服務設計很依賴外部儲存,所以對應的分散式儲存服務業應運而生。

有狀態的服務 Stateful

上面提到無狀態服務是微服務架構中的最佳實踐,為什麼還需要介紹有狀態服務?並不是說有狀態服務不好,或者一點優勢都沒有,針對一些業務場景其實有狀態服務設計也是有優勢的。

有狀態服務設計,對於一個使用者的請求每次會落在相同的機器上,也就是粘滯會話(Sticky Sessions)。不需要考慮與其他節點同步的問題,實現簡單。因此優勢如下:

資料狀態儲存在本地,不需要依賴外部儲存

因為資料儲存在本地所以擁有很強的一致性和可用性。

無狀態服務主要是把狀態轉移到第三方儲存服務中,所有節點之間存在服務狀態同步的問題。但是有狀態服務就是將不同的使用者資料在本地做資料分片。

Sticky Sessions也存在響應的問題,比如透過一致性hash來保證每次請求到相同機器上,這種方案可以做到水平擴容,但是可能導致訪問的不均勻。

相應的對於有狀態服務來說,資料遷移也是一個很大的挑戰,遷移有狀態服務就需要遷移對應的資料。並且如果有狀態服務其中一個節點掛掉,需要同步其他節點的狀態資料到新節點後,才能將新節點投入運營。所以有狀態服務雖然前期實現很容易,但是增加後期維護管理中。在微服務化的系統中,大量的服務如果都是有狀態的,那運維管理成本可能會累死人。

一種比較好的做法是使用到 Gossip 協議,透過這個協議在各個節點之間互相散播訊息來同步元資料,這樣新增或減少節點,叢集內部可以很容易重新分配(聽起來要實現好真的好複雜)。在有狀態的服務上做自動化伸縮的是有一些相關的真實案例的。比如,Facebook 的 Scuba,這是一個分散式的記憶體資料庫,它使用了靜態的方式,也就是上面的第一種方式。Uber 的 Ringpop 是一個開源的 Node。js 的根據地理位置分片的路由請求的庫(開源地址為:https://github。com/uber-node/ringpop-node )。

還有微軟的 Orleans,Halo 4 就是基於其開發的,其使用了 Gossip 協議,一致性雜湊和 DHT 技術相結合的方式。使用者透過其 ID 的一致性雜湊演算法對映到一個節點上,而這個節點儲存了這個使用者對應的 DHT,再透過 DHT 定位到處理使用者請求的位置,這個專案也是開源的(開源地址為:https://github。com/dotnet/orleans )。

關於可擴充套件的有狀態服務,這裡強烈推薦 Twitter 的美女工程師 Caitie McCaffrey 的演講 Youtube 影片《Building Scalable Stateful Service》(演講 PPT),其文字版是在 High Scalability 上的這篇文章《Making the Case for Building Scalable Stateful Services in the Modern Era》。

服務狀態的容錯設計

在容錯設計中,服務狀態是一件非常複雜的事。尤其對於運維來說,因為你要排程服務就需要排程服務的狀態,遷移服務的狀態就需要遷移服務的資料。在資料量比較大的情況下,這一點就變得更為困難了。

雖然上述有狀態的服務的排程透過 Sticky Session 的方式是一種方式,但我依然覺得理論上來說雖然可以這麼幹,這實際在運維的過程中,這麼幹還是件挺麻煩的事兒,不是很好的玩法。

很多系統的高可用的設計都會採取資料在執行時就複製的方案,比如:ZooKeeper、Kafka、Redis 或是 ElasticSearch 等等。在執行時進行資料複製就需要考慮一致性的問題,所以,強一致性的系統一般會使用兩階段提交。

這要求所有的結點都需要有一致的結果,這是 CAP 裡的 CA 系統。而也有的系統採用的是大多數人一致就可以了,比如 Paxos 演算法,這是 CP 系統。

但我們需要知道,即使是這樣,當一個結點掛掉了以後,在另外一個地方重新恢復這個結點時,這個結點需要把資料同步過來才能提供服務。然而,如果資料量過大,這個過程可能會很漫長,這也會影響我們系統的可用性。

所以,我們需要使用底層的分散式檔案系統,對於有狀態的資料不但在執行時進行多結點間的複製,同時為了避免掛掉,還需要把資料持久化在硬碟上,這個硬碟可以是掛載到本地硬碟的一個外部分散式的檔案卷。

這樣當結點掛掉以後,以另外一個宿主機上啟動一個新的服務例項時,這個服務可以從遠端把之前的檔案系統掛載過來。然後,在啟動的過程中就裝載好了大多數的資料,從而可以從網路其它結點上同步少量的資料,因而可以快速地恢復和提供服務。

這一點,對於有狀態的服務來說非常關鍵。所以,使用一個分散式檔案系統是排程有狀態服務的關鍵。

補償事務

前面,我們說過,分散式系統有一個比較明顯的問題就是,一個業務流程需要組合一組服務。這樣的事情在微服務下就更為明顯了,因為這需要業務上的一致性的保證。也就是說,如果一個步驟失敗了,那麼要麼回滾到以前的服務呼叫,要麼不斷重試保證所有的步驟都成功。

這裡,如果需要強一性的需求,那麼,在業務層上需要使用“兩階段提交”這樣的方式。但是好在我們的很多情況下並不需要這麼強的一致性,而且強一致性的最佳保證最好是在底層完成。或是像之前說的那樣 Stateful 的 Sticky Session 那樣在一臺機器上完成。在我們接觸到的大多數業務,其實只需要最終一致性就好。

ACID 和 BASE區別

ACID 保證了資料庫的實時一致性(銀行轉賬)

分散式系統,尤其微服務,這樣的方式是很難高效能。因為CAP 理論和提高效能,出現了 ACID 的一個變種 BASE。

Basic Availability

:基本可用。暫時不可用的狀態,後面會快速恢復。

Soft-state:

軟狀態。“有狀態”和“無狀態”的服務的一種中間狀態。為了提高效能,讓服務暫時儲存一些狀態或資料,這些狀態和資料不是強一致性的。

Eventual Consistency:

最終一致性,短暫的時間段內不一致,最終整個系統看到的資料是一致的。

BASE 的系統傾向於設計出更加有彈力的系統,這種系統的設計特點是,要保證在短時間內,就算是有資料不同步的風險,我們也應該允許新的交易可以發生,而後面我們在業務上將可能出現問題的事務給處理掉,以保證最終的一致性。

電商場景

ACID

:買同一本書的過程中,每個使用者的購買請求都需要把庫存鎖住,等減完庫存後,把鎖釋放出來,後續的人才能進行購買。同一時間不可能有多個使用者下單,有排隊的情況,不可能做出效能比較高的系統來。

BASE

:可以同時下單,不需要去真正地分配庫存,

非同步且批次

地處理訂單。下單沒有真正去減庫存,有可能會有超賣的情況。非同步地處理訂單時,發現庫存沒有了,於是才會告訴使用者你沒有購買成功。

BASE 這種玩法,其實就是亞馬遜的玩法,因為要根據使用者的地址去不同的倉庫檢視庫存,這個操作非常耗時,所以,不想做成非同步的都不行。

系統收到你的訂單了,然後一會兒你會收到你的訂單被確認的郵件,這時候才是真正地分配了庫存。某些時候,你先收到了下單的郵件,過一會又收到了沒有庫存的致歉的郵件。

業務補償

業務補償:首先做成冪等性的,一個事務失敗了或是超時了,不斷地重試,努力地達到最終我們想要的狀態。如果達到,狀態恢復到之前。如果

有變化

的請求,啟動整個事務的

業務更新機制

比如:要找很多方協調很多事,每一件事都要成功,否則整件事就做不到。

可以並行地做這些事,而如果某個事有變化,其它的事都會跟著出現一些變化。

1。業務補償機制需要做到下面這幾點:

(1)

起始狀態定義

:要達到什麼樣的狀態(比如:請假、機票、酒店這三個都必須成功,租車是可選的),如果條件不滿足,要回退到哪一個狀態。是整個業務的

(2)

狀態擬合

:當整條業務跑起來的時候,我們可以序列或並行地做這些事。對於旅遊訂票是可以並行的,但是對於網購流程(下單、支付、送貨)是不能並行的。總之,我們的系統需要努力地透過一系列的操作達到一個我們想要的狀態。如果達不到,就需要透過補償機制回滾到之前的狀態。

(3)

修改事務

:對於已經完成的事務進行整體修改。

純技術的世界裡也有這樣的事。比如,線上運維繫統進行水平擴充套件,先找到相應的機器,然後初始化環境,再部署上應用,再做相應的健康檢查,最後接入流量。這一系列的動作都要完全成功,所以,我們的部署系統就需要管理好整個過程和相關的執行狀態。

2。業務補償的設計重點

(1)支援冪等性,在上游有重試機制,因為要把一個業務流程執行完成,

(2)工作流引擎(高可用和穩定的),維護和監控整個過程的狀態,不要把這些狀態放到不同的元件中。把需求告訴它,幫我們搞定所有的事。如果有問題,也會回滾和補償的。阿里內部的tbbpm可以作為服務編排引擎,支援狀態監控和重試。

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

(4)業務補償的業務邏輯是強業務相關的,很難通用的。

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

3。事務補償TCC

TCC 模式可以解決 2PC 中的資源鎖定和阻塞問題,減少資源鎖定時間。

TCC 是 try 、confirm、cancel 三個詞語的縮寫,TCC 要求每個分支事務實現三個操作:預處理Try 、確認Confirm、撤銷 Cancel 。Try 操作坐業務檢查及資源預留,Confirm 做業務確認操作,Cancel 實現一個 與 Try 相反的操作即回滾操作。TM 首先發起所有的分支事務的 try 操作,任何一個分支事務的 try 操作執行失敗,TM 將會發起所有分支事務的cancel 操作,若try操作全部成功,TM 將會發起所有分支事務的Confirm 操作,其中 confirm/cancel 操作若執行失敗,TM 會進行重試。

TCC 分為三個階段:

Try 階段做業務檢查(一致性)及資源預留(隔離),此階段僅是一個初步操作,它和後續的 confirm 一起才能真正構成一個完整的業務邏輯。

Confirm 階段是做確認提交,Try階段所有分支事務執行成功後開始執行 Confirm。通常情況下,採用TCC 則認為Confirm 階段是不會出錯的。即:只要 Try成功,Confirm一定成功,

若Confirm 階段真的出錯了,就需要引入重試機制或人工處理

Cancel 階段是在業務執行錯誤需要回滾的狀態下執行分支事務的業務取消,預留資源釋放。通常情況下,採用TCC 則認為Cancel 階段也是一定成功的。

若Cancel 階段真的出錯了,需要引入重試機制會人工處理

TM事務管理器:TM事務管理器可以實現為獨立的服務,也可以讓全域性事務發起方充當TM 的角色,TM 獨立出來是為了稱為公用元件,是為了考慮系統結構和軟體複用。

TM在發起全域性事務時生成全域性事務記錄,全域性事務 ID 貫穿整個分散式事務呼叫鏈條,用來記錄事務上下文,追蹤和記錄狀態,由於Confirm 和 cancel 失敗需進行重試,因此需要實現為冪等,冪等性是指同一個操作無論請求多少次,其結果都相同。

分散式架構之彈力設計

執行流程:

1、啟動事務

2、呼叫各個服務中的 try 介面,分佈嘗試執行事務

3、判斷 try 的結果,如果都成功了就提交事務,否則就要回滾事務

4、提交事務就呼叫Confirm 介面,回滾事務就呼叫Cancel 介面

5、系統判斷要不要回滾是透過 ”是否丟擲異常“ 來判斷的,而不是根據返回值。

目前主流的 TCC 方案如下:

Seata :阿里雲推出的元件,支援較多方案,主推AT(二階段+分散式鎖)

tcc-transaction:不和底層rpc耦合,使用dubbo,http,thrift,webservice都可

tx-lcn:支援常用的dubbo,springcloud框架,維護不頻繁,熱度有所下降

hmily:國內工程師開發,非同步高效能TCC框架,適應國內環境

ByteTcc:國內開發,相容JTA規範的TCC框架

EasyTransaction:柔性事務、TCC、SAGA、可靠訊息等功能齊全,一站式解決

相對來說,Hmily 它更加輕量級,無需部署獨立的 TCC 協調器,也適合Dubbo 和 SpringCloud 環境

其優勢和缺點如下:

優勢

:TCC執行的每一階段都會提交本地事務並釋放鎖,並不需要等待其他事務的執行結果。而如果其他事務執行失敗,最後不是回滾,而是執行補償操作。這樣就避免了資源的長期鎖定和阻塞等待,執行效率比較高,屬於效能比較好的分散式事務方式。

缺點

程式碼侵入:需要認為編寫程式碼實現 try 、confirm、 cancel 程式碼侵入較多

開發成本高:一個業務需要拆分成 3個步驟,分別編寫業務實現,業務編寫比較複雜

安全性考慮:cancel 動作如果執行失敗,資源就無法釋放,需要引入重試機制,而重試導致重複執行,還有考慮重試的冪等性問題。

重試

重試是系統提高容錯能力的一種手段。在一次請求中,往往需要經過多個服務之間的呼叫,由於網路波動或者其他原因,請求可能無法正常到達服務端或者服務端的請求無法正常的返回,從而導致請求失敗,這種失敗往往可以透過重試的方式來解決。因此服務之間的重試機制提高了整個系統的故障恢復能力。

重試場景

典型的重試場景如下:

網路抖動問題造成的請求失敗,透過重試提高成功率。

由於系統負載高原因導致請求變慢,導致請求超時,透過重試提高成功率。

由於系統故障或服務不可用導致請求沒能成功,透過重試保證資料落地。

要重試:呼叫超時,被呼叫端返回了某種可以重試的錯誤(如繁忙中,流程中,維護中,資源不足等)

不要重試:業務級別的錯誤(如沒有許可權,或者是非法資料等),技術上的錯誤(如http的500等),這型別的錯誤重試下去沒有意義。

重試機制

重試機制:同步 、非同步模式

常見的重試主要有兩種模式:原地重試、非同步重試。

原地重試很好理解,就是程式在呼叫下游服務失敗的時候重新發起一次;非同步重試是將請求資訊丟到某個 mq 中,後續有一個程式消費到這個事件進行重試。

總的來說,原地重試實現簡單,能解決大部分網路抖動問題,但是如果是服務追求強一致性,並且希望在下游故障的時候不影響正常服務計算,這個時候可以考慮用非同步重試,上游服務可快速響應使用者請求由非同步消費者去完成重試。

重試演算法

無論是非同步還是同步模式,重試都有固定的幾個演算法:

線性退避:

每次失敗固定等待固定的時間。

隨機退避:

每次失敗等待隨機的時間重試。

指數退避:

連續重試時,每次等待的時間都是前一次等待時間的倍數。

綜合退避:

結合多種方式,比如線性 + 隨機抖動、指數 + 隨機抖動。加上隨機抖動可以打散眾多服務失敗時對下游的重試請求,防止雪崩。

為什麼需要等待下再重試?

因為網路抖動或者下游負載高,馬上重試成功的機率必然遠遠小於稍等一會再重試,相當於是讓下游先喘一口氣。

重試風暴

在微服務架構中,務必要注意避免重試風暴的產生。那麼,什麼是重試風暴呢?

分散式架構之彈力設計

如圖所示,資料庫出現了負載過高的情況,這個時候 Server 3 對它的請求會失敗。但是因為配置了重試機制,Server 3 最多對資料庫發起了3次請求。然而,這個時候荒唐的事情就出現了,為了避免抖動上游的每個服務都設定了超時重試3次的機制,這樣明明是一次業務請求,在上述中由於有3個環節存在變成了對資料庫的 27 (3 ^(n)) 次請求!這對原本就要崩潰的資料庫,更是雪上加霜。

微服務架構通常一次請求會經過數個甚至數百個服務處理,如果每個都這樣重試,資料庫壓力稍微彪高一點本身沒啥問題,但是很可能就因為重試導致雪崩。

如何防止重試風暴

單例項限流

首先,我們接受請求的是單個例項(程序)中的執行緒,所以可以以單程序的粒度進行限流。

關於限流,我們常用的是令牌桶或者滑動視窗兩種實現,這裡簡單實用滑動視窗實現。如下圖所示,每秒會產生一個Bucket,我們在Bucket裡記錄這一秒內對下游某個介面的成功、失敗數量。進而可以統計出每秒的失敗率,結合失敗率及失敗請求數判斷是否需要重試,每個 Bucket 在一定時間後過期。

如果下游大面積失敗,這種時候是不適合重試的,我們可以配置一個比如失敗率超過10%不重試的策略,這樣在單機層面就可以避免很多不必要的重試。

規範重試狀態碼

鏈路層面防止重試的最好做法是隻在最下游重試(我們上面圖的 Server3),Google SRE中指出了Google內部使用特殊錯誤碼的方式來實現:

約定一個特殊的業務狀態碼,它表示失敗了,但是別重試。

任何一個環節收到下游這個錯誤,不會重試,繼續透傳給上游。

透過這個模式,如果是資料庫抖動情況下,只有最下游的三個重試請求,上游服務判斷狀態碼知道不可重試不再重試。除此之外,在一些業務異常情況下也可透過狀態碼區分出無需重試的狀態。

這個方法可以有效避免重試風暴,但是缺陷是需要業務方強耦合上這個狀態碼的邏輯,一般需要公司層面做框架上的約束。

超時最佳化

在重試中,最頭疼的莫過於超時這種場景。我們知道網路超時,有可能請求壓根沒到下游服務就產生了,也可能是已經到達下游並且被處理了,只是來不及返回,一個典型的兩軍問題。

關於超時的情況,顯然無法透過錯誤碼識別,例如 A -> B -> C -> D 情況,如果C故障了,B可以獲取到錯誤碼,並返回給 A,但是因為 A 請求 B 超時了,所以是獲取不到錯誤碼的,這個時候 A 又會發起重試。那麼針對超時的情況有沒什麼辦法做最佳化,避免無必要的重試呢?

我認為有幾個地方是可以做的:

上游重試的請求不重試

超時導致的重試請求,在請求中帶一個 Flag 標記。如果下游發現上游是因為超時而發起的請求,自己在請求下游時如果再超時出錯,不再重試。例如 A -> B -> C 時,A 請求 B 超時重試,那麼重試時會帶上 Flag,B 發現 A 的重試請求中的 Flag,如果這個時候請求 C 失敗,那麼也不再重試請求,這樣就避免了重試被放大。

合理設定各個環節超時時間

A -> B -> C,B -> C 加上超時最多是 1s 時間,那麼 A -> B 的超時時間要 >= 1秒,否則可能 B 對 C 的重試還沒結束, A 就發起重試請求了。這類問題,我們可以透過分析離線資料發現環節中存在的不合理配置。

透過上述的最佳化,我們可以在一定程度上規避超時引發的重試風暴。

降低時延的重試

我們上文主要都在闡述為了保障請求 SLA 的重試以及規避重試風暴的手段,但是其實在實際應用過程中有一些低時延的業務場景也經常使用重試來最佳化,這個最佳化措施就是 backupRequest。

比方說使用者下單介面,我們希望更低的時延,因為延遲變高了使用者可能下單量就減少了,直接影響到公司的盈利。假設我們的介面時延 p95 是 300ms,也就是95%的使用者能在 300ms 內完成下單,雖然看起來很美好,但是可能存在 “長尾效應”,這尾部的 5% 對於業務來說也是至關重要的。

對於這種情況,常見的最佳化方案就是 backupRequest,簡單來說策略就是這樣的:

如果正常請求的超時時間是1s,那麼當超時時間超過x ms(eg。 500ms)不等超時時間直接再發起一個相同的請求,如果舊的請求超時,新的請求正常落在300ms以內,那麼我們這次請求不會超時且會在超時時間內完成。

這個機制對於時延敏感的業務非常有效,但是必須要保證請求是可重試的。

重試元件

spring-retry

該專案為Spring應用程式提供宣告式重試支援。它用於Spring批處理、Spring整合等。命令式重試也支援顯式使用。它主要是針對可能丟擲異常的一些呼叫操作,進行有策略的重試。

具體使用參考

spring-retry 官方文件:

https://github。com/spring-projects/spring-retry

spring-retry 存在兩個不友好設計:

重試實體限定為 Throwable 子類,說明重試針對的是可捕捉的功能異常為設計前提的,但是我們希望依賴某個資料物件實體作為重試實體,但 sping-retry框架必須強制轉換為Throwable子類。

如果你要以返回值的某個狀態來判定是否需要重試,可能只能透過自己判斷返回值然後顯式丟擲異常了

guava-retrying

guava retrying模組提供了一種通用方法,用於重試具有特定停止、重試和異常處理功能的任意Java程式碼,這些功能透過guava的謂詞匹配得到了增強。

github中該專案已經很久沒更新維護了,雖然很久沒維護,但不影響使用。

具體使用參考官方文件:https://github。com/rholder/guava-retrying

總結

1。首先定義好重試錯誤碼規範,明確哪些錯誤碼可以重試,哪些不可以重試,並不是所有的錯誤都可以重試。

2。重試之前要確保服務保證冪等性,如果不具備冪等服務重試後,會導致髒資料以及意想不到的後果。

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

4。如果超過重試次數,或是一段時間,那麼重試就沒有意義了。這個時候,說明這個錯誤不是一個短暫的錯誤,那麼我們對於新來的請求,就沒有必要再進行重試了,這個時候對新的請求直接返回錯誤就好了。但是,這樣一來,如果後端恢復了,我們怎麼知道呢,此時需要使用我們的熔斷設計了。這個在後面會說。

5。對於有事務相關的操作。我們可能會希望能重試成功,而不至於走業務補償那樣的複雜的回退流程。對此,我們可能需要一個比較長的時間來做重試,但是我們需要儲存請求的上下文,這可能對程式的執行有比較大的開銷,因此,有一些設計會先把這樣的上下文暫存在本機或是資料庫中,然後騰出資源來做別的事,過一會再回來把之前的請求從儲存中撈出來重試。

6。重試風暴,在微服務中是一大隱患,我們可以透過單機重試限流以及約定重試狀態碼來規避。

7。超時場景下的重試最佳化,上游因超時發起的流量,下游收到不再重複重試;合理配置鏈路超時時間。

8。針對時延敏感業務,可使用 backup request 減輕長尾效應。

限流

限速器 (Rate Limiter) 相信大家都不會陌生,在網路系統中,限速器可以控制客戶端傳送流量的速度,比如 TCP, QUIC 等協議。而在 HTTP 的世界中, 限速器可以限制客戶端在一段時間內傳送請求的次數,如果超過設定的閾值,多餘的請求就會被丟棄。

生活中也有很多這樣的例子,比如

使用者一分鐘最多能發 5 條微博

使用者一天最多能投 3 次票

使用者一小時登入超過5次後,需要等待一段時間才能重試。

限速器(Rate Limiter)有很多好處,可以防止一定程度的 Dos 攻擊,也可以遮蔽掉一些網路爬蟲的請求,避免系統資源被耗盡,導致服務不可用。

限流目的

一般來講,我們限流主要是透過對併發訪問/請求進行限速或者一段時間內請求的速率限制來保護系統。一旦達到我們限制的速率,則可以有以下一些處理方式:

拒絕服務(定向到錯誤頁或告知資源沒有了)

排隊或等待(比如秒殺、評論、下單)

降級(返回兜底資料或預設資料)

限流手段

限流手段:

限制總併發數(比如資料庫連線池、執行緒池)

限制瞬時併發數(如Nginx的limit_conn模組,用來限制瞬時併發連線數)

限制時間視窗內的平均速率(如Guava的RateLimiter、Nginx的limit_req模組,用來限制每秒的平均速率)

限制遠端介面呼叫速率、限制MQ的消費速率等。

另外,還可以根據網路連線數、網路流量、CPU或記憶體負載等來限流。

從我前面的介紹來看,限流的手段有很多的,在真正需要用到上面的方式來進行限流還需要掌握限流方面的知識。下面我會一一總結。從限流演算法、應用級限流、分散式限流、接入層限流來詳細學習限流技術手段。

限流演算法

固定視窗計數器演算法

固定視窗又稱固定視窗(又稱計數器演算法,Fixed Window)限流演算法,是最簡單的限流演算法,透過在單位時間內維護的計數器來控制該時間單位內的最大訪問量

分散式架構之彈力設計

固定視窗最大的優點在於

易於實現

;並且

記憶體佔用小

,我們只需要儲存時間視窗中的計數即可;它能夠確保處理更多的最新請求,不會因為舊請求的堆積導致新請求被餓死。當然也面臨著

臨界問題

,當兩個視窗交界處,瞬時流量可能為

2n

計數器滑動視窗演算法

為了防止瞬時流量,可以把固定視窗近一步劃分成多個格子,每次向後移動一小格,而不是固定視窗大小,這就是滑動視窗(Sliding Window)。

比如每分鐘可以分為6個10秒中的單元格,每個格子中分別維護一個計數器,視窗每次向前滑動一個單元格。每當請求到達時,只要視窗中所有單元格的計數總和不超過閾值都可以放行。TCP協議中資料包的傳輸,同樣也是採用滑動視窗來進行流量控制。

分散式架構之彈力設計

滑動視窗解決了計數器中的瞬時流量高峰問題,其實計數器演算法也是滑動視窗的一種,只不過視窗沒有進行更細粒度單元的劃分。對比計數器可見,當視窗劃分的粒度越細,則流量控制更加精準和嚴格。

不過當視窗中流量到達閾值時,流量會瞬間切斷,在實際應用中我們要的限流效果往往不是把流量一下子掐斷,而是讓流量平滑地進入系統當中。

漏桶演算法

如何更加平滑的限流?不妨看看漏桶演算法(Leaky Bucket),請求就像水一樣以任意速度注入漏桶,而桶會按照固定的速率將水漏掉;當注入速度持續大於漏出的速度時,漏桶會變滿,此時新進入的請求將會被丟棄。限流和整形是漏桶演算法的兩個核心能力。

基於漏桶(桶+恆定處理速率),可以起到對請求整流效果。漏桶演算法可基於執行緒池來實現,執行緒池使用固定容量的阻塞佇列+固定個數的處理執行緒來實現;最簡單且最常見的漏桶思想的實現就是基於SynchronousQueue的執行緒池,其相當於一個空桶+固定處理執行緒 : )。

優點:

漏出速率是固定的,可以起到整流的作用、漏斗演算法之後,變成了有固定速率的穩定流量,從而對下游的系統起到保護作用

缺點:

不能解決流量突發的問題,當短時間內有大量的突發請求時,即便此時伺服器沒有任何負載,每個請求也都得在佇列中等待一段時間才能被響應或直接被丟棄

不能充分使用系統資源,因為漏桶的漏出速率是固定的,即使在某一時刻下游能夠處理更大的流量,漏桶也不允許突發流量透過

令牌桶演算法

令牌桶演算法的原理是系統會以一個恆定的速度往桶裡放入令牌,而如果請求需要被處理,則需要先從桶裡獲取一個令牌,當桶裡沒有令牌可取時,請求則會被阻塞或等待,簡單的流程如下

分散式架構之彈力設計

所有的請求在處理之前都需要拿到一個可用的令牌才會被處理

根據限流大小,設定按照一定的速率往桶裡新增令牌

桶設定最大的放置令牌限制,當桶滿時、新新增的令牌就被丟棄或者拒絕

請求達到後首先要獲取令牌桶中的令牌,拿著令牌才可以進行其他的業務邏輯,處理完業務邏輯之後,將令牌直接刪除

令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之後將不會刪除令牌,以此保證足夠的限流

令牌桶演算法支援先消費後付款,比如一個請求可以獲取多個甚至全部的令牌,但是需要後面的請求付費。也就是說後面的請求需要等到桶中的令牌補齊之後才能繼續獲取。

漏桶與令牌桶對比

漏桶演算法與令牌桶演算法在表面看起來類似,很容易將兩者混淆。但事實上,這兩者具有截然不同的特性,且為不同的目的而使用。漏桶演算法與令牌桶演算法的區別在於:

漏桶演算法能夠強行限制資料的傳輸速率

令牌桶演算法能夠在限制資料的平均傳輸速率的同時還允許某種程度的突發傳輸

在某些情況下,漏桶演算法不能夠有效地使用網路資源。因為漏桶的漏出速率是固定的,所以即使網路中沒有發生擁塞,漏桶演算法也不能使某一個單獨的資料流達到埠速率。因此,漏桶演算法對於存在突發特性的流量來說缺乏效率。而令牌桶演算法則能夠滿足這些具有突發特性的流量。通常,漏桶演算法與令牌桶演算法結合起來為網路流量提供更高效的控制

應用級別限流

1。 限流總併發/連線請求數

對於一個應用級別的系統,一般都會有一個極限併發數,也可以理解為一個TPS/QPS 閾值,如果超過了這個,則系統會不響應請求,或者相應的特別慢,因此我們最好進行過載保護,防止系統被擊垮。

如果你使用過Tomcat, Connector其中一 種配置中有如下幾個引數。

• acceptCount: 如果Tomcat的執行緒都忙於響應,新來的連線會進入佇列排隊,如果超出排隊大小,則拒絕連線;

• maxConnections: 瞬時最大連線數,超出的會排隊等待;

• maxThreads: Tomcat能啟動用來處理請求的最大執行緒數,如果請求處理一直遠遠大於最大執行緒數,則會引起響應變慢甚至會僵死。

這些配置都可以在Tomcat官方文件找到,舉一反三,我們也可以去應用在Mysql、Redis中去找到對應的配置。

2。 限流總資源數

總資源主要就是資料資源的訪問,比如資料庫連線、執行緒等,可能會有很多執行緒進來,這時候可以去最佳化連線池的配置去控制限流。

3。限流某個介面的總併發/請求數

如果介面可能會有突發訪問情況,但又擔心訪問量太大造成崩潰,如搶購業務,那麼這個時候就需要限制這個介面的總併發/請求數/總請求數了。

之前我總結的使用hystrix去控制執行緒隔離,使用執行緒池去限流,或者我們可以使用原子類去控制最大介面併發數或者使用Semaphore去限流。

這種方式適合對可降級業務或者需要過載保護的服務進行限流,如搶購業務,超出限額,要麼讓使用者排隊,要麼告訴使用者沒貨了,這對使用者來說是可以接受的。

如果只是要控制最大請求數,使用計數器的方式,暴力控制就可以了,這種沒有什麼請求速率處理,平滑過渡。

4 限流某個介面的時間窗請求數

即一 個時間視窗內的請求數,如想限制某個介面/服務每秒/每分鐘/每天的請求數/呼叫量。

如一些基礎服務會被很多其他系統呼叫,比如商品詳情頁服務會呼叫基礎商品服務呼叫,但是更新量比較大有可能將基礎服務打掛。這時我們要對每秒/每分鐘的呼叫量進行限速。

使用Guava的Cache來儲存計數器,過期時間設定為2秒(保證能記錄l秒內的計數)。然後,我們獲取當前時間戳,取秒數來作為key進行計數統計和限流,這種方式簡單粗暴,但應付剛才說的場景夠用了。

5。平滑限流某個介面的請求數

之前的限流方式都不能很好地應對突發請求,即瞬間請求可能都被允許,從而導致一 些問題。因此 ,在一 些場景中需要對突發請求進行整形,整形為平均速率請求處理 (比如5r/s, 則每隔 200毫秒處理一個請求 ,平滑了速率)。這個時候有兩種演算法滿足我們的場景:令牌桶和漏桶演算法。Guava框架提供了令牌桶演算法實現,可直接拿來使用。還提供了一個類,可以模擬漏桶演算法來使用。

分散式限流

什麼是分散式限流呢?當應用為單點應用時,只要應用進行了限流,那麼應用所依賴的各種服務也都得到了保護。但線上業務出於各種原因考慮,多是分散式系統,單節點的限流僅能保護自身節點,但無法保護應用依賴的各種服務,並且在進行節點擴容、縮容時也無法準確控制整個服務的請求限制。

所以我們可以一句話概括:分散式限流就是保證分散式節點的分散式環境下多個節點的限流。

分散式限流最關鍵的是要將限流服務做成原子化,而解決方案可以使用Redis+Lua或者sentinel技術進行實現,透過這兩種技術可以實現高併發和高效能。

Sentinel

Sentinel分散式限流是啟動一個token server伺服器,其他sentinel client端就是token client端,當做限流操作時,從token server獲取token,獲取成功表示未觸發限流;否則表示觸發了限流;通訊出現異常,可配置降級走本地Sentinel限流機制。分散式限流文件可以參考《Sentinel叢集流控》

sentinel的分散式限流是token client呼叫以下方法到服務端獲取token,相當於是每次都會獲取acquireCount個token:

//獲取令牌Token, 引數規則Id,獲取令牌數,優先順序

TokenResult requestToken(Long ruleId, int acquireCount, boolean prioritized);

基於Redis限流

基於Redis做限流操作,使用lua指令碼保證命令原子性,比如qps設定為10,如果key不存在,就設定key過期時間1s,value=1;如果value小於10,則自增value;value達到10觸發流控。示例lua程式碼如下:

//獲取令牌Token, 引數規則Id,獲取令牌數,優先順序 TokenResult requestToken(Long ruleId, int acquireCount, boolean prioritized); local key = “rate。limit:” 。。 KEYS[1]local limit = tonumber(ARGV[1])local expire_time = ARGV[2]local is_exists = redis。call(“EXISTS”, key)if is_exists == 1 then if redis。call(“INCR”, key) > limit then return 0 else return 1 endelse redis。call(“SET”, key, 1) redis。call(“EXPIRE”, key, expire_time) return 1end]

接入層限流

接入層通常指請求流量的入口,該層的主要目的有:負載均衡、非法請求過濾、請求聚合、快取、降級、限流、A/B測試、服務質量監控等。一般我們理解的就是nginx做控制。

對於Nginx接入層限流可以使用Nginx自帶的兩個模組:連線數限流模組ngx _ http limit conn_ module和漏桶演算法實現的請求限流模組ngx_http limit_req module。還可以使用OpenResty提供的Lua限流模組lua-resty-limit-traffic應對更復雜的限流場景。

limit_conn用來對某個key對應的總的網路連線數進行限流,可以按照如IP、域名維度進行限流。

limit_req用來對某個key對應的請求的平均速率進行限流,有兩種用法:平滑模式(delay)和允許突發模式(nodelay)。

熔斷

熔斷機制這個詞對你來說肯定不陌生,它的靈感來源於我們電閘上的“保險絲”,當電壓有問題時(比如短路),自動跳閘,此時電路就會斷開,我們的電器就會受到保護。不然,會導致電器被燒壞,如果人沒在家或是人在熟睡中,還會導致火災。所以,在電路世界通常都會有這樣的自我保護裝置。

同樣,在我們的分散式系統設計中,也應該有這樣的方式。前面說過重試機制,如果錯誤太多,或是在短時間內得不到修復,那麼我們重試也沒有意義了,此時應該開啟我們的熔斷操作,尤其是後端太忙的時候,使用熔斷設計可以保護後端不會過載。

熔斷器設計

熔斷器即為呼叫端向服務端發起通訊時對下游服務的服務質量進行監測與策略熔斷的中介軟體。

如下圖:

分散式架構之彈力設計

上游服務 A 向下遊服務 B 發起通訊時首先經過了 Breaker中介軟體的處理。

如果按照上下游分層的話,由此可見:Breaker 屬於上游服務 A,即說明了上文熔斷是對呼叫端自身的一種保護。

Breaker 熔斷器主流程分為三步驟,Before 、Call、After。下文講訴熔斷器構造時會詳細描述。

熔斷器結構

狀態機

滑動計數器

執行三步驟

狀態機

熔斷器內部狀態機有三種狀態

Closed:熔斷器關閉狀態,即服務正常

Open:熔斷器開啟狀態,直接返回錯誤,不再發起請求(沒有網路開銷)

Half-Open:半熔斷狀態,介於關閉和開啟之間,此狀態下會發送少量請求給對應的服務,如果呼叫成功且達到一定比例則恢復服務關閉熔斷器,反之回到熔斷器開啟狀態

分散式架構之彈力設計

Init -> Close 熔斷器初始化為Close 狀態

Close -> Open 服務方提供服務異常,熔斷器由 Close 變為 Open

服務異常的定位由上游服務自己定義,比如:

服務方請求 Timeout

服務方請求 Http Code 非2xx

業務自定義範圍 errNo > 0

熔斷策略也是自定義,比如:

請求錯誤數>N

請求錯誤佔比>N%

連續請求錯誤數>N

Open -> Half Open 熔斷器度過冷卻期,準備嘗試恢復服務,狀態變為Half Open。

冷卻期:指當熔斷器開啟後, 在一段自定義的時間內拒絕任何服務。

Half Open -> Open 在熔斷器半開狀態內,發現服務方異常,則熔斷器再次Open。

Half Open -> Close 當熔斷器半開時間內,滿足恢復條件,則熔斷器變為 Close。

恢復條件為呼叫方自定義,比如:

連續成功數>N

連續成功請求佔比 > N%

滑動計數器

熔斷器的熔斷和恢復策略都是基於請求計數,並且每一個滑動時間視窗都會存在一個計數器。

所以說:熔斷策略是透過在某一個時間視窗內,計數器達到某一個閾值而觸發。

如下圖:

分散式架構之彈力設計

TimeLine 的每一個節點為一個時間視窗,每一個時間視窗對應了一組計數器。

注意

視窗的滑動操作不僅有正向時間推移,狀態機狀態流轉也會主動滑動視窗。

執行三步驟

上文有講,熔斷器執行機制主要分位三步驟:

Before:狀態機狀態檢查和流量攔截

Call:代理請求目標服務方

After:基於 Call 返回的 Response進行計數器指標統計和狀態更新

開源Hystrix熔斷器實現

分散式架構之彈力設計

從這個流程圖中,可以看到:

有請求來了,首先 allowRequest() 函式判斷是否在熔斷中,如果不是則放行,如果是的話,還要看有沒有到達一個熔斷時間片,如果熔斷時間片到了,也放行,否則直接返回出錯。

每次呼叫都有兩個函式 markSuccess(duration) 和 markFailure(duration) 來統計一下在一定的 duration 內有多少呼叫是成功還是失敗的。

判斷是否熔斷的條件 isOpen(),是計算一下 failure/(success+failure) 當前的錯誤率,如果高於一個閾值,那麼開啟熔斷,否則關閉。

Hystrix 會在記憶體中維護一個數組,其中記錄著每一個週期的請求結果的統計。超過時長長度的元素會被刪除掉。

Hystrix是目前最成熟的熔斷器之一,它是一個類庫,方便與我們的系統繼承,實現了上述功能外還提供監控、報警及視覺化操作能力,目前成熟的JVM微服務框架都有其整合方案。

Hystrix已處於維護期,不再新增新功能,推薦使用更輕量的resilience4j。

熔斷器設計要點

錯誤的型別

。根據不同的錯誤情況來調整相應的策略。重試一樣,需要對返回的錯誤進行識別。一些錯誤先走重試的策略(比如限流,或是超時),重試幾次後再開啟熔斷。一些錯誤是遠端服務掛掉,恢復時間比較長;這種錯誤不必走重試,可以直接開啟熔斷策略。

日誌監控

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

測試服務是否可用

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

手動重置

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

併發問題

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

資源分割槽

。熔斷器只對有問題的分割槽進行熔斷,而不是整體。比如,資料庫的分庫分表,某個分割槽可能出現問題,而其它分割槽還可用。單一的熔斷器會把所有的分割槽訪問給混為一談,從而,一旦開始熔斷,那麼所有的分割槽都會受到熔斷影響。或是出現一會兒熔斷一會兒又好,來來回回的情況。

重試錯誤的請求

。有時候,錯誤和請求的資料和引數有關係,所以,記錄下出錯的請求,在半開狀態下重試能夠準確地知道服務是否真的恢復。當然,這需要被呼叫端支援冪等呼叫,否則會出現一個操作被執行多次的副作用。

降級

當整個服務整體負載超出預設的上限閾值或即將到來的流量預計將會超過預設閾值時,為了保證重要或基本的服務能正常執行,拒絕部分請求或者將一些不重要或不緊急的服務或任務繼續服務的延遲使用或暫停使用。

降級意味著多種方案,當系統出現問題的時候,你有一個備選方案可以馬上切換,比如有一個介面的功能是實時預測未來一個月某個商品的採購數量,突然間依賴的上游系統出現問題了,那麼我們的介面就完全不可用了嗎?顯然這是不應該的,這時我介面就可以降級,返回昨天實時計算出來的結果,雖然準確性可能差一點,但系統能夠正常運轉,降級也分為 自動降級和 手動降級:

自動降級:是系統自動檢測到問題時自動切換,

手動降級:是系統檢測到問題報警,人為的透過開關進行切換

降級代表著系統相比降級之前其功能表現不如之前的完美(這個具體體現在功能準確性,可用性上等,如上面介面的例子)。

熔斷和降級共性與區別

共性:

目的 -> 都是從可用性、可靠性出發,提高系統的容錯能力。

最終表現->使某一些應用不可達或不可用,來保證整體系統穩定。

粒度 -> 一般都是服務級別,但也有細粒度的層面:如做到資料持久層、只許查詢不許增刪改等。

自治 -> 對其自治性要求很高。都要求具有較高的自動處理機制。

區別:

觸發原因 -> 服務熔斷通常是下級服務故障引起;服務降級通常為整體系統而考慮。

管理目標 -> 熔斷是每個微服務都需要的,是一個框架級的處理;而服務降級一般是關注業務,對業務進行考慮,抓住業務的層級,從而決定在哪一層上進行處理:比如在IO層,業務邏輯層,還是在外圍進行處理。

實現方式 -> 程式碼實現中的差異。

降級往往代表系統功能部分不可用,熔斷代表的是完全不可用。

降級使用場景

當整個微服務架構整體的負載超出了預設的上限閾值或即將到來的流量預計將會超過預設的閾值時,為了保證重要或基本的服務能正常執行,我們可以將一些

不重要

不緊急

的服務或任務進行服務的

延遲使用

暫停使用

降級方式

延遲服務:比如發表了評論,重要服務,比如在文章中顯示正常,但是延遲給使用者增加積分,只是放到一個快取中,等服務平穩之後再執行。

在粒度範圍內關閉服務(片段降級或服務功能降級):比如關閉相關文章的推薦,直接關閉推薦區

頁面非同步請求降級:比如商品詳情頁上有推薦資訊/配送至等非同步載入的請求,如果這些資訊響應慢或者後端服務有問題,可以進行降級;

頁面跳轉(頁面降級):比如可以有相關文章推薦,但是更多的頁面則直接跳轉到某一個地址

寫降級:比如秒殺搶購,我們可以只進行Cache的更新,然後非同步同步扣減庫存到DB,保證最終一致性即可,此時可以將DB降級為Cache。

讀降級:比如多級快取模式,如果後端服務有問題,可以降級為只讀快取,這種方式適用於對讀一致性要求不高的場景。

降級預案

在進行降級之前要對系統進行梳理,看看系統是不是可以丟卒保帥;從而梳理出哪些必須誓死保護,哪些可降級;比如可以參考日誌級別設定預案:

一般:比如有些服務偶爾因為網路抖動或者服務正在上線而超時,可以自動降級;

警告:有些服務在一段時間內成功率有波動(如在95~100%之間),可以自動降級或人工降級,併發送告警;

錯誤:比如可用率低於90%,或者資料庫連線池被打爆了,或者訪問量突然猛增到系統能承受的最大閥值,此時可以根據情況自動降級或者人工降級;

嚴重錯誤:比如因為特殊原因資料錯誤了,此時需要緊急人工降級。

服務降級分類

降級按照是否自動化可分為:自動開關降級(超時、失敗次數、故障、限流)和人工開關降級(秒殺、電商大促等)。

降級按照功能可分為:讀服務降級、寫服務降級。

降級按照處於的系統層次可分為:多級降級。

自動降級分類

超時降級:主要配置好超時時間和超時重試次數和機制,並使用非同步機制探測回覆情況

失敗次數降級:主要是一些不穩定的api,當失敗呼叫次數達到一定閥值自動降級,同樣要使用非同步機制探測回覆情況

故障降級:比如要呼叫的遠端服務掛掉了(網路故障、DNS故障、http服務返回錯誤的狀態碼、rpc服務丟擲異常),則可以直接降級。降級後的處理方案有:預設值(比如庫存服務掛了,返回預設現貨)、兜底資料(比如廣告掛了,返回提前準備好的一些靜態頁面)、快取(之前暫存的一些快取資料)

限流降級: 當我們去秒殺或者搶購一些限購商品時,此時可能會因為訪問量太大而導致系統崩潰,此時開發者會使用限流來進行限制訪問量,當達到限流閥值,後續請求會被降級;降級後的處理方案可以是:排隊頁面(將使用者導流到排隊頁面等一會重試)、無貨(直接告知使用者沒貨了)、錯誤頁(如活動太火爆了,稍後重試)

降級設計

1。分散式開關

根據上述需求,我們可以設定一個分散式開關,用於實現服務的降級,然後集中式管理開關配置資訊即可。具體方案如下:

分散式架構之彈力設計

2。配置中心

微服務降級的配置資訊是集中式的管理,然後透過視覺化介面進行友好型的操作。配置中心和應用之間需要網路通訊,因此可能會因網路閃斷或網路重啟等因素,導致配置推送資訊丟失、重啟或網路恢復後不能再接受、變更不及時等等情況,因此服務降級的配置中心需要實現以下幾點特性,從而儘可能的保證配置變更即使達到:

分散式架構之彈力設計

啟動主動拉取配置

—— 用於初始化配置(減少第一次定時拉取週期)

釋出訂閱配置

—— 用於實現配置及時變更(可以解決90%左右的配置變更)

定時拉取配置

—— 用於解決釋出訂閱失效或消失丟失的情況(可以解決9%左右的釋出訂閱失效的訊息變更)

離線檔案快取配置

—— 用於臨時解決重啟後連線不上配置中心的問題

可編輯式配置文件

—— 用於直接編輯文件的方式來實現配置的定義

提供Telnet命令變更配置

—— 用於解決配置中心失效而不能變更配置的常見

3。處理策略

當觸發服務降級後,新的交易再次到達時,我們該如何來處理這些請求呢?從微服務架構全域性的視角來看,我們通常有以下是幾種常用的降級處理方案:

頁面降級

—— 視覺化介面禁用點選按鈕、調整靜態頁面

延遲服務

—— 如定時任務延遲處理、訊息入MQ後延遲處理

寫降級

—— 直接禁止相關寫操作的服務請求

讀降級

—— 直接禁止相關度的服務請求

快取降級

—— 使用快取方式來降級部分讀頻繁的服務介面

針對後端程式碼層面的降級處理策略,則我們通常使用以下幾種處理措施進行降級處理:

拋異常

返回NULL

呼叫Mock資料

呼叫Fallback處理邏輯

4。降級技術選型

熔斷降級技術主要有 Netflex的 Hystrix、阿里的 Sentinel。

Hystrix有Java和Go版本的,Java版本的是Netflix公司開發並開源的,Go版本的是由afex(個人)建立的。

Hystrix引入以下手段來保護系統:

資源隔離(執行緒池和訊號量兩種手段的隔離)

限流

降級

熔斷(斷路器)

Hystrix如何設計實現這些手段呢?

使用命令模式將所有對外部服務(或依賴關係)的呼叫包裝在 HystrixCommand 或 HystrixObservableCommand 物件中,並將該物件放在單獨的執行緒中執行

每一個依賴都有自己對應的執行緒池或者訊號量,執行緒池耗盡時,拒絕請求

維護請求的各種狀態(成功,失敗,超時的次數)

當錯誤率到達一定閾值時,進行熔斷,過一定的時間後又恢復

提供降級,失敗,成功,熔斷後的回撥邏輯

實時的監控指標和配置資訊的修改

總結

首先,我們的服務不能是單點,所以,我們需要在我們的架構中冗餘服務,也就是說有多個服務的副本。這需要使用到的具體技術有:

負載均衡 + 服務健康檢查–可以使用像 Nginx 或 HAProxy 這樣的技術;

服務發現 + 動態路由 + 服務健康檢查,比如 Consul 或 Zookeeper;

自動化運維,Kubernetes 服務排程、伸縮和故障遷移。

然後,我們需要隔離我們的業務,要隔離我們的服務我們就需要對服務進行解耦和拆分,這需要使用到以前的相關技術。

bulkheads 模式:業務分片 、使用者分片、資料庫拆分。

自包含系統:所謂自包含的系統是從單體到微服務的中間狀態,其把一組密切相關的微服務給拆分出來,只需要做到沒有外部依賴就行。

非同步通訊:服務發現、事件驅動、訊息佇列、業務工作流。

自動化運維:需要一個服務呼叫鏈和效能監控的監控系統。

然後,接下來,我們就要進行和能讓整個架構接受失敗的相關處理設計,也就是所謂的容錯設計。這會用到下面的這些技術。

錯誤方面:呼叫重試 + 熔斷 + 服務的冪等性設計。

一致性方面:強一致性使用兩階段提交、最終一致性使用非同步通訊方式。

流控方面:使用限流 + 降級技術。

自動化運維方面:閘道器流量排程,服務監控。

我不敢保證有上面這些技術可以解決所有的問題,但是,只要我們設計得當,絕大多數的問題應該是可以扛得住的了。

下面我畫一個圖來表示一下。

分散式架構之彈力設計

在上面這個圖上,我們可以看到,有三個大塊的東西。

冗餘服務。透過冗餘服務的複本數可以消除單點故障。這需要服務發現,負載均衡,動態路由和健康檢查四個功能或元件。

服務解耦。透過解耦可以做到把業務隔離開來,不讓服務間受影響,這樣就可以有更好的穩定性。在水平層面上,需要把業務或使用者分片分割槽(業分做隔離,使用者做多租戶)。在垂直層面上,需要非同步通訊機制。因為應用被分解成了一個一個的服務,所以在服務的編排和聚合上,需要有工作流(像 Spring 的 Stream 或 Akk 的 flow 或是 AWS 的 Simple Workflow)來把服務給串聯起來。而一致性的問題又需要業務補償機制來做反向交易。

服務容錯。服務容錯方面,需要有重試機制,重試機制會帶來冪等操作,對於服務保護來說,熔斷,限流,降級都是為了保護整個系統的穩定性,並在可用性和一致性方面在出錯的情況下做一部分的妥協。

當然,除了這一切的架構設計外,你還需要一個或多個自動運維的工具,否則,如果是人肉運維的話,那麼在故障發生的時候,不能及時地做出運維決定,也就空有這些彈力設計了。比如:監控到服務效能不夠了,就自動或半自動地開始進行限流或降級。

更多技術實現細節參考我的GitHub專案:https://github。com/acticfox/redis-distributed-tools

TAG: 重試服務請求限流降級