SpringCloud-Hystrix服務熔斷與降級工作原理&源碼 | 京東物流技術團隊

先附上Hystrix源碼圖

在微服務架構中,根據業務來拆分成一個個的服務,服務與服務之間可以相互調用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和Feign來調用。爲了保證其高可用,單個服務通常會集羣部署。由於網絡原因或者自身的原因,服務並不能保證100%可用,如果單個服務出現問題,調用這個服務就會出現線程阻塞,此時若有大量的請求湧入,Servlet容器的線程資源會被消耗完畢,導致服務癱瘓。服務與服務之間的依賴性,故障會傳播,會對整個微服務系統造成災難性的嚴重後果,這就是服務故障的“雪崩”效應。

爲了解決這個問題,業界提出了斷路器模型。

在生活中,如果電路的負載過高,保險箱會自動跳閘,以保護家裏的各種電器,這就是熔斷器的一個活生生例子。在Hystrix中也存在這樣一個熔斷器,當所依賴的服務不穩定時,能夠自動熔斷,並提供有損服務,保護服務的穩定性。在運行過程中,Hystrix會根據接口的執行狀態(成功、失敗、超時和拒絕),收集並統計這些數據,根據這些信息來實時決策是否進行熔斷。

一、Hystrix簡介

Netflix has created a library called Hystrix that implements the circuit breaker pattern. In a microservice architecture it is common to have multiple layers of service calls.

. —-摘自官網

Netflix開源了Hystrix組件,實現了斷路器模式,SpringCloud對這一組件進行了整合。 在微服務架構中,一個請求需要調用多個服務是非常常見的,如下圖:

較底層的服務如果出現故障,會導致連鎖故障。當對特定的服務的調用的不可用達到一個閥值(Hystric 是5秒20次) 斷路器將會被打開。

斷路打開後,可用避免連鎖故障,fallback方法可以直接返回一個固定值。

Hystrix 是什麼?

在分佈式系統中,每個服務都可能會調用很多其他服務,被調用的那些服務就是依賴服務,有的時候某些依賴服務出現故障也是很正常的。

Hystrix 可以讓我們在分佈式系統中對服務間的調用進行控制,加入一些調用延遲或者依賴故障容錯機制

Hystrix 通過將依賴服務進行資源隔離,進而阻止某個依賴服務出現故障時在整個系統所有的依賴服務調用中進行蔓延;同時Hystrix 還提供故障時的 fallback 降級機制。

總而言之,Hystrix 通過這些方法幫助我們提升分佈式系統的可用性和穩定性。

Hystrix 的歷史

Hystrix 是高可用性保障的一個框架。Netflix(可以認爲是國外的優酷或者愛奇藝之類的視頻網站)的 API 團隊從 2011 年開始做一些提升系統可用性和穩定性的工作,Hystrix 就是從那時候開始發展出來的。

在 2012 年的時候,Hystrix 就變得比較成熟和穩定了,Netflix 中,除了 API 團隊以外,很多其他的團隊都開始使用 Hystrix。

時至今日,Netflix 中每天都有數十億次的服務間調用,通過 Hystrix 框架在進行,而 Hystrix 也幫助 Netflix 網站提升了整體的可用性和穩定性。

2018 年 11 月,Hystrix 在其 Github 主頁宣佈,不再開放新功能,推薦開發者使用其他仍然活躍的開源項目。維護模式的轉變絕不意味着 Hystrix 不再有價值。相反,Hystrix 激發了很多偉大的想法和項目,我們高可用的這一塊知識還是會針對 Hystrix 進行講解。

Hystrix 的設計原則

•對依賴服務調用時出現的調用延遲和調用失敗進行控制和容錯保護

•在複雜的分佈式系統中,阻止某一個依賴服務的故障在整個系統中蔓延。比如某一個服務故障了,導致其它服務也跟着故障。

•提供 fail-fast(快速失敗)和快速恢復的支持。

•提供 fallback 優雅降級的支持。

•支持近實時的監控、報警以及運維操作。

•阻止任何一個依賴服務耗盡所有的資源,比如 tomcat 中的所有線程資源。

•避免請求排隊和積壓,採用限流和 fail fast 來控制故障。

