Spring Cloud源碼分析——Hystrix服務(wù)容錯保護

IZONE崔叡娜(?′ω`?)

在微服務(wù)架構(gòu)中,系統(tǒng)被拆分為多個服務(wù)單元,各個服務(wù)單元之間通過服務(wù)注冊和訂閱的方式互相依賴。每個服務(wù)單元運行在不同進程中,依賴通過遠程調(diào)用的方式執(zhí)行。

運行期間,可能因為網(wǎng)絡(luò)原因或服務(wù)自身問題導致調(diào)用故障或延遲,而這些問題又會直接導致調(diào)用方的對外服務(wù)也出現(xiàn)延遲。如果調(diào)用方的請求不斷增加,最后就會出現(xiàn)因等待出現(xiàn)故障的依賴方響應而形成任務(wù)積壓,線程資源無法釋放,繼而最終導致自身服務(wù)癱瘓,更嚴重的后果會是故障蔓延導致整個系統(tǒng)的癱瘓。

為了解決以上的問題,斷路器等一系列服務(wù)保護機制應運而生。(參考了翟永超[程序猿DD])的《Spring Cloud微服務(wù)實戰(zhàn)》Hystrix工作原理(官方文檔翻譯)
本節(jié)從幾個方面對Spring Cloud Hystrix進行討論。

  • 服務(wù)降級
  • 依賴隔離
  • 斷路器

服務(wù)降級

Hystrix工作流程

從官方流程圖中,我們來解析下都發(fā)生了些什么

  • 構(gòu)建一個HystrixCommand或HystrixObservableCommand對象
    該對象表示一個依賴請求,向構(gòu)造函數(shù)中傳入請求依賴所需要的參數(shù)。根據(jù)返回響應來決定構(gòu)建HystrixCommand還是HystrixObservableCommand

  • 執(zhí)行命令
    1.execute() :該方法是阻塞的,從依賴請求中接收到單個響應(或者出錯拋異常)
    2.queue() : 從依賴請求中返回一個包含單個響應的Future對象
    3.observe() : 訂閱一個從依賴請求中返回的代表響應的Observable對象
    4.toObservable() : 返回一個Observable對象,只有當你訂閱它時,它才會執(zhí)行Hystrix命令并發(fā)射響應

    P.S 同步調(diào)用execute()實際上就是調(diào)用queue().get()方法,queue()方法的調(diào)用的是toObservable() .toBlocking().toFuture()。簡單來說,最終每一個HystrixCommand都是通過Observable來實現(xiàn)的,就算這些命令只是返回一個簡單的單個值。

  • 響應是否被緩存

  • 斷路器(circuit-breaker)是否打開
    當命令執(zhí)行時,Hystrix會檢查斷路器是否打開。如果已打開(或tripped),那Hystrix就不會再執(zhí)行命令,而是直接路由到getFallback() or resumeWithFallback(),獲取fallback方法,并執(zhí)行fallback邏輯

  • 線程池、隊列、信號量是否已滿
    跟上一步一樣,如果與該命令相關(guān)的線程池或隊列已滿,就不再執(zhí)行命令,直接執(zhí)行fallback邏輯

  • HystrixObservableCommand.construct()或HystrixCommand.run()
    通過自定義的方法邏輯來調(diào)用對依賴的請求

  • 計算回路指標(calculate circuit health)
    Hystrix會報告成功、失敗、拒絕和超時的指標給斷路器(circuit-breaker),斷路器(circuit-breaker)包含了一系列滑動窗口數(shù)據(jù),并通過該數(shù)據(jù)統(tǒng)計。
    P.S Hystrix使用這些統(tǒng)計數(shù)據(jù)來決定斷路器(circuit-breaker)是否應該熔斷,如果需要熔斷,則會在一定時間內(nèi)斷開依賴請求(短路請求),當再次檢查請求會重新關(guān)閉斷路器(circuit-breaker)

  • 獲取Fallback(getFallback)

  • 返回成功響應
    Hystrix命令執(zhí)行成功,將以O(shè)bservable形式返回響應給調(diào)用者。具體返回根據(jù)當初執(zhí)行的命令是啥。


斷路器(circuit-breaker)

上一節(jié)服務(wù)降級的關(guān)鍵操作之一是斷路器。在分布式架構(gòu)下,當某個服務(wù)單元發(fā)生故障之后,服務(wù)降級邏輯會因為Hystrix命令調(diào)用依賴服務(wù)超時時間,產(chǎn)生調(diào)用堆積、響應延遲。而通過斷路器的故障監(jiān)控,則可以直接切斷主邏輯的調(diào)用。當然,Hystrix的斷路器不僅僅是切斷主邏輯依賴這一操作,還有著更復雜的邏輯。

HystrixCommand或HystrixObservableCommand與HystrixCircuitBreaker交互流程

如圖所示,服務(wù)降級涉及到斷路器三個重要參數(shù):快照時間窗、請求總數(shù)下限、錯誤百分比下限。
1.快照時間窗:HystrixCommandProperties.metricsHealthSnapshotIntervalInMilliseconds()
斷路器確定是否打開需要統(tǒng)計一些請求或錯誤數(shù)據(jù),統(tǒng)計的時間范圍就是快照時間窗。默認為最近10秒。
2.請求總數(shù)下限:HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
在快照時間窗內(nèi),必須滿足請求總數(shù)下限才有資格進行熔斷。
3.錯誤百分比下限:HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
當請求總數(shù)在快照時間窗內(nèi)超過下限,并且又超過錯誤百分比下限,就會打開斷路器。

舉個例子,斷路器在10秒內(nèi)發(fā)現(xiàn)請求總數(shù)超過20,并且錯誤百分比超過了50%,這時斷路器會打開Open。打開之后,再有請求調(diào)用,將不會調(diào)用主邏輯,而是直接調(diào)用降級邏輯,這就不會出現(xiàn)等待5秒后才fallback。
主邏輯被熔斷后,Hystrix會啟動一個休眠時間窗HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(),在這個時間窗內(nèi),降級fallback邏輯會臨時成為主邏輯。當休眠時間窗到期后,斷路器會進入HalfOpen半開狀態(tài),釋放一次請求到原來的主邏輯上,如果正常返回,則斷路器閉合Close,主邏輯恢復,否則斷路器繼續(xù)打開Open,休眠時間窗重新計時。
簡單來說,斷路器可以實現(xiàn)自動發(fā)現(xiàn)錯誤并將降級邏輯切換為主邏輯,減少響應延遲的效果。

再結(jié)合HystrixCircuitBreaker源碼實現(xiàn)看下

// String is HystrixCommandKey.name() (we can't use HystrixCommandKey directly as we can't guarantee it implements hashcode/equals correctly)
        private static ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = new ConcurrentHashMap<String, HystrixCircuitBreaker>();

        /**
         * Get the {@link HystrixCircuitBreaker} instance for a given {@link HystrixCommandKey}.
         * <p>
         * This is thread-safe and ensures only 1 {@link HystrixCircuitBreaker} per {@link HystrixCommandKey}.
         * 
         * @param key
         *            {@link HystrixCommandKey} of {@link HystrixCommand} instance requesting the {@link HystrixCircuitBreaker}
         * @param group
         *            Pass-thru to {@link HystrixCircuitBreaker}
         * @param properties
         *            Pass-thru to {@link HystrixCircuitBreaker}
         * @param metrics
         *            Pass-thru to {@link HystrixCircuitBreaker}
         * @return {@link HystrixCircuitBreaker} for {@link HystrixCommandKey}
         */
        public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
            // this should find it for all but the first time
            HystrixCircuitBreaker previouslyCached = circuitBreakersByCommand.get(key.name());
            if (previouslyCached != null) {
                return previouslyCached;
            }

            // if we get here this is the first time so we need to initialize

            // Create and add to the map ... use putIfAbsent to atomically handle the possible race-condition of
            // 2 threads hitting this point at the same time and let ConcurrentHashMap provide us our thread-safety
            // If 2 threads hit here only one will get added and the other will get a non-null response instead.
            HystrixCircuitBreaker cbForCommand = circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreakerImpl(key, group, properties, metrics));
            if (cbForCommand == null) {
                // this means the putIfAbsent step just created a new one so let's retrieve and return it
                return circuitBreakersByCommand.get(key.name());
            } else {
                // this means a race occurred and while attempting to 'put' another one got there before
                // and we instead retrieved it and will now return it
                return cbForCommand;
            }
        }

HystrixCircuitBreaker實例化,首先定義ConcurrentHashMap類型的circuitBreakersByCommand對象,最后circuitBreakersByCommand.get(HystrixCommandKey.name())實例出HystrixCircuitBreaker。整個過程是線程安全的,并且確保一個HystrixCommandKey對應一個HystrixCircuitBreaker。

@Override
        public void markSuccess() {
            if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
                //This thread wins the race to close the circuit - it resets the stream to start it over from 0
                metrics.resetStream();
                Subscription previousSubscription = activeSubscription.get();
                if (previousSubscription != null) {
                    previousSubscription.unsubscribe();
                }
                Subscription newSubscription = subscribeToStream();
                activeSubscription.set(newSubscription);
                circuitOpened.set(-1L);
            }
        }

markSuccess()判斷當前狀態(tài)是不是HalfOpen或Closed,如果是的話,重置metrics指標(或叫Counter計數(shù)器),并關(guān)掉斷路器。

@Override
        public boolean attemptExecution() {
            if (properties.circuitBreakerForceOpen().get()) {
                return false;
            }
            if (properties.circuitBreakerForceClosed().get()) {
                return true;
            }
            if (circuitOpened.get() == -1) {
                return true;
            } else {
                if (isAfterSleepWindow()) {
                    if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
                        //only the first request after sleep window should execute
                        return true;
                    } else {
                        return false;
                    }
                } else {
                    return false;
                }
            }
        }

attemptExecution() 這個方法,當休眠時間窗到期后,如果當前斷路器狀態(tài)為Open或HalfOpen,就嘗試釋放請求到原來的主邏輯,從而實現(xiàn)主邏輯自動恢復。

通過一系列斷路器機制,實現(xiàn)了切斷故障資源的依賴、降級策略自動切換以及主邏輯自動恢復。相較于通過設(shè)置開關(guān)來監(jiān)控運維切換的傳統(tǒng)方式,斷路器模式使得微服務(wù)在依賴外部服務(wù)或資源的情況下得到很好的保護,同時還具備一些降級邏輯的業(yè)務(wù)需求自動化切換和恢復的能力。

依賴隔離

Hystrix依賴隔離

Hystrix采用艙壁模式來隔離相互之間的依賴關(guān)系,并限制對其中任何一個的并發(fā)訪問。Hystrix會為每一個HystrixCommand命令創(chuàng)建一個獨立的線程池,這樣即使某個再Hystrix命令包裝下的依賴服務(wù)出現(xiàn)延遲過高的情況,也只是對該依賴服務(wù)的調(diào)用產(chǎn)生影響,不會拖慢其他服務(wù)。
通過對依賴服務(wù)的線程池隔離實現(xiàn),有以下好處:

  1. 應用自身得到完全的保護,不會受不可控的依賴服務(wù)影響。即使是在給依賴服務(wù)分配的線程池被填滿的情況下;
  2. 有效降低了接入新服務(wù)的風險;
  3. 依賴服務(wù)自動恢復正常后,它的線程池會被清理并馬上恢復健康的服務(wù)。要比容器級別的清理恢復速度快很多;
  4. 依賴服務(wù)出現(xiàn)配置錯誤的時候,線程池可以快速做出反應(通過失敗次數(shù)、延遲、超時、拒絕等指標的波動);
  5. 依賴服務(wù)因?qū)崿F(xiàn)機制調(diào)整等原因造成其性能出現(xiàn)很大變化的時候,線程池同樣可以通過監(jiān)控指標信息做出反應;
  6. 每個線程池都提供了內(nèi)置的并發(fā)實現(xiàn),可以利用其為同步的依賴服務(wù)構(gòu)建異步的訪問。

當然,使用線程池隔離會增加系統(tǒng)的負載和開銷,如果很在意,Hystrix還有另外一種解決方案:信號量。(信號量默認值為10)。信號量同樣可以控制單個依賴服務(wù)的并發(fā)度,開銷和負載都要遠小于線程池,但是它不支持設(shè)置超時和實現(xiàn)異步訪問。

HystrixCommand和HystrixObservableCommand中有兩處支持信號量的使用,分別是隔離策略參數(shù)execution.isolation.strategy設(shè)置為SEMAPHORE,Hystrix會使用信號量替代線程池;Hystrix嘗試降級邏輯的時候,它會在調(diào)用線程中使用信號量。

第一節(jié)中@HystrixCommand將某個方法包裝成Hystrix命令,除了定義服務(wù)降級之外,還自動為該方法實現(xiàn)調(diào)用隔離。所以使用過程中,依賴隔離和服務(wù)降級是一體化實現(xiàn)的。

Hystrix源碼分析差不多就這樣,Hystrix運用了命令模式,線程安全,并發(fā)邏輯,設(shè)計巧妙。讀者可根據(jù)以上三個要點,針對性地閱讀源碼。后面可能會不定期更新,有興趣的朋友可以在評論區(qū)一起討論研究。

最后有件很重要的事,那就是麻煩點贊關(guān)注贊賞,謝謝(??????)??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容