服務容錯保護斷路器Hystrix之八:Hystrix資源隔離策略


在一個基於微服務的應用程序中,您通常需要調用多個微服務完成一個特定任務。不使用艙壁模式,這些調用默認是使用相同的線程來執行調用的,這些線程Java容器為處理所有請求預留的。在高服務器請求的情況下,一個性能較低的服務會“霸占”java容器中絕大多數線程,而其它性能正常的服務的請求則需要等待線程資源的釋放。最后,整個java容器會崩潰。艙壁模式能將遠程調用隔離在各個遠程調用自己的線程池中,因此單個性能出問題的服務能得到控制,java容器也不會崩潰。
Hystrix將遠程服務的請求托管在一個線程池中。即默認情況下,所有Hystrix命令(@HystrixCommand)共享同一個線程池來處理這些請求。該線程池中持有10個線程來處理各種遠程服務請求,可以是REST服務調用、數據庫訪問等。如下圖所示:
@HystrixCommand的默認配置適用於只有少量遠程調用的應用。幸運的是,Hystrix提供了簡單易用的方法實現艙壁來隔離不同的遠程資源調用。下圖說明了Hystrix將不同的遠程調用隔離在不同的“艙室”(線程池)中:

實現這種隔離的線程池,需要使用到@HystrixCommand注解提供的其他屬性。接下來,我們會:

  1. 為方法getLicensesByOrg()設置一個隔離的線程池
  2. 設置該線程池的線程數
  3. 設置隊列的容量,該隊列的作用是當線程池中的線程都處於工作狀態,接下來的請求會進入該隊列。
    下面的代碼演示了如何自定義艙壁:
@HystrixCommand(
        fallbackMethod = "buildFallbackLicenseList",
        threadPoolKey = "licenseByOrgThreadPool",
        threadPoolProperties = {
            @HystrixProperty(name = "coreSize",value="30"),
            @HystrixProperty(name="maxQueueSize", value="10")
        }
)
public List<License> getLicensesByOrg(String organizationId){
    randomlyRunLong();
    return licenseRepository.findByOrganizationId(organizationId);
}

 

資源隔離的兩種策略

1.依賴隔離概述

依賴隔離是Hystrix的核心目的。依賴隔離其實就是資源隔離,把對依賴使用的資源隔離起來,統一控制和調度。那為什么需要把資源隔離起來呢?主要有以下幾點:

1.合理分配資源,把給資源分配的控制權交給用戶,某一個依賴的故障不會影響到其他的依賴調用,訪問資源也不受影響。

2.可以方便的指定調用策略,比如超時異常,熔斷處理。

3.對依賴限制資源也是對下游依賴起到一個保護作用,避免大量的並發請求在依賴服務有問題的時候造成依賴服務癱瘓或者更糟的雪崩效應。

4.對依賴調用進行封裝有利於對調用的監控和分析,類似於hystrix-dashboard的使用。

 

Hystrix提供了兩種依賴隔離方式:線程池隔離 和 信號量隔離。兩種隔離方式都是限制對共享資源的並發訪問量,線程在就緒狀態、運行狀態、阻塞狀態、終止狀態間轉變時需要由操作系統調度,占用很大的性能消耗;而信號量是在訪問共享資源時,進行tryAcquire,tryAcquire成功才允許訪問共享資源。

如下圖,線程池隔離,Hystrix可以為每一個依賴建立一個線程池,使之和其他依賴的使用資源隔離,同時限制他們的並發訪問和阻塞擴張。每個依賴可以根據權重分配資源(這里主要是線程),每一部分的依賴出現了問題,也不會影響其他依賴的使用資源。

2.線程池隔離

如果簡單的使用異步線程來實現依賴調用會有如下問題:1、線程的創建和銷毀;2、線程上下文空間的切換,用戶態和內核態的切換帶來的性能損耗。

使用線程池的方式可以解決第一種問題,但是第二個問題計算開銷是不能避免的。Netflix在使用過程中詳細評估了使用異步線程和同步線程帶來的性能差異,結果表明在99%的情況下,異步線程帶來的幾毫秒延遲的完全可以接受的。

3.線程池隔離的優缺點

