上一篇我們聊了微服務(wù)的全鏈路日志問題,這里我們就來聊聊所有微服務(wù)會遇到的第二個問題——熔斷。
可能你想說,熔斷不是流量大時才會出現(xiàn)嗎?我們公司流量又不大,應(yīng)該不用考慮熔斷的問題吧。其實不是的,這里你存在一定誤區(qū),希望通過下面內(nèi)容的內(nèi)容能助你走出誤區(qū)。
一、業(yè)務(wù)場景
為了便于理解下面的內(nèi)容,我們先從一個業(yè)務(wù)背景入手。
一個新零售架構(gòu)系統(tǒng)中,有一個通用用戶服務(wù)(很多頁面都會使用),它包含兩個接口。
第一個接口是用戶狀態(tài)接口,包含用戶車輛所在位置,并且在用戶信息展示頁面都會使用到,比如客服系統(tǒng)中的用戶信息頁面。
第二個接口是需要我們返回用戶一個可操作的權(quán)限列表,它包含一個通用權(quán)限,也包含用戶定制權(quán)限,而且每次用戶打開 App 時都會使用它。
而這兩個接口分別會碰到相應(yīng)問題,我們分開討論下。
(一)、第一個接口會遇到的問題:請求慢
用戶狀態(tài)的接口、服務(wù)間的調(diào)用關(guān)系如下圖所示。

在 Basic Data Service 中,有個接口 /currentCarLocation 需要調(diào)用第三方系統(tǒng)的數(shù)據(jù),但第三方響應(yīng)速度很慢且有時還會抽風,導(dǎo)致響應(yīng)時間更長,接口經(jīng)常出現(xiàn)超時報錯。
有一次,用戶反饋 App 整體運行速度慢到無法接受的程度。通過后臺監(jiān)控,我們查看了幾個 Thread Dump ,發(fā)現(xiàn) User API 與 Basic Data Service 的線程請求數(shù)爆滿,且所有的線程都在訪問第三方接口。因為連接數(shù)滿了,其他頁面便不再受理 User API 的請求,最終導(dǎo)致 App 整體出現(xiàn)了卡頓。
之前我們針對這個問題做過相關(guān)處理,考慮響應(yīng)時間長,我們就把超時的時間設(shè)置很長,雖然超時報錯概率小了,其他頁面也保持正常,但是會導(dǎo)致客服后臺查看用戶信息的頁面響應(yīng)時間長。
(二)、第二個接口會遇到的問題:流量洪峰緩存超時
用戶權(quán)限的接口、服務(wù)間的調(diào)用關(guān)系與上面類似,如下圖所示。

