Spring Cloud Hystrix
Hystrix 產生背景
在微服務架構中,我們將系統拆分成了很多服務單元,各單元的應用間通過服務註冊 與訂閱的方式互相依賴。由於每個單元都在不同的進程中運行,依賴通過遠程調用的方式執行,這樣就有可能因爲網絡原因或是依賴服務自身問題出現調用故障或延遲,而這些問 題會直接導致調用方的對外服務也出現延遲,若此時調用方的請求不斷增加,最後就會因 等待出現故障的依賴方響應形成任務積壓,最終導致自身服務的癱瘓。
舉個例子,在一個電商網站中,我們可能會將系統拆分成用戶、訂單、庫存、積分、評論等一系列服務單元。用戶創建一個訂單的時候,客戶端將調用訂單服務的創建訂單接口,此時創建訂單接口又會向庫存服務來請求出貨(判斷是否有足夠庫存來出貨)。此時若 庫存服務因自身處理邏輯等原因造成響應緩慢,會直接導致創建訂單服務的線程被掛起,以等待庫存申請服務的響應,在漫長的等待之後用戶會因爲請求庫存失敗而得到創建訂單失敗的結果。如果在高併發情況之下,因這些掛起的線程在等待庫存服務的響應而未能釋放,使得後續到來的創建訂單請求被阻塞,最終導致訂單服務也不可用。
在微服務架構中,存在着那麼多的服務單元,若一個單元出現故障,就很容易因依賴 關係而引發故障的蔓延,最終導致整個系統的癱瘓,這樣的架構相較傳統架構更加不穩定。 爲了解決這樣的問題,產生了斷路器等一系列的服務保護機。
斷路器模式源於 Martin Fowler 的 Circuit Breaker 一文。“斷路器”本身是一種開關裝置,用於在電路上保護線路過載,當線路中有電器發生短路時,“斷路器”能夠及時切斷故障電路,防止發生過載、發熱甚至起火等嚴重後果。
在分佈式架構中,斷路器模式的作用也是類似的,當某個服務單元發生故障(類似用電器發生短路)之後,通過斷路器的故障監控(類似熔斷保險絲),向調用方返回一個錯誤響應,而不是長時間的等待。這樣就不會使得線程因調用故障服務被長時間用不釋放,避免了故障在分佈式系統中的蔓延。針對上述問題,SpringCloudHystrix實現了斷路器、線程隔離等一系列服務保護功能。 它也是基於Netflix的開源框架Hystrix實現的,該框架的目標在於通過控制那些訪問遠程系統、服務和第三方庫的節點,從而對延遲和故障提供更強大的容錯能力。Hystrix具備服 務降級、服務熔斷、線程和信號隔離、請求緩存、請求合併以及服務監控等強大功能。接下來,我們就從一個簡單示例開始對Spring Cloud Hystrix的學習與使用。
快速入門
在開始使用SpringCloud HyStrtix 實現斷路器之前,我們先用之前實現的一些內容作爲基礎,構建一個如下圖所示的服務調用關係。
我們在這裏需要啓動的工程有如下一些。
- springcloud-eureka集羣:服務註冊中心,端口號分別爲1111,2222,3333。
- springcloud-provider,springcloud-provider2兩個服務提供者:兩個實例的啓動端口分別爲8080,8081。
- springcloud-consumer服務的消費者:使用 Ribbon 實現的服務消費者,端口爲8989。在未加入熔斷器之前,關閉8081服務實例,發送 GET 請求到 http://localhost:8989/springcloud-consumer/testHystrix 可以在控制檯看出:java.net.ConnectException: Connection refused: connect
- 下面我們開始引入Spring Cloud Hystrix。
- 在springcloud-consumer工程中的pom.xml中的 dependency 節點中引入 spring-cloud-starter-hystrix依賴:
<!--熔斷器--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency>
- 在springcloud-consumer工程的主類 SpringcloudConsumerApplication 中使用 @EnableCircuitBreaker 註解開啓斷路器功能:
@EnableEurekaClient @EnableCircuitBreaker//該註解是開啓斷路器功能 @SpringBootApplication public class SpringcloudConsumerApplication { public static void main(String[] args) { SpringApplication.run(SpringcloudConsumerApplication.class, args); } }
- 改造服務的消費方式,新增HalloService 類,注入RestTemplate 實例。最後在queryAll函數上增加@HystrixCommand(fallbackMethod = "hellowFallback")註解來指定回調方法:
@RestController public class HallowController { private final String url = "http://springcloud-provider"; //使用restTemplate對rest接口進行調用 封裝的對象 //RestTemplate對象提供了多種便捷訪問遠程http服務的方法 是一種簡單便捷的restful服務模板類,是spring提供的用於訪問rest服務的客戶端模板類 @Autowired private RestTemplate restTemplate; //調用服務端的查詢所有的服務 @RequestMapping(value = "/testHystrix") @HystrixCommand(fallbackMethod = "hellowFallback",commandKey = "queryAllKey")//熔斷器指定操作的方法 public Object queryAll(){ System.out.println("==========進入訪問方法============"); List forObject = restTemplate.getForObject(url+"/queryAll", List.class); return forObject; } //熔斷器方法 建議把熔斷方法放在service中去處理 public String hellowFallback(){ return "error"; } }
- 下面我們來驗證一下通過斷路器實現的服務回調邏輯,重新啓動之前關閉的 8081 端口的服務提供者,確保測試註冊中心、兩個服務提供者一個服務消費者均已啓動,訪問 http://localhost:8989/springcloud-consumer/testHystrix ,當輪詢到兩個服務提供者並返回結果。此時我們繼續斷開 8081 的服務提供者,然後訪問 http://localhost:8989/springcloud-consumer/testHystrix,當輪詢到8081服務端時,輸出的內容爲 “error”,不再是之前的錯誤內容,Hystrix的服務回調生效。除了通過斷開具體的服務實例來模擬某個節點無法訪問的情況之外,我們還可以模擬一下服務阻塞(長時間未響應)的情況。我們對兩個服務的提供者做一些修改,具體如下:
//@GetMapping("/queryAll") 等於下面的註解 @RequestMapping(value = "/queryAll",method = RequestMethod.GET,produces = "application/json; charset=utf-8") public Object queryAll(){ List<User> users = userService.queryAll(); //測試熔斷 模擬服務阻塞的情況(長時間未響應) //讓線程等待幾秒鐘 int i = new Random().nextInt(3000); System.out.println("等待的時間爲 :~~~~~~~~~~~~~~~~~"+i); try { Thread.sleep(i); } catch (InterruptedException e) { e.printStackTrace(); } return users; }
private final String url = "http://springcloud-provider"; //使用restTemplate對rest接口進行調用 封裝的對象 //RestTemplate對象提供了多種便捷訪問遠程http服務的方法 是一種簡單便捷的restful服務模板類,是spring提供的用於訪問rest服務的客戶端模板類 @Autowired private RestTemplate restTemplate; //調用服務端的查詢所有的服務 @RequestMapping(value = "/testHystrix") //,commandKey = "queryAllKey" @HystrixCommand(fallbackMethod = "hellowFallback",commandKey = "queryAllKey")//熔斷器指定操作的方法 public Object queryAll(){ System.out.println("==========進入訪問方法============"); long l1 = System.currentTimeMillis(); List forObject = restTemplate.getForObject(url+"/queryAll", List.class); long l2 = System.currentTimeMillis(); System.out.println("調用服務的時間爲:"+(l2-l1)); return forObject; } //熔斷器方法 建議把熔斷方法放在service中去處理 public String hellowFallback(){ return "error"; }