09 Spring Cloud的集群保護框架Hystrix
1.概述
在很多系統架構中都需要考慮橫向擴展、單點故障等問題,對於一個龐大的應用集群,部分服務或機器出現問題不可避免。在出現問題時,如何減少故障的影響、保障集群的高可用,成為一個重要的課題。在微服務集群中,不管是服務器,還是客戶端,都支持集群部署,本節將介紹Spring Cloud中所用的集群保護框架:Hystrix.
1.1實際問題
假設有如下應用程序
用戶範圍銷售模塊,服務通過Web接口或者其他方式訪問會員模塊,會員模塊訪問數據庫。如果數據庫因為某些原因變得不可用,會員模塊就會得到“數據庫無法訪問”的信息,並且會將此信息告知銷售模塊。在實際問題中,用戶會不斷地向銷售模塊發請求,而銷售模塊這繼續請求會員模塊,會員模塊會不斷地請求連接有問題的數據庫直到超時,但是還是會有大量的用戶請求(包括重試的)會發過來,導致整個應用不堪重負。可能情況會比這個更糟糕,用戶的請求不停的發送給銷售模塊,而由於數據庫的原因,會員模塊遲遲沒有響應,有可能導致整個機房的網絡阻塞,受害的不僅僅是這個應用程序,機房中的所有服務都有可能因為網絡的原因而癱瘓。
1.2傳統的解決方式
對於前面遇到的實際問題,可以選擇在連接數據庫的同時加上超時的配置,讓會員模塊快速響應。但這僅僅是解決了其中的一種情況,在實際情況中,會員模塊有可能出現問題,例如部分線程阻塞、進程假死等,在這些情況下,對外的服務銷售模塊面對大量的用戶與有故障的會員模塊,仍然無法獨善其身,前面所說的問題依舊會出現。
在當今的互聯網時代,面對大量的用戶請求,傳統或者單一的解決方式在復雜的急群中顯得力不從心,我們需要跟優雅更完善的方案來解決這些問題。
2.集群容錯框架Hystrix
在分布式環境中,總會有一些被依賴的服務會失效,例如像網絡短暫無法訪問、服務器宕機等情況。Hystrix是Netflix下的一個java庫,Spring Cloud將Hystrix整合到Netflix項目中,Hystrix通過添加延遲閾值以及容錯的邏輯,來幫助我們控制分布式系統間組件的交互。Hystrix通過隔離服務間的訪問點、停止他們之間的級聯故障、提供可回退操作來實現容錯。
例如我們之前講到的問題,如果數據庫層面出現問題,銷售模塊在訪問會員模塊時必然會出現超時的情況,此時可以將會員模塊隔離開來,銷售模塊短時間內不再調用會員模塊,並且會快速響應用戶的請求,從而保證銷售模塊自身乃至整個集群的穩定性,這是Hystrix可以解決的問題。加入了容錯機制,當會員模塊或者數據庫不可用時,銷售模塊將對其進行“熔斷”,在一定時間內,銷售模塊不會再調用會員模塊,以維持自身的穩定,結構圖就變成下面的圖了
Hystrix主要實現以下的功能:
> 當所依賴的網絡服務發生延遲或者失敗時,對訪問的客戶端程序進行保護,就像上面的例子對銷售模塊進行保護一樣;
> 在分布式系統中停止級聯故障;
> 網絡服務恢復正常後,可以快速恢復客戶端的訪問能力;
> 調用失敗時執行服務回退;
> 可支持實時監控、報警和其他操作。
3.第一個Hystrix程序
本例將編寫一個簡單的Hello World程序,展示Hystrix的基本功能。
3.1 創建服務提供者
使用SpringBoot的spring-boot-starter-web項目,創建一個普通的web項目,發布兩個測試服務用於測試,項目目錄及代碼清單如下
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.triheart</groupId> <artifactId>hystrixserver</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>1.5.4.RELEASE</version> </dependency> </dependencies> </project>View Code
ServerApp.java
package com.triheart.hystrixserver;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
/**
* @author 阿遠
* Date: 2018/8/29
* Time: 21:16
*/
@SpringBootApplication
public class ServerApp {
public static void main(String[] args) {
new SpringApplicationBuilder(ServerApp.class).run(args);
}
}
View Code
MyController.java
package com.triheart.hystrixserver;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @author 阿遠
* Date: 2018/8/29
* Time: 21:18
*/
@RestController
public class MyController {
@GetMapping("/normalHello")
public String normalHello(HttpServletRequest request) {
return "normal Hello!";
}
@GetMapping("/errorHello")
public String errorHello(HttpServletRequest request) throws Exception{
// 模擬處理讓線程睡眠10秒
Thread.sleep(10000);
return "error Hello";
}
}
View Code
在MyController控制器中,我們提供了一個正常的服務,提供了一個需要等待10秒才有返回的服務。
3.2 創建客戶端並使用Hystrix
使用Hystri來請求Web服務,與原來的方式不太一樣,新建項目hystrixclient,項目的目錄結構如下
添加相關的依賴,pom.xml的代碼清單如下
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.triheart</groupId> <artifactId>hystrixclient</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-core</artifactId> <version>1.5.12</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <version>1.7.25</version> <artifactId>slf4j-log4j12</artifactId> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.2</version> </dependency> </dependencies> </project>View Code
客戶端除了要使用Hystrix外,還會使用HttpClient模塊來訪問Web服務,因此要加入httpclient的依賴。新建命令類
HelloCommand.java
package com.triheart.hystrixclient; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; /** * @author 阿遠 * Date: 2018/8/29 * Time: 21:43 */ public class HelloCommand extends HystrixCommand<String> { private String url; CloseableHttpClient httpclient; public HelloCommand(String url) { // 調用父類的構造器,設置命令組的key,默認用來作為線程池的key super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); // 創建HttpClient客戶端 this.httpclient = HttpClients.createDefault(); this.url = url; } protected String getFallback() { System.out.println("執行 HelloCommand 的回退方法"); return "error"; } protected String run() throws Exception { try { // 調用 GET 方法請求服務 HttpGet httpget = new HttpGet(url); // 得到服務響應 HttpResponse response = httpclient.execute(httpget); // 解析並返回命令執行結果 return EntityUtils.toString(response.getEntity()); } catch (Exception e) { e.printStackTrace(); } return ""; } }View Code
新建運行類,執行HelloCommand命令
HelloMain.java
package com.triheart.hystrixclient; /** * @author 阿遠 * Date: 2018/8/29 * Time: 21:49 */ public class HelloMain { public static void main(String[] args) { // 請求正常的服務 String normalUrl = "http://localhost:8080/normalHello"; HelloCommand command = new HelloCommand(normalUrl); String result = command.execute(); System.out.println("請求正常的服務,結果:" + result); } }View Code
正常情況下,直接調用HttpClient的API來請求Web服務,而此處命令類和運行類則通過執行命令來執行調用的工作。在命令類HelloCommond中,實現了父類的run方法,使用HttpClient調用服務的過程,都放到了該方法中。運行HelloMain類,可以看到,結果與平常調用Web服務無異。
假設我們所調用的Hello服務發生故障,導致無法正常訪問,那麽對於客戶端來說,該如何自保呢?下面將演示調用異常服務的情況
在HelloCommand類中,加入回退方法。在運行類中,調用發生故障的服務
HelloErrorMain.java
package com.triheart.hystrixclient; /** * @author 阿遠 * Date: 2018/8/29 * Time: 21:49 */ public class HelloErrorMain { public static void main(String[] args) { // 請求異常的服務 String normalUrl = "http://localhost:8080/errorHello"; HelloCommand command = new HelloCommand(normalUrl); String result = command.execute(); System.out.println("請求異常的服務,結果:" + result); } }View Code
運行對應的類,可以看到控制臺輸入如下
根據結果可知,回退方法被執行了。本例中調用的errorHello服務,會阻塞10秒才有返回。默認情況下,如果調用Web服務無法再一秒內完成,那麽將觸發回退。
回退更像是一個備胎,當請求的服務無法正常返回時,就調用該備胎來實現。這樣就可以很好的保護客戶端,服務端所提供的服務受網絡等條件的制約,如果有服務真的需要10秒才能返回結果,而客戶端有沒有容錯的機制,後果就是客戶端將一直等待返回,直到網絡超時活著服務有響應,而外界會一直不停地發送請求給客戶端,最終導致的結果就是客戶端因為請求過多而癱瘓。
09 Spring Cloud的集群保護框架Hystrix