•提供 fallback 降級機制來應對故障。

•使用資源隔離技術,比如 bulkhead(艙壁隔離技術)、circuit breaker(斷路技術)來限制任何一個依賴服務的故障的影響。

•通過近實時的統計/監控/報警功能,來提高故障發現的速度。

•通過近實時的屬性和配置熱修改功能,來提高故障處理和恢復的速度。

•保護依賴服務調用的所有故障情況,而不僅僅只是網絡故障情況。

二、demo演示

1:在ribbon使用斷路器

改造serice-ribbon 工程的代碼,首先在pox.xml文件中加入spring-cloud-starter-hystrix的起步依賴:

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

在程序的啓動類SpringCloudServiceRibbonApplication 加@EnableHystrix註解開啓Hystrix:

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableHystrixDashboard
public class SpringCloudServiceRibbonApplication {
 public static void main(String[] args) {
     SpringApplication.run(SpringCloudServiceRibbonApplication.class, args);
 } 
 @Bean
 @LoadBalanced
 RestTemplate restTemplate(){
    return new RestTemplate();
 }
}



改造UserService類,在query方法上加上@HystrixCommand註解。該註解對該方法創建了熔斷器的功能,並指定了fallbackMethod熔斷方法,熔斷方法直接返回了一個對象,代碼如下:

@Service
public class UserService {

 @Autowired
 RestTemplate restTemplate;
 
 @HystrixCommand(commandKey="queryCommandKey",groupKey = "queryGroup",threadPoolKey="queryThreadPoolKey",fallbackMethod = "queryFallback",
 commandProperties = {
     @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100"),//指定多久超時,單位毫秒。超時進fallback
     @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "3"),//判斷熔斷的最少請求數,默認是10;只有在一個統計窗口內處理的請求數量達到這個閾值,纔會進行熔斷與否的判斷
     @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),//判斷熔斷的閾值,默認值50,表示在一個統計窗口內有50%的請求處理失敗,會觸發熔斷
 },
 threadPoolProperties = {
     @HystrixProperty(name = "coreSize", value = "30"),
     @HystrixProperty(name = "maxQueueSize", value = "100"),
     @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
     @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
     @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
     @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "100000")
 })
public List<User> query(){
     return restTemplate.getForObject("http://service-user/user/query",List.class);
}
 
public List<User> queryFallback(){
    List<User> list = new ArrayList<>();
     User user = new User();
     user.setId("1211");
     user.setName("queryFallback");
     list.add(user);
     return list;
}

}

啓動:service-ribbon 工程,當我們訪問http://127.0.0.1:9527/user/query,瀏覽器顯示:

[{
		"id": "id0",
		"name": "testname0"
	},
	{
		"id": "id1",
		"name": "testname1"
	},
	{
		"id": "id2",
		"name": "testname2"
	}
]

此時關閉 service-user工程,當我們再訪問http://127.0.0.1:9527/user/query,瀏覽器會顯示:

[{
	"id": "1211",
	"name": "queryFallback"
}]

這就說明當 service-user 工程不可用的時候,service-ribbon調用 service-user的API接口時,會執行快速失敗,直接返回一組字符串,而不是等待響應超時,這很好的控制了容器的線程阻塞。

2:在Feign中使用斷路器

Feign是自帶斷路器的,在D版本的Spring Cloud中,它沒有默認打開。需要在配置文件中配置打開它,在配置文件加以下代碼:

feign:
 hystrix:
 enabled: true

基於service-feign工程進行改造,只需要在FeignClient的UserService接口的註解中加上fallback的指定類就行了:

@FeignClient(value="service-user",fallback = UserServiceFallback.class)
public interface UserService {
 
 @RequestMapping(value="/user/query",method = RequestMethod.GET)
 public List<User> query();
 
}

UserServiceFallback需要實現UserService 接口,並注入到Ioc容器中,代碼如下:

@Component
public class UserServiceFallback implements UserService {

  @Override
  public List<User> query() {
     List<User> list = new ArrayList<>();
     User user = new User();
     user.setId("1211");
     user.setName("feignFallback");
     list.add(user);
     return list;
  }
  
}