優點:

  • 一個依賴可以給予一個線程池,這個依賴的異常不會影響其他的依賴。
  • 使用線程可以完全隔離第三方代碼,請求線程可以快速放回。
  • 當一個失敗的依賴再次變成可用時,線程池將清理,並立即恢復可用,而不是一個長時間的恢復。
  • 可以完全模擬異步調用,方便異步編程。
  • 使用線程池,可以有效的進行實時監控、統計和封裝。

缺點:

  • 使用線程池的缺點主要是增加了計算的開銷。每一個依賴調用都會涉及到隊列,調度,上下文切換,而這些操作都有可能在不同的線程中執行。

4.Command Name&Command Group

Hystrix使用Command模式對依賴調用進行封裝。當我們寫一個調用繼承HystrixCommand的時候,可以指定一個名稱Command Name。如果不指定Hystrix將會使用getClass().getSimpleName()來默認獲取。如果要指定,可以使用如下代碼,使用HystrixCommandKey.Factory幫助類在構造函數中指定。

復制代碼
public HelloWorldCommand(String name)
    {
        //定義命令組 和 方法調用超時時間
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorldCommand")));
        this.name = name;
    }
復制代碼

而Command Group可以把一組Command歸為一組。如上例代碼,可以使用HystrixCommandGroupKey.Factory.asKey來指定Command Group。一般情況下,邏輯上是同一類型的會放在同一個Command Group中。比如,獲取用戶相關信息的依賴可以放在一起。

雖然Hystrix可以為每個依賴建立一個線程池,但是如果依賴成千上萬,建立那么多線程池肯定是不可能的。所以默認情況下,Hystrix會為每一個Command Group建立一個線程池。

 

5.Command Thread Pool

Hystrix可以指定創建或關聯上一個線程池,每一個線程池都有一個Key。這個線程池就是線程隔離的關鍵,所有的監控、緩存、調用等等都來自於這個線程池。可以通過如下代碼指定線程池:

復制代碼
public HelloWorldCommand(String name)
    {
        //定義命令組 和 方法調用超時時間
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorldCommand"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
        this.name = name;
    }
復制代碼

上文說到,默認情況下,每一個Command Group會自動創建一個線程池。那什么時候我們需要單獨指定線程池呢?因為線程池主要的目的是隔離,所以當有一些依賴在一個Command Group中,但是又有隔離的必要的時候,比如一個依賴的超時會用滿所有的線程池線程,而不應該影響其他的依賴。

 

6.基本實現原理

Command模式:Hystrix中大量使用rxjava來實現Command模式。所有自定義的Command,不管繼承於HystrixObservableCommand還是HystrixCommand,最終都繼承於AbstractCommand。Thread Pool,Command Group,Command Key都在AbstractCommand這里實現。

 

線程池的創建和管理:Hystrix的線程池在HystrixConcurrencyStrategy初始化,線程池是由ThreadPoolExecutor實現的。每個線程池默認初始化10個線程。Hystrix有個靜態類Factory,創建的線程池會被存儲在Factory中的ConcurrentHashMap中。ConcurrentHashMap的Key則是上文說到的CommandGroupKey或者指定的ThreadPoolKey。每次命令執行的時候,都會根據ThreadPoolKey去找到對應的線程池。線程池擁有一個繼承於rxjava中Scheduler的HystrixContextScheduler,用於在執行命令的時候,把命令在這個線程池上調度執行。

 

命令的執行:以execute()方法為例,Hystrix通過toObservable()來構造命令,構造過程中,定義了整個命令執行過程中的stage(未開始、執行中、完成執行、執行異常等等)的回調和處理方法。在executeCommandWithSpecifiedIsolation()方法中,使用exjava的subscribeOn方法,傳入上文提到的HystrixContextScheduler對象,通過HystrixContextScheduler的ThreadPoolScheduler把命令submit到ThreadPoolExecutor中去執行。

 

7.最佳實踐

對於那些本來延遲就比較小的請求(例如訪問本地緩存成功率很高的請求)來說,線程池帶來的開銷是非常高的,這時,你可以考慮采用其他方法,例如非阻塞信號量(不支持超時),來實現依賴服務的隔離,使用信號量的開銷很小。但絕大多數情況下,Netflix 更偏向於使用線程池來隔離依賴服務,因為其帶來的額外開銷可以接受,並且能支持包括超時在內的所有功能。

 

