學習筆記--Hystrix服務容錯學習筆記【1】
Hystrix具有服務降級、服務熔斷、執行緒和訊號隔離、請求快取、請求合併以及服務監控等功能。
文章目錄
1 簡單的配置:
1.1 引入Spring Cloud Hystrix
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
1.2 添加註解
消費者工程主類使用註解@EnableCircuitBreaker
開啟斷路器。這時候我們的主類的所有註解如下:
@EnableDiscoveryClient
@EnableCircuitBreaker
@SpringBootApplication
P.S.: 可以使用@SpringCloudApplication
註解。
1.3 改造服務消費方式。
增加HelloService類,注入RestTemplate例項,然後在HelloService中使用restTemplate。最後,在HelloService函式上增加@HystrixCommand
註解指定回撥方法。
重新測試服務關閉的情況,返回資訊為我們回撥方法的輸出。
注意Hystrix的預設超時時間為2000ms。當呼叫服務超時也會觸發熔斷。
2 Hystrix的工作流程
-
建立HystrixCommand或HystrixObservableCommand物件
-
命令執行。execute/queue/observe/toObservable
-
結果是否被快取
若當前命令的請求快取被啟用,並且該命令快取命中。那麼快取的結果會立即以Observable物件的形式返回。 -
斷路器是否開啟
若命令結果沒有快取命中的時候,Hystrix在執行命令前需要檢查斷路器是否為開啟狀態。
如果斷路器未開啟狀態,則Hystrix不會執行命令,而是轉接到fallback處理邏輯【第8步】
若斷路器是關閉的,則檢查是否有可用資源來執行命令。【第5步】 -
執行緒池、請求佇列、訊號量是否佔滿。
如果與命令相關的執行緒池和請求佇列,或者訊號量(不適用執行緒池的時候)已經被佔滿,那麼Hystrix也不會執行命令,而是轉到fallback處理邏輯。【第八步】
需要注意的是,這裡Hystrix所判斷的執行緒池並非容器的執行緒池,而是每個依賴服務的專有執行緒池。Hystrix為了保證不會因為某個依賴服務的問題影響到其他依賴服務而採用了“艙壁模式”來隔離每個依賴的服務。 -
HystrixObservableCommand.construct()或HystrixCommand.run()
Hystrix會根據我們編寫的方法來決定採取什麼樣的方式請求依賴服務。
HystrixCommand.run():返回一個單一的結果,或者丟擲異常。
HystrixObservableCommand.construct():返回一個Observable物件來發射多個結果,或通過orError傳送錯誤通知。
如果run或construct方法的持續時間超過子命令設定的超時閾值,則會丟擲超時異常。Hystrix轉接fallback處理邏輯。【第8步】
如果當前命令沒有取消或中斷,則會最終忽略run,construct方法的返回。
如果命令沒有丟擲異常並返回了結果,Hystrix在戶口頁一些日誌並採集監控報告之後將該結果返回。在run的情況下,Hystrix會返回一個Observable,它發射單個結果併產生onCompleted的結果通知;而使用Hystrix會直接返回該方法產生的Observable物件。 -
計算斷路器的健康度
Hystrix會將“成功”,“失敗”,“拒絕”,“超時”等資訊報告給斷路器,而斷路器會維護一組計數器來統計這些資料。
斷路器會使用這些統計資料來決定是否將斷路器開啟,來對某個依賴服務的請求進行“熔斷/短路”,直到恢復期結束。若恢復期結束後,根據統計資料判斷如果還是未達到健康指標,就再次“熔斷/短路”。 -
fallback處理
當命令執行失敗的時候Hystrix會進入fallback的嘗試回退處理,通常該操作也稱為“服務降級”。引起服務降級的操作的可能情況。
- 第4步,當前命令處於“熔斷/短路”狀態,斷路器是開啟的時候。
- 第5步,當前命令的執行緒池、請求佇列或者訊號量被佔滿的時候 。
- 第6步,HystrixObservableCommand.construct()或HystrixCommand.run()丟擲異常的時候。
在服務降級邏輯中,我們需要實現一個通用的響應結果,並且該結果的處理邏輯應當是從快取或是根據一些靜態邏輯來獲取,而不是依賴網路請求獲取。如果一定要在降級邏輯中包含網路請求,那麼該請求也必須被包裝在HystrixObservableCommand或HystrixCommand中,從而形成級聯的降級策略,而最終的降級邏輯一定不是一個依賴網路請求的處理,而是一個能夠穩定返回結果的處理邏輯。
在HystrixObservableCommand和HystrixCommand中實現降級邏輯的不同。
- 當使用HystrixCommand的時候,通過實現HystrixCommand.getFallback()來實現服務降級邏輯。他會返回一個Obse物件,該物件會發射getFallback()的處理結果。
- 當使用HystrixObservableCommand的時候,通過HystrixObservableCommand.resumeWithFallback()實現服務降級邏輯,該方法直接返回一個Observable物件來發射一個或多個降級結果。
如果我們沒有為命令實現降級邏輯或者降級處理邏輯丟擲了異常,Hystrix依然會返回一個Observable物件,但是它不會發射任何處理結果資料,而是通過onError方法通知命令立即中斷請求,並通過onError方法將引起命令失敗的請求傳送給呼叫者。
當降級執行失敗的時候,Hystrix會有如下處理:
- execute():丟擲異常。
- queue():正常返回Future物件,但是當呼叫get()來獲取結果時候會丟擲異常。
- observe():正常返回Observable物件,當訂閱它的時候,將立即通過呼叫訂閱者的onError方法來通知中止請求。
- toObservable():正常返回Obse物件,當訂閱它的時候,將通過呼叫訂閱者的onError方法來通知中止請求。
-
返回成功的響應
當Hystrix命令執行成功後,它會將處理結果直接返回或是以Observable的形式返回。而具體以哪種方法返回取決於之前第2步中所提到的命令的4鐘不同執行方式。- toObservable():返回最原始的Observable,必須通過訂閱它才會真正觸發命令的執行流程。
- observe():在toObservable()產生原始Observable後立即訂閱它,讓命令能夠馬上開始非同步執行,並返回一個Observable物件,當呼叫它的subscribe時,將重新產生結果和通知給訂閱者。
- quque():將toObservable()產生的原始Observable通過toBlocking()方法轉換成BlockingObservable物件,並呼叫它的toFuture()方法返回非同步的Future物件。
- execute():在queue()產生非同步結果Future物件後,通過get()方法阻塞並等待結果的返回。
3 依賴隔離
使用該模式實現執行緒池的隔離,它會為每一個依賴服務建立一個獨立的執行緒池,這樣就算某個依賴服務出現延遲過高的情況,也只是對該依賴服務的呼叫產生影響,而不會拖慢其他的依賴服務。
優點:
- 應用自身不會受不可控的依賴服務影響。即便給依賴服務分配的執行緒池被填滿,也不會影響應用自身的其餘部分。
- 可以有效降低接入新服務的影響。
- 當依賴的服務從失效恢復正常後,它的執行緒池會被清理並且能夠馬上恢復健康的服務。相比之下,容器級別的清理恢復速度要慢得多。
- 當依賴的服務出現配置錯誤的時候,執行緒池會快速反應出此問題。同時,可以在不影響應用功能的情況下通過實時的動態屬性重新整理來進行處理。
- 當依賴的服務因實現機制調整等原因造成其效能出現很大變化的時候,執行緒池的監控指標資訊會反映出這樣的變化。
- 每個專有執行緒池都提供了內建的併發實現,可以利用他為同步的依賴服務構建非同步訪問。
在Hystrix中除了可使用執行緒池之外,還可以使用訊號量來控制單個依賴服務併發度,訊號量的開銷遠比執行緒池的開銷小,但是不能設定超時和實現非同步訪問。所以只有在依賴服務是可靠的情況下才使用訊號量。
HystrixObservableCommand和HystrixCommand中使用訊號量的情況:
- 命令執行:如果將隔離策略引數execution.isolation.strategy設定為SEMAPHORE,Hystrix會使用訊號量替代執行緒池來控制依賴服務的併發。
- 降級邏輯:當Hystrix嘗試降級邏輯時,會在呼叫執行緒中使用訊號量。
訊號量的預設值是10,可以通過動態重新整理配置的方式來控制併發執行緒的數量。訊號量的估算方法:僅訪問記憶體資料的請求一般耗時在1ms以內,效能可以達到5000rps(每秒請求5000次),這樣級別的請求可以將訊號量設為1或者2。
4 Hystrix各介面和註解具體使用方法。
在實現其下的方法之前,要確保引入了相關的依賴,註解, 注入RestTemplate
回顧一下配置:
配置POM檔案
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.demo</groupId>
<artifactId>customer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>customer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RC2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<useUniqueVersions>false</useUniqueVersions>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.demo.customer.CustomerApplication</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
添加註解
@EnableDiscoveryClient
@EnableCircuitBreaker
@SpringBootApplication
public class CustomerApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerApplication.class, args);
}
@Bean
@LoadBalanced
// 建立RestTemplate的spring bean例項,通過@LoadBalanced註解開啟客戶端均衡負載
RestTemplate restTemplate(){
return new RestTemplate();
}
}
Hystrix用來封裝具體的依賴服務呼叫邏輯。可以使用繼承和註解的方式實現。
建議建立兩個工程,一個使用繼承方式, 一個使用註解方式.
4.1 實現請求的同步執行、非同步執行、響應式執行。
同步執行: User u = new UserCommand(restTemplate, 1L).execute()
非同步執行: Future<User> futureUser = new UserCommand(restTemplate, 1L).queue()
非同步執行的時候,可以通過對返回的futureUser呼叫get方法來獲取結果
- 使用繼承方式
注意我們沒有使用@Service 在controller中呼叫的時候例項化一個就好.
public class CustomerService extends HystrixCommand<String> {
private static Logger logger = LoggerFactory.getLogger(CustomerService.class);
private RestTemplate restTemplate;
public CustomerService(RestTemplate restTemplate) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.restTemplate = restTemplate;
}
@Override
protected String run() throws Exception {
logger.info("ooo呼叫UserCommand run");
return restTemplate.getForObject("http://bookservice/UserController/hello", String.class);
}
}
下面的程式碼是呼叫的方法:
// 同步執行 Nikola Zhang 【2018/12/22 14:27】
@RequestMapping(value = "ribbonCustomerSync", method = RequestMethod.GET)
public String ribbonCustomerSync(){
logger.info("使用同步方式呼叫");
return new CustomerService(restTemplate).execute();
}
// 非同步執行 Nikola Zhang 【2018/12/22 14:27】
@RequestMapping(value = "ribbonCustomerAsync", method = RequestMethod.GET)
public String ribbonCustomerAsync() throws ExecutionException, InterruptedException {
Future<String> queue = new CustomerService(restTemplate).queue();
logger.info("使用非同步方式呼叫");
String res = queue.get();
return res;
}
另外還有一種響應式的實現方式:
public class CustomerServiceObservable extends HystrixObservableCommand<String> {
private RestTemplate restTemplate;
private static Logger logger = LoggerFactory.getLogger(CustomerServiceObservable.class);
/**
* 除了以上兩種方法之外還可以使用HystrixCommand提供的toObservable 和observer
* 方法實現響應式執行方式。以上兩種方法均返回observable物件。
* observe()在呼叫的時候會立即執行,當Observable每次被訂閱的時候重放它的行為。
* 而toObservable()則是在所有訂閱則訂閱之後執行
*/
public CustomerServiceObservable(RestTemplate restTemplate) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.restTemplate = restTemplate;
}
@Override
// TODO 使用HystrixObservableCommand實現命令封裝 Nikola Zhang 【2018/12/20 8:18】
protected Observable<String> construct() {
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
try {
if(!subscriber.isUnsubscribed()) {
String res = restTemplate.getForObject("http://bookservice/UserController/hello"
, String.class);
subscriber.onNext(res);
subscriber.onCompleted();
}
} catch (Exception e){
subscriber.onError(e);
}
}
});
}
}
使用Observable實現響應式執行方式的呼叫:
// 響應式執行 Nikola Zhang 【2018/12/22 14:27】
@RequestMapping(value = "ribbonCustomerResCold", method = RequestMethod.GET)
public Observable<String> ribbonCustomerRes() throws ExecutionException, InterruptedException {
logger.info("使用響應式冷執行呼叫");
Observable<String> stringObservable = new CustomerServiceObservable(restTemplate).toObservable();
return stringObservable;
}
// 響應式執行 Nikola Zhang 【2018/12/22 14:27】
@RequestMapping(value = "ribbonCustomerResHot", method = RequestMethod.GET)
public Observable<String> ribbonCustomerResHot() throws ExecutionException, InterruptedException {
logger.info("使用響應式熱執行呼叫");
Observable<String> stringObservable = new CustomerServiceObservable(restTemplate).observe();
return stringObservable;
}
- 使用註解方式
private RestTemplate restTemplate;
@HystrixCommand
public String visitServiceSync(){
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://bookservice/UserController/hello", String.class);
return responseEntity.getBody();
}
@HystrixCommand
// 非同步實現 Nikola Zhang 【2018/12/20 7:48】
public Future<String> visitServiceAsync (){
return new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForObject("http://bookservice/UserController/hello"
, String.class);
}
};
}
public CustomerService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
/**
* 使用@HystrixCommand註解實現響應式命令時
* ,可以通過observableExecutionMode引數來控制是使用observer()
* 還是toObservable()的執行方式。該引數有下面兩種設定方式。
* @HystrixCommand(observableExecutionMode=
* observableExecutionMode.EAGER):EVGER是該引數的模式值,
* 表示使用observer()執行方式。
* @HystrixCommand(observableExecutionMode=
* observableExecutionMode.LAZY):表示使用toObservable()執行方式。
*/
// 使用observer() Nikola Zhang 【2018/12/22 20:47】
// @HystrixCommand(observableExecutionMode = ObservableExecutionMode.EAGER )
// 使用toObservable() Nikola Zhang 【2018/12/22 20:47】
@HystrixCommand(observableExecutionMode = ObservableExecutionMode.LAZY )
public Observable<String> visitServiceObservable() {
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
try {
if(!subscriber.isUnsubscribed()) {
String str = restTemplate.getForObject("http://bookservice/UserController/hello"
, String.class);
subscriber.onNext(str);
subscriber.onCompleted();
}
} catch (Exception e) {
subscriber.onError(e);
}
}
}) ;
}
5 END
以上都是訪問服務的呼叫方法. 但是我們呼叫服務的時候, Hystrix命令總有出錯的情況(出錯, 超時, 執行緒池拒絕, 斷路器熔斷等情況), 之後會繼續介紹服務降級的處理邏輯.