關(guān)于服務(wù)間的關(guān)系調(diào)用具體流程分為以下三個步驟:
App 訪問 User API;
User API 訪問基礎(chǔ)數(shù)據(jù)服務(wù)的接口 /commonAccesses;
基礎(chǔ)數(shù)據(jù)服務(wù)提供一個通用權(quán)限列表。因為權(quán)限列表對所有用戶都一樣,所以我們把它放在了 Redis 中,如果通用權(quán)限在 Redis 中找不到,我們再去數(shù)據(jù)庫中查找。
接下來聊聊服務(wù)間的關(guān)系調(diào)用流程中,我們曾經(jīng)遇到過的一些問題。
有一次,因為歷史代碼的原因,在流量高峰時 Redis 中的通用權(quán)限列表超時了,那一瞬間所有的線程都需要去數(shù)據(jù)庫中讀取數(shù)據(jù),導(dǎo)致 DB 中的 CPU 立馬飆到了 100%。
DB 掛后,緊接著 Basic Data Service 也掛了,因所有的線程堵塞了,我們獲取不到數(shù)據(jù)庫連接,導(dǎo)致 Basic Data Service 無法接受新的請求。
而 User API 因調(diào)用了 Basic Data Service 的線程出現(xiàn)了堵塞,以至于 User API 服務(wù)的所有線程也出現(xiàn)堵塞,即 User API 也掛了,導(dǎo)致 App 上的所有操作都不能使用,事情就鬧大了。
二、覆蓋場景
為了解決以上兩個問題,我們需要引入一個技術(shù),且它還得滿足以下兩個條件。
(一)、線程隔離
針對第一個問題,我們希望的處理方式是這樣,比如 User API 中每個服務(wù)配置的最大連接數(shù)是 1000,每次 API 調(diào)用 BasicDataService 的 /currentCarLocation 的速度就會很慢。
因此,我們希望控制 /currentCarLocation 的調(diào)用請求數(shù),保證不超過 50 條,以此保證至少還有 950 條的連接可用來處理常規(guī)請求。如果 /currentCarLocation 的調(diào)用請求數(shù)超過 50 條,我們就設(shè)計一些備用邏輯進行處理,比如在界面上給用戶進行提示。
(二)、熔斷
針對第二個問題,因那時 DB 沒有死鎖,流量洪峰緩存超時單純是因為壓力太大,此時我們可以使用 Basic Data Service 暫緩一點兒時間,讓它不接受新的請求,這樣 Redis 的數(shù)據(jù)會被補上,數(shù)據(jù)庫的連接也會降下來,我們的服務(wù)也就沒事了。
因此,我們希望這個技術(shù)能實現(xiàn)以下兩點需求:
- 發(fā)現(xiàn)近期某個接口的請求老出異常、有貓膩,先別訪問接口的服務(wù);
- 發(fā)現(xiàn)某個接口的請求老超時,先判斷接口的服務(wù)是否不堪重負,如果不堪重負,先別訪問它。
了解了這個技術(shù)需要滿足的條件后,我們就可以有針對性地進行選型了。
三、Hystrix 的設(shè)計思路
這次的技術(shù)選型過程很簡單,我們使用的是 Spring Cloud 中的 Hystrix 組件,市面上使用 Spring Cloud 都是因為需要使用它的組件。
關(guān)于 Hystrix,我還想多提一嘴。Spring Cloud Hystrix 的設(shè)計思想是事前配置熔斷機制,也就是說,要事先預(yù)見流量是什么情況?系統(tǒng)負載能力如何?然后預(yù)先配置好熔斷的機制。但這種操作的缺點是,一旦實際流量或系統(tǒng)狀況與預(yù)測的不一樣,那么預(yù)先配置好的機制就達不到預(yù)期的效果。
因此,開源 Hystrix 的公司 Netflix 想使用一個動態(tài)適應(yīng)更靈活的熔斷機制。不過 2018 年后官方已不再開發(fā)新功能,轉(zhuǎn)向開發(fā) Resilience4j 了,對于原有功能只做簡單維護。
接下來我們討論下 Hystrix 為什么能滿足我們的需求。
(一)、線程隔離機制
在 Hystrix 機制中,當前服務(wù)與其他接口存在強依賴關(guān)系,且每個依賴都有一個隔離的線程池。
比如下面這張架構(gòu)圖,當前服務(wù)調(diào)用接口 A 時,并發(fā)線程的最大個數(shù)是 10,調(diào)用接口 M 時,并發(fā)線程的最大個數(shù)是 5。

一般來說,當前服務(wù)依賴的一個接口響應(yīng)慢時,當前運行的線程會一直處于未釋放狀態(tài),最終把所有的連接線程卷入慢接口中。為此,在隔離線程的過程中,Hystrix 的做法是每個依賴接口(也可以配置成幾個接口共用)維護一個線程池,然后通過線程池的大小、排隊數(shù)等隔離每個服務(wù)對依賴接口的調(diào)用,這樣就不會出現(xiàn)前面的問題。
當然,在 Hystrix 機制中,我們除了使用線程池來隔離線程,還可以使用信號量(計數(shù)器)。
比如還是調(diào)用接口 A,因并發(fā)線程的最大個數(shù)是 10,在信號量隔離的機制中,Hystix 并不使用 1 個 size 為 10 的線程池來隔離,而是使用一個信號 semaphoresA,每當調(diào)用接口 A 時 semaphoresA++,A 調(diào)用完后 semaphoresA--,semaphoresA 一旦超過 10,不再調(diào)用。
這里留一個小問題:semaphoresA 如果超過 10,業(yè)務(wù)代碼會如何?
因為我們在使用線程池時經(jīng)常需要切換線程,資源損耗較大,而信號量的優(yōu)點恰巧就是切換快,大大解決了我們的煩惱。不過它也有一個缺點,即接口一旦開始調(diào)用就無法中斷。因為調(diào)用依賴的線程是當前請求的主線程,不像線程隔離,調(diào)用依賴的是另外 1 個線程,當前請求的主線程可以根據(jù)超時時間把它中斷。
這也就是說我們的第一個問題有救了,那第二個問題如何解決呢?這就涉及接下來我們要說的熔斷機制。
(二)、熔斷機制
關(guān)于 Hystrix 熔斷機制的設(shè)計思路,我們將從以下幾個方面來說說。
1、在哪種條件下會觸發(fā)熔斷?
熔斷判斷規(guī)則是某段時間內(nèi)調(diào)用失敗數(shù)超過特定的數(shù)量或比率時,就會觸發(fā)熔斷。那這個數(shù)據(jù)是如何統(tǒng)計出來的呢?
在 Hystrix 機制中,我們會配置一個不斷滾動的統(tǒng)計時間窗口 metrics.rollingStats.timeInMilliseconds,在每個統(tǒng)計時間窗口中,當調(diào)用接口的總數(shù)量達到 circuitBreakerRequestVolumeThreshold,且接口調(diào)用超時或異常的調(diào)用次數(shù)與總調(diào)用次數(shù)的占比超過 circuitBreakerErrorThresholdPercentage,此時就會觸發(fā)熔斷。
2、熔斷了會怎么樣?
如果熔斷被觸發(fā)了,在 circuitBreakerSleepWindowInMilliseconds 的時間內(nèi),我們便不再對外調(diào)用接口,而是直接調(diào)用本地的一個降級方法,如下代碼所示:
@HystrixCommand(fallbackMethod = "getCurrentCarLocationFallback")
3、熔斷后怎么恢復(fù)?
circuitBreakerSleepWindowInMilliseconds 到時間后,Hystrix 首先會放開對接口的限制(斷路器狀態(tài) HALF-OPEN),然后嘗試使用 1 個請求去調(diào)用接口,如果調(diào)用成功,則恢復(fù)正常(斷路器狀態(tài) CLOSED),如果調(diào)用失敗或出現(xiàn)超時等待,就需要再重新等待circuitBreakerSleepWindowInMilliseconds 的時間,之后再重試。
學到這,你可能就想問了,這個不斷滾動的時間窗口,到底是什么意思?
(三)、滾動(滑動)時間窗口
比如我們把滑動事件的時間窗口設(shè)置為 10 秒,并不是說我們需要在 1 分 10 秒時統(tǒng)計一次,1 分 20 秒時再統(tǒng)計一次,而是我們需要統(tǒng)計每一個 10 秒的時間窗口。
因此,我們還需要設(shè)置一個 metrics.rollingStats.numBuckets,假設(shè)我們設(shè)置 metrics.rollingStats.numBuckets 為 10,表示時間窗口劃分為 10 小份,每 1 份是 1 秒。然后我們就會 1 分 0 秒 - 1 分 10 秒統(tǒng)計 1 次、1 分 1 秒 - 1 分 11 秒統(tǒng)計 1 次、1 分 2 秒 - 1 分 12 秒統(tǒng)計 1 次……(即每隔 1 秒都有 1 個時間窗口。)
下圖就是 1 個 10 秒時間窗口,我們把它分成了 10 個桶。

每個桶中 Hystrix 首先會統(tǒng)計調(diào)用請求的成功數(shù)、失敗數(shù)、超時數(shù)和拒絕數(shù),再單獨統(tǒng)計每 10 個桶的數(shù)據(jù)(到了第 11 個桶時就是統(tǒng)計第 2 個桶到第 11 個桶的合計數(shù)據(jù))。
說到這,你可能會覺得知識有點割裂,接下來我把 Hystrix 調(diào)用接口的請求處理流程說一下。
(四)、Hystrix 調(diào)用接口的請求處理流程
這是 1 次調(diào)用成功的流程,如下圖所示:

這是 1 次調(diào)用失敗的流程,如下圖所示:

Hystrix 調(diào)用接口的請求處理流程結(jié)束后,我們就可以直接啟用它了。在 Spring Cloud 中啟用 Hystrix 的操作也比較簡單,我們不過多贅述了。
最后,關(guān)于 Hystrix,它還有包含 request caching(請求緩存) 和 request collapsing(請求合并)這兩個功能,因為它們與熔斷關(guān)系不大,這里我們也就不講了。
四、注意事項
把 Hystrix 的設(shè)計思路搞清楚后,使用它之前我們還需要考慮幾個注意事項:
(一)、數(shù)據(jù)一致性
這里,通過一個例子我們就好理解了。
假設(shè)服務(wù) A 更新了數(shù)據(jù)庫,在調(diào)用服務(wù) B 時直接降級了,那服務(wù) A 的數(shù)據(jù)庫更新是否需要回滾?
我們再舉一個復(fù)雜點的例子,比如服務(wù) A 調(diào)用了服務(wù) B,服務(wù) B 調(diào)用了服務(wù) C,我們在服務(wù) A 中成功更新了數(shù)據(jù)庫并成功調(diào)用了服務(wù) B,而服務(wù) B 調(diào)用服務(wù) C 時降級了,直接調(diào)用了 Fallback 方法,此時就會出現(xiàn)兩個問題:服務(wù) B 向服務(wù) A 返回成功還是失???服務(wù) A 的數(shù)據(jù)庫更新需不需要回滾?
以上兩個例子體現(xiàn)的就是數(shù)據(jù)一致性的問題。關(guān)于這個問題并沒有一個固定的設(shè)計標準,只是在不同需求下使用熔斷時,我們結(jié)合具體的情況設(shè)計即可。
(二)、超時降級
比如服務(wù) A 調(diào)用服務(wù) B 時,因為調(diào)用過程中 B 沒有在設(shè)置的時間內(nèi)返回結(jié)果,被判斷超時了,所以服務(wù) A 又調(diào)用了降級的方法,其實服務(wù) B 在接收到服務(wù) A 的請求后,已經(jīng)在執(zhí)行工作并且沒有中斷。等服務(wù) B 處理成功后,還是會返回處理成功的結(jié)果給服務(wù) A??墒欠?wù) A 又已經(jīng)走了降級的方法,而服務(wù) B 又已經(jīng)把工作做完了,此時就會導(dǎo)致服務(wù) B 中的數(shù)據(jù)出現(xiàn)異常。
(三)、用戶體驗
請求觸發(fā)熔斷后,一般會出現(xiàn)以下三種情況:
用戶讀數(shù)據(jù)的請求時遇到有些接口降級了,導(dǎo)致部分數(shù)據(jù)獲取不到,這時我們需要在界面上給用戶提供一定的提示,或讓用戶發(fā)現(xiàn)不了這部分數(shù)據(jù)的缺失;
用戶寫數(shù)據(jù)的請求時,熔斷觸發(fā)降級后,有些寫操作就會改為異步,后續(xù)處理對用戶沒有任何影響,但我們要根據(jù)實際情況判斷是否需要給用戶提供一定的提示;
用戶寫數(shù)據(jù)的請求時,熔斷觸發(fā)降級后,操作可能就回滾掉,此時我們必須提示讓用戶重新操作。
因此,服務(wù)調(diào)用觸發(fā)了熔斷降級時,我們需要把這些情況都考慮到以此保證用戶體驗,而不是僅僅保證服務(wù)器不宕機。
(四)、熔斷監(jiān)控
熔斷使用上線后,其實我們只是完成了熔斷設(shè)計的第一步。因為 Hystrix 是一個事前配置的熔斷框架,關(guān)于熔斷配置到底對不對,效果好不好,我們只有實際使用后才知道。
為此,實際使用時,我們還需要盯著 Hystrix 的監(jiān)控面板查看各個服務(wù)的熔斷數(shù)據(jù),然后根據(jù)實際情況再做調(diào)整。只有這樣,我們才能在真正使用熔斷時將服務(wù)器的異常損失降到最低。
五、總結(jié)
目前,市面上的熔斷框架已經(jīng)設(shè)計得非常好了。對于使用熔斷的人來說,雖然可以通過簡單配置或代碼書寫實現(xiàn)使用,但是因為它是高并發(fā)中非常核心的一個技術(shù),所以我們有必要搞懂它的原理機制及使用場景。
六、聯(lián)系我
微信公眾號:服務(wù)端技術(shù)精選
個人博客網(wǎng)站:http://www.jiangyi.cool