啓動servcie-feign工程,瀏覽器打開http://127.0.0.1:9528/user/query注意此時service-user工程沒有啓動,網頁顯示:

[{
	"id": "1211",
	"name": "feignFallback",
	"date": null
}]

這證明斷路器起到作用了。

基於service-ribbon 改造,Feign的改造和這一樣。

首選在pom.xml引入spring-cloud-starter-hystrix-dashboard的起步依賴:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
 </dependency>

 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
 </dependency>

在主程序啓動類中加入@EnableHystrixDashboard註解,開啓hystrixDashboard:

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableHystrixDashboard
public class ServiceRibbonApplication {
 
 public static void main(String[] args) {
     SpringApplication.run(ServiceRibbonApplication.class, args);
 }
 
 @Bean
 @LoadBalanced
 RestTemplate restTemplate() {
     return new RestTemplate();
 }
 
}

3:Hystrix Dashboard (斷路器:Hystrix 儀表盤)

打開瀏覽器:訪問http://localhost:9527/hystrix,界面如下:

點擊monitor stream,進入下一個界面,訪問:http://127.0.0.1:9527/user/query

此時會出現監控界面:

4:Hystrix Turbine簡介

看單個的Hystrix Dashboard的數據並沒有什麼多大的價值,要想看這個系統的Hystrix Dashboard數據就需要用到Hystrix Turbine。Hystrix Turbine將每個服務Hystrix Dashboard數據進行了整合。Hystrix Turbine的使用非常簡單,只需要引入相應的依賴和加上註解和配置就可以了。

三:Hystrix流程圖

下面的流程圖展示了當使用Hystrix的依賴請求,Hystrix是如何工作的。

下面將更詳細的解析每一個步驟都發生哪些動作:

1.構建一個HystrixCommand或者HystrixObservableCommand對象。

第一步就是構建一個HystrixCommand或者HystrixObservableCommand對象,該對象將代表你的一個依賴請求,向構造函數中傳入請求依賴所需要的參數

如果構建HystrixCommand中的依賴返回單個響應,例如:

HystrixCommand command = new HystrixCommand(arg1, arg2);

如果依賴需要返回一個Observable來發射響應,就需要通過構建HystrixObservableCommand對象來完 成,例如:

•HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);

2.執行命令

•有4種方式可以執行一個Hystrix命令。

K value = command.execute();

Future<K> fValue = command.queue();

Observable<K> ohValue = command.observe(); //hot observable

•Observable<K> ocValue = command.toObservable(); //cold observable

同步調用方法execute()實際上就是調用queue().get()方法,queue()方法的調用的是toObservable().toBlocking().toFuture().也就是說,最終每一個HystrixCommand都是通過Observable來實現的,即使這些命令僅僅是返回一個簡單的單個值。

3.響應是否被緩存

•如果這個命令的請求緩存已經開啓,並且本次請求的響應已經存在於緩存中,那麼就會立即返回一個包含緩存響應的Observable(下面將Request Cache部分將對請求的cache做講解)。

4.迴路器是否打開

當命令執行時,Hystrix會檢查迴路器是否被打開。

如果迴路器被打開(或者tripped),那麼Hystrix就不會再執行命令,而是直接路由到第8步,獲取fallback方法,並執行fallback邏輯。

•如果迴路器關閉,那麼將進入第5步,檢查是否有足夠的容量來執行任務。(其中容量包括線程池的容量,隊列的容量等等)。

5.線程池、隊列、信號量是否已滿

•如果與該命令相關的線程池或者隊列已經滿了,那麼Hystrix就不會再執行命令,而是立即跳到第8步,執行fallback邏輯。

6.HystrixObservableCommand.construct() 或者 HystrixCommand.run()

•在這裏,Hystrix通過你寫的方法邏輯來調用對依賴的請求,通過下列之一的調用:

HystrixObservableCommand.construct() —返回一個發射響應的Observable或者發送一個onError()的通知。

如果run()或construct()方法超出命令的超時值,則線程將拋出TimeoutException(如果命令本身未在其自己的線程中運行,則將拋出單獨的計時器線程)。

在這種情況下,Hystrix將響應路由到8.獲取回退,如果該方法不取消/中斷,它將丟棄最終返回值run()或construct()方法。

請注意,沒有辦法強制潛在的線程停止工作 - 最好的Hystrix可以在JVM上執行的操作是將其拋出InterruptedException。

如果由Hystrix包裝的工作不遵守InterruptedExceptions,則Hystrix線程池中的線程將繼續其工作,儘管客戶端已經收到TimeoutException。

這種行爲可以使Hystrix線程池飽和,儘管負載“正確脫落”。

大多數Java HTTP客戶端庫不解釋InterruptedExceptions。

因此,請確保在HTTP客戶端上正確配置連接和讀/寫超時。

如果該命令沒有拋出任何異常並且它返回了響應,則Hystrix在執行一些日誌記錄和度量報告後返回此響應。

在run()的情況下,Hystrix返回一個Observable,它發出單個響應,然後發出onCompleted通知;

在construct()的情況下,Hystrix返回由construct()返回的相同Observable。

7.計算迴路指標[Circuit Health]

Hystrix會報告成功、失敗、拒絕和超時的指標給迴路器,迴路器包含了一系列的滑動窗口數據,並通過該數據進行統計。

•它使用這些統計數據來決定迴路器是否應該熔斷,如果需要熔斷,將在一定的時間內不在請求依賴[短路請求],當再一次檢查請求的健康的話會重新關閉迴路器。

8.獲取FallBack

•當命令執行失敗時,Hystrix會嘗試執行自定義的Fallback邏輯:

寫一個fallback方法,提供一個不需要網絡依賴的通用響應,從內存緩存或者其他的靜態邏輯獲取數據。如果再fallback內必須需要網絡的調用,更好的做法是使用另一個HystrixCommand或者HystrixObservableCommand。

如果你的命令是繼承自HystrixCommand,那麼可以通過實現HystrixCommand.getFallback()方法返回一個單個的fallback值。

如果你的命令是繼承自HystrixObservableCommand,那麼可以通過實現HystrixObservableCommand.resumeWithFallback()方法返回一個Observable,並且該Observable能夠發射出一個fallback值。

Hystrix會把fallback方法返回的響應返回給調用者。

如果你沒有爲你的命令實現fallback方法,那麼當命令拋出異常時,Hystrix仍然會返回一個Observable,但是該Observable並不會發射任何的數據,並且會立即終止並調用onError()通知。通過這個onError通知,可以將造成該命令拋出異常的原因返回給調用者。

失敗或不存在回退的結果將根據您如何調用Hystrix命令而有所不同:

•execute():拋出一個異常。

•queue():成功返回一個Future,但是如果調用get()方法,將會拋出一個異常。

•observe():返回一個Observable,當你訂閱它時,它將立即終止,並調用onError()方法。

•toObservable():返回一個Observable,當你訂閱它時,它將立即終止,並調用onError()方法。

9.返回成功的響應

•如果Hystrix命令執行成功,它將以Observable形式返回響應給調用者。根據你在第2步的調用方式不同,在返回Observablez之前可能會做一些轉換。

•execute():通過調用queue()來得到一個Future對象,然後調用get()方法來獲取Future中包含的值。

•queue():將Observable轉換成BlockingObservable,在將BlockingObservable轉換成一個Future。

•observe():訂閱返回的Observable,並且立即開始執行命令的邏輯,

•toObservable():返回一個沒有改變的Observable,你必須訂閱它,它才能夠開始執行命令的邏輯。

四:斷路器

下圖顯示了HystrixCommand或HystrixObservableCommand如何與HystrixCircuitBreaker及其邏輯和決策流程進行交互,包括計數器在斷路器中的行爲方式。

迴路器打開和關閉有如下幾種情況:

•假設迴路中的請求滿足了一定的閾值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())

•假設錯誤發生的百分比超過了設定的錯誤發生的閾值HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()

•迴路器狀態由CLOSE變換成OPEN

•如果迴路器打開,所有的請求都會被迴路器所熔斷。

•一定時間之後HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(),下一個的請求會被通過(處於半打開狀態),如果該請求執行失敗,迴路器會在睡眠窗口期間返回OPEN,如果請求成功,迴路器會被置爲關閉狀態,重新開啓1步驟的邏輯。

Hystrix的熔斷器實現在HystrixCircuitBreaker類中,比較重要的幾個參數如下:

1、circuitBreaker.enabled

熔斷器是否啓用,默認是true

2、circuitBreaker.forceOpen

熔斷器強制打開,始終保持打開狀態,默認是false

3、circuitBreaker.forceClosed

熔斷器強制關閉,始終保持關閉狀態,默認是false

4、circuitBreaker.requestVolumeThreshold

滑動窗口內(10s)的請求數閾值,只有達到了這個閾值,纔有可能熔斷。默認是20,如果這個時間段只有19個請求,就算全部失敗了,也不會自動熔斷。

5、circuitBreaker.errorThresholdPercentage

錯誤率閾值,默認50%,比如(10s)內有100個請求,其中有60個發生異常,那麼這段時間的錯誤率是60,已經超過了錯誤率閾值,熔斷器會自動打開。

6、circuitBreaker.sleepWindowInMilliseconds

熔斷器打開之後,爲了能夠自動恢復,每隔默認5000ms放一個請求過去,試探所依賴的服務是否恢復。

•在最新代碼中,已經棄用了allowRequest(),取而代之的是attemptExecution()方法。

和allowRequest()方法相比,唯一改進的地方是通過compareAndSet修改狀態值。通過attemptExecution()方法的返回值決定執行正常邏輯,還是降級邏輯。

1、如果circuitBreaker.forceOpen=true,說明熔斷器已經強制開啓,所有請求都會被熔斷。

2、如果circuitBreaker.forceClosed =true,說明熔斷器已經強制關閉,所有請求都會被放行。

3、circuitOpened默認-1,用以保存最近一次發生熔斷的時間戳。

4、如果circuitOpened不等於-1,說明已經發生熔斷,通過isAfterSleepWindow()判斷當前是否需要進行試探。

這裏就是熔斷器自動恢復的邏輯,如果當前時間已經超過上次熔斷的時間戳 + 試探窗口5000ms,則進入if分支,通過compareAndSet修改變量status,競爭試探的能力。其中status代表當前熔斷器的狀態,包含CLOSED, OPEN, HALF_OPEN,只有試探窗口之後的第一個請求可以執行正常邏輯,且修改當前狀態爲HALF_OPEN,進入半熔斷狀態,其它請求執行compareAndSet(Status.OPEN, Status.HALF_OPEN)時都返回false,執行降級邏輯。

5、如果試探請求發生異常,則執行markNonSuccess()

通過compareAndSet修改status爲熔斷開啓狀態,並更新當前熔斷開啓的時間戳。

6、如果試探請求返回成功,則執行markSuccess()

通過compareAndSet修改status爲熔斷關閉狀態,並重置接口統計數據和circuitOpened標識爲-1,後續請求開始執行正常邏輯。

說了這麼多,如何實現自動熔斷還沒提到,在Hystrix內部有一個Metric模塊,專門統計每個Command的執行狀態,包括成功、失敗、超時、線程池拒絕等,在熔斷器的中subscribeToStream()方法中,通過訂閱數據流變化,實現函數回調,當有新的請求時,數據流發生變化,觸發回調函數onNext

在onNext方法中,參數hc保存了當前接口在前10s之內的請求狀態(請求總數、失敗數和失敗率),其主要邏輯是判斷請求總數是否達到閾值requestVolumeThreshold,失敗率是否達到閾值errorThresholdPercentage,如果都滿足,說明接口的已經足夠的不穩定,需要進行熔斷,則設置status爲熔斷開啓狀態,並更新circuitOpened爲當前時間戳,記錄上次熔斷開啓的時間。

五:隔離

Hystrix採用艙壁模式來隔離相互之間的依賴關係,並限制對其中任何一個的併發訪問。

線程和線程池

客戶端(第三方包、網絡調用等)會在單獨的線程執行,會與調用的該任務的線程進行隔離,以此來防止調用者調用依賴所消耗的時間過長而阻塞調用者的線程。

•[Hystrix uses separate, per-dependency thread pools as a way of constraining any given dependency so latency on the underlying executions will saturate the available threads only in that pool]

您可以在不使用線程池的情況下防止出現故障,但是這要求客戶端必須能夠做到快速失敗(網絡連接/讀取超時和重試配置),並始終保持良好的執行狀態。

Netflix,設計Hystrix,並且選擇使用線程和線程池來實現隔離機制,有以下幾個原因:

•很多應用會調用多個不同的後端服務作爲依賴。

•每個服務會提供自己的客戶端庫包。

•每個客戶端的庫包都會不斷的處於變更狀態。

•[Client library logic can change to add new network calls]

•每個客戶端庫包都可能包含重試、數據解析、緩存等等其他邏輯。

•對用戶來說,客戶端庫往往是“黑盒”的,對於實現細節、網絡訪問模式。默認配置等都是不透明的。

•[In several real-world production outages the determination was “oh, something changed and properties should be adjusted” or “the client library changed its behavior.]

•即使客戶端本身沒有改變,服務本身也可能發生變化,這些因素都會影響到服務的性能,從而導致客戶端配置失效。

•傳遞依賴可以引入其他客戶端庫,這些客戶端庫不是預期的,也許沒有正確配置。

•大部分的網絡訪問是同步執行的。

•客戶端代碼中也可能出現失敗和延遲,而不僅僅是在網絡調用中。

使用線程池的好處

•通過線程在自己的線程池中隔離的好處是:

簡而言之,由線程池提供的隔離功能可以使客戶端庫和子系統性能特性的不斷變化和動態組合得到優雅的處理,而不會造成中斷。

注意:雖然單獨的線程提供了隔離,但您的底層客戶端代碼也應該有超時和/或響應線程中斷,而不能讓Hystrix的線程池處於無休止的等待狀態。

線程池的缺點

線程池最主要的缺點就是增加了CPU的計算開銷,每個命令都會在單獨的線程池上執行,這樣的執行方式會涉及到命令的排隊、調度和上下文切換。

•Netflix在設計這個系統時,決定接受這個開銷的代價,來換取它所提供的好處,並且認爲這個開銷是足夠小的,不會有重大的成本或者是性能影響。

線程成本

Hystrix在子線程執行construct()方法和run()方法時會計算延遲,以及計算父線程從端到端的執行總時間。所以,你可以看到Hystrix開銷成本包括(線程、度量,日誌,斷路器等)。

Netflix API每天使用線程隔離的方式處理10億多的Hystrix Command任務,每個API實例都有40多個線程池,每個線程池都有5-20個線程(大多數設置爲10)

•下圖顯示了一個HystrixCommand在單個API實例上每秒執行60個請求(每個服務器每秒執行大約350個線程執行總數):

在中間位置(或者下線位置)不需要單獨的線程池。

在第90線上,單獨線程的成本爲3ms。

在第99線上,單獨的線程花費9ms。但是請注意,線程成本的開銷增加遠小於單獨線程(網絡請求)從2跳到28而執行時間從0跳到9的增加。

對於大多數Netflix用例來說,這樣的請求在90%以上的開銷被認爲是可以接受的,這是爲了實現韌性的好處。

對於非常低延遲請求(例如那些主要觸發內存緩存的請求),開銷可能太高,在這種情況下,可以使用另一種方法,如信號量,雖然它們不允許超時,提供絕大部分的有點,而不會產生開銷。然而,一般來說,開銷是比較小的,以至於Netflix通常更偏向於通過單獨的線程來作爲隔離實現。

線程隔離-信號量

上面提到了線程池隔離的缺點,當依賴延遲極低的服務時,線程池隔離技術引入的開銷超過了它所帶來的好處。這時候可以使用信號量隔離技術來代替,通過設置信號量來限制對任何給定依賴的併發調用量。下圖說明了線程池隔離和信號量隔離的主要區別:

使用線程池時,發送請求的線程和執行依賴服務的線程不是同一個,而使用信號量時,發送請求的線程和執行依賴服務的線程是同一個,都是發起請求的線程。

您可以使用信號量(或計數器)來限制對任何給定依賴項的併發調用數,而不是使用線程池/隊列大小。這允許Hystrix在不使用線程池的情況下卸載負載,但它不允許超時和離開。如果您信任客戶端而您只想減載,則可以使用此方法。

HystrixCommand和HystrixObservableCommand支持2個地方的信號量:回退:當Hystrix檢索回退時,它總是在調用Tomcat線程上執行此操作。執行:如果將屬性execution.isolation.strategy設置爲SEMAPHORE,則Hystrix將使用信號量而不是線程來限制調用該命令的併發父線程數。您可以通過定義可以執行多少併發線程的動態屬性來配置信號量的這兩種用法。您應該使用在調整線程池大小時使用的類似計算來調整它們的大小(以毫秒爲單位返回的內存中調用可以在5000rps下執行,信號量僅爲1或2 ......但默認值爲10)。注意:如果依賴項與信號量隔離然後變爲潛在的,則父線程將保持阻塞狀態,直到基礎網絡調用超時。信號量拒絕將在限制被觸發後開始,但填充信號量的線程無法離開。

由於Hystrix默認使用線程池做線程隔離,使用信號量隔離需要顯示地將屬性execution.isolation.strategy設置爲ExecutionIsolationStrategy.SEMAPHORE,同時配置信號量個數,默認爲10。客戶端需向依賴服務發起請求時,首先要獲取一個信號量才能真正發起調用,由於信號量的數量有限,當併發請求量超過信號量個數時,後續的請求都會直接拒絕,進入fallback流程。

信號量隔離主要是通過控制併發請求量,防止請求線程大面積阻塞,從而達到限流和防止雪崩的目的。

隔離總結

線程池和信號量都可以做線程隔離,但各有各的優缺點和支持的場景,對比如下:

  線程切換 支持異步 支持超時 支持熔斷 限流 開銷
信號量
線程池

線程池和信號量都支持熔斷和限流。相比線程池,信號量不需要線程切換,因此避免了不必要的開銷。但是信號量不支持異步,也不支持超時,也就是說當所請求的服務不可用時,信號量會控制超過限制的請求立即返回,但是已經持有信號量的線程只能等待服務響應或從超時中返回,即可能出現長時間等待。線程池模式下,當超過指定時間未響應的服務,Hystrix會通過響應中斷的方式通知線程立即結束並返回。

請求合併

您可以使用請求合併器(HystrixCollapser是抽象父代)來提前發送HystrixCommand,通過該合併器您可以將多個請求合併爲一個後端依賴項調用。

下面的圖展示了兩種情況下的線程數和網絡連接數,第一張圖是不使用請求合併,第二張圖是使用請求合併(假定所有連接在短時間窗口內是“併發的”,在這種情況下是10ms)。

爲什麼使用請求合併

•事情請求合併來減少執行併發HystrixCommand請求所需要的線程數和網絡連接數。請求合併以自動方式執行的,不需要代碼層面上進行批處理請求的編碼。

全局上下文(所有的tomcat線程)

理想的合併方式是在全局應用程序級別來完成的,以便來自任何用戶的任何Tomcat線程的請求都可以一起合併。

例如,如果將HystrixCommand配置爲支持任何用戶請求獲取影片評級的依賴項的批處理,那麼當同一個JVM中的任何用戶線程發出這樣的請求時,Hystrix會將該請求與其他請求一起合併添加到同一個JVM中的網絡調用。

•請注意,合併器會將一個HystrixRequestContext對象傳遞給合併的網絡調用,爲了使其成爲一個有效選項,下游系統必須處理這種情況。

用戶請求上下文(單個tomcat線程)

如果將HystrixCommand配置爲僅處理單個用戶的批處理請求,則Hystrix僅僅會合並單個Tomcat線程的請求。

•例如,如果一個用戶想要加載300個影片的標籤,Hystrix能夠把這300次網絡調用合併成一次調用。

對象建模和代碼的複雜性

有時候,當你創建一個對象模型對消費的對象而言是具有邏輯意義的,這與對象的生產者的有效資源利用率不匹配。

例如,給你300個視頻對象,遍歷他們,並且調用他們的getSomeAttribute()方法,但是如果簡單的調用,可能會導致300次網絡調用(可能很快會佔滿資源)。

有一些手動的方法可以解決這個問題,比如在用戶調用getSomeAttribute()方法之前,要求用戶聲明他們想要獲取哪些視頻對象的屬性,以便他們都可以被預取。

或者,您可以分割對象模型,以便用戶必須從一個位置獲取視頻列表,然後從其他位置請求該視頻列表的屬性。

這些方法可以會使你的API和對象模型顯得笨拙,並且這種方式也不符合心理模式與使用模式。由於多個開發人員在代碼庫上工作,可能會導致低級的錯誤和低效率開發的問題。因爲對一個用例的優化可以通過執行另一個用例和通過代碼的新路徑來打破。

通過將合併邏輯移到Hystrix層,不管你如何創建對象模型,調用順序是怎樣的,或者不同的開發人員是否知道是否完成了優化或者是否完成。

•getSomeAttribute()方法可以放在最適合的地方,並以任何適合使用模式的方式被調用,並且合併器會自動將批量調用放置到時間窗口。

請求Cache

HystrixCommand和HystrixObservableCommand實現可以定義一個緩存鍵,然後用這個緩存鍵以併發感知的方式在請求上下文中取消調用(不需要調用依賴即可以得到結果,因爲同樣的請求結果已經按照緩存鍵緩存起來了)。

以下是一個涉及HTTP請求生命週期的示例流程,以及在該請求中執行工作的兩個線程:

請求cache的好處有:

•不同的代碼路徑可以執行Hystrix命令,而不用擔心重複的工作。

這在許多開發人員實現不同功能的大型代碼庫中尤其有用。

例如,多個請求路徑都需要獲取用戶的Account對象,可以像這樣請求:

Account account = new UserGetAccount(accountId).execute();

//or

Observable<Account> accountObservable = new UserGetAccount(accountId).observe();

Hystrix RequestCache將只執行一次底層的run()方法,執行HystrixCommand的兩個線程都會收到相同的數據,儘管實例化了多個不同的實例。

•整個請求的數據檢索是一致的。

每次執行該命令時,不再會返回一個不同的值(或回退),而是將第一個響應緩存起來,後續相同的請求將會返回緩存的響應。

•消除重複的線程執行。

由於請求緩存位於construct()或run()方法調用之前,Hystrix可以在調用線程執行之前取消調用。

如果Hystrix沒有實現請求緩存功能,那麼每個命令都需要在構造或者運行方法中實現,這將在一個線程排隊並執行之後進行。

六:源碼入口

Spring Boot 中有一種非常解耦的擴展機制:Spring Factories.這種機制實際上是仿照java中的SPI擴展機制實現的。

什麼是SPI 機制

SPI 的全名爲 Service Provider Interface,簡單總結下Java SPI機制的思想。我們系統裏抽象的各個模塊,往往有很多不同的實現方案,比如 日誌模塊的方案,xml解析模塊、jdbc模塊的方案等。面向的對象設計裏,我們一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及了具體的實現類,就違反了可插拔的原則,如果需要替換一種實現,就需要修改代碼。爲了實現在模塊裝配的時候能不在程序裏動態指明,這就需要一種服務發現機制。

Java SPI 就是提供這樣的一種機制:爲某個接口尋找服務的實現的機制,有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制很重要。

Spring Boot中的SPI機制

在Spring中也有一種類似與Java SPI的加載機制。它在META-INF/spring.factories文件中配置接口的實現類名稱,然後在程序中讀取這些配置文件並實例化。

這種自定義的SPI機制是Spring Boot Starter實現的基礎。

Spring Factories實現原理

spring-core包裏定義了SpringFactoriesLoader類,這個類實現了檢索META-INF/spring.factories文件,並獲取指定接口的配置的功能。在這個類中定義了兩個對外的方法:

loadFactories 根據接口類獲取其實現類的實例,這個方法返回的是對象列表。

loadFactoryNames 根據接口獲取其接口類的名稱,這個方法返回的是類名的列表。

上面的兩個方法的關鍵都是從指定的ClassLoader中獲取spring.factories文件,並解析得到類名列表

引用

https://github.com/Netflix/Hystrix/wiki

作者:京東物流 馮志文

來源:京東雲開發者社區 自猿其說Tech

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章