在一個分布式系統中,服務之間都是相互調用的,如下圖所示:

例如,我們容器(Tomcat)配置的線程個數為1000,服務A-服務R,其中服務I的並發量非常的大,需要500個線程來執行,此時,服務I又掛了,那么這500個線程很可能就夯死了,那么剩下的服務,總共可用的線程為500個,隨着並發量的增大,剩余服務掛掉的風險就會越來越大,最后導致整個系統的所有服務都不可用,直到系統宕機。這就是服務的雪崩效應。Hystrix就是用來做資源隔離的,比如說,當客戶端向服務端發送請求時,給服務I分配了10個線程,只要超過了這個並發量就走降級服務,就算服務I掛了,最多也就導致服務I不可用,容器的10個線程不可用了,但是不會影響系統中的其他服務。下面,我們就來具體說下這兩種隔離策略:

1、線程池

線程池隔離的示意圖如下:

上圖的左邊2/3是線程池資源隔離示意圖,右邊的1/3是信號量資源隔離示意圖,我們先來看左邊的示意圖。

當用戶請求服務A和服務I的時候,tomcat的線程(圖中藍色箭頭標注)會將請求的任務交給服務A和服務I的內部線程池里面的線程(圖中橘色箭頭標注)來執行,tomcat的線程就可以去干別的事情去了,當服務A和服務I自己線程池里面的線程執行完任務之后,就會將調用的結果返回給tomcat的線程,從而實現資源的隔離,當有大量並發的時候,服務內部的線程池的數量就決定了整個服務的並發度,例如服務A的線程池大小為10個,當同時有12請求時,只會允許10個任務在執行,其他的任務被放在線程池隊列中,或者是直接走降級服務,此時,如果服務A掛了,就不會造成大量的tomcat線程被服務A拖死,服務I依然能夠提供服務。整個系統不會受太大的影響。

2、信號量

 execution.isolation.strategy: "SEMAPHORE"
 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 3000
 execution.isolation.semaphore.maxConcurrentRequests: 100

信號量的資源隔離只是起到一個開關的作用,例如,服務X的信號量大小為10,那么同時只允許10個tomcat的線程(此處是tomcat的線程,而不是服務X的獨立線程池里面的線程)來訪問服務X,其他的請求就會被拒絕,從而達到限流保護的作用。

3、二者的比較

 

  線程池隔離 信號量隔離
線程 與調用線程非相同線程 與調用線程相同(jetty線程)
開銷 排隊、調度、上下文開銷等 無線程切換,開銷低
異步 可以是異步,也可以是同步。看調用的方法 同步調用,不支持異步
並發支持 支持(最大線程池大小hystrix.threadpool.default.maximumSize) 支持(最大信號量上限maxConcurrentRequests)  
是否超時  支持,可直接返回 不支持,如果阻塞,只能通過調用協議(如:socket超時才能返回)
是否支持熔斷 支持,當線程池到達maxSize后,再請求會觸發fallback接口進行熔斷 支持,當信號量達到maxConcurrentRequests后。再請求會觸發fallback
隔離原理 每個服務單獨用線程池 通過信號量的計數器
資源開銷   大,大量線程的上下文切換,容易造成機器負載高 小,只是個計數器  

 

4、總結

當請求的服務網絡開銷比較大的時候,或者是請求比較耗時的時候,我們最好是使用線程隔離策略,這樣的話,可以保證大量的容器(tomcat)線程可用,不會由於服務原因,一直處於阻塞或等待狀態,快速失敗返回。而當我們請求緩存這些服務的時候,我們可以使用信號量隔離策略,因為這類服務的返回通常會非常的快,不會占用容器線程太長時間,而且也減少了線程切換的一些開銷,提高了緩存服務的效率。

異步RPC

異步RPC主要目的是提高並發,比如你的接口,內部調用了3個服務,時間分別為T1, T2, T3。如果是順序調用,則總時間是T1 + T2 + T3;如果並發調用,總時間是Max(T1,T2,T3)。

當然,這里有1個前提條件,這3個調用直接,互相不依賴。

同樣,一般成熟的RPC框架,本身都提高了異步化接口,Future或者Callback形式。

同樣,Hystrix也提高了同步調用、異步調用方式,

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM