1.概述
1.分佈式系統面臨的問題
在複雜的分佈式架構系統裏,服務之間的相互調用無法避免出現失敗的情況。如果一個服務的調用鏈路比較長的時候,這種調用失敗的概率會更高一些,比如A調用B,B調用C,C調用D這種長鏈路調用,又或者一個服務的調用需要依賴多個服務協作完成,這種調用失敗的概率也會高一些,比如A需要調用B,C,D服務,只有B,C,D服務都走完,A服務才能完成。
扇出:當前模塊直接調用下級模塊的個數。
如果扇出的鏈路上某個服務調用響應時間過長或不可用,對當前模塊的調用就會佔用越來越多的資源,甚至造成系統崩潰。這就是所謂的系統雪崩效應。
一個高流量的應用,單一的後端依賴可能導致所有服務器上的所有資源在很快時間內飽和,更糟糕的是,應用程序還可能導致服務之間的延遲增加,隊列,線程等資源緊張,進而導致整個系統發生更多的故障。
爲了避免出現這樣的情況,就需要對故障和延遲進行隔離,避免因爲某一個服務的問題,拖累其他服務的正常運行。
當一個模塊出現問題後,如果這個模塊還在接受請求,而且還調用了其他模塊,這樣就會導致級聯故障,導致系統雪崩效應。
2.Hystrix是什麼
Hystrix是一個用於處理分佈式系統延遲和容錯的開源庫,在分佈式系統裏,許多依賴不可避免出現調用失敗的情況,常見的超時、程序異常報錯等,Hystrix能夠保證在一個依賴出現問題的情況下,不會導致整體服務失敗,避免了級聯故障,從而提高分佈式系統的彈性。
“斷路器”本身是一種開關裝置,當某個服務發生故障的時候之後,通過斷路器的故障監控(類似於保險絲熔斷),向調用方返回一個符合預期的、可以處理的備選響應(FallBack),而不是長時間的等待或者拋出調用方無法處理的異常,這樣就保證了服務調用方的線程不會長時間、不必要地佔用,從而避免了故障在分佈式系統中的蔓延,乃至雪崩。
3.Hystrix能做什麼
服務降級、服務熔斷、接近實時的監控。
4.Hystrix官網
https://github.com/Netflix/Hystrix/wiki/How-To-Use
從https://github.com/Netflix/Hystrix的Hystrix Status的信息可知,Hystrix目前不再進行開發了,從github的更新來看,已經好久沒有更新了,Hystrix推薦使用Resilience4j來替代,不過,國內使用Resilience4j的大廠比較少,更多的大廠使用的是Spring Cloud Alibaba Sentinel來實現熔斷與限流,又或者在現有的開源框架上進行包裝,自己實現功能。
雖然Hystrix不再更新,但是Hystrix的設計理念值得學習,也爲後面學習Spring Cloud Alibaba Sentinel做好鋪墊。
2.Hystrix重要概念
1.服務降級(fallback)
當程序出現異常、超時、服務熔斷、線程池、信號量已滿的情況,會發生服務降級,此時服務會返回一個友好的提示,請稍後再試,不讓客戶端一直等待並立刻返回一個有好的提示。
2.服務熔斷(break)
當服務的請求量達到一定限度的時候,可以發生服務熔斷,類似於家裏的保險絲在大電流的時候,會觸發熔斷,此時服務直接不可訪問,後續可以走服務降級的方式立刻給客戶端返回響應。
3.服務限流(flowlimit)
常用在某一時刻的高併發請求,比如秒殺階段,爲了不把服務打死,會將某些請求進行限流,比如放進隊列,讓其暫時等待。
3.Hystrix案例
1.構建項目
先把cloud-eureka-server7001和cloud-eureka-server7002改成單機版,後序啓動會快一些,修改defaultZone的值爲指向自己服務的地址,而不是互相註冊的方式。
新建cloud-provider-hystrix-payment8001模塊,修改pom.xml,這裏加入spring-cloud-starter-netflix-hystrix依賴。
<?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>org.example</groupId>
<artifactId>cloud-provider-hystrix-payment8001</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
添加application.yml配置文件。
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
創建主啓動類。
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
創建業務類(Service和Controller)。
package com.atguigu.springcloud.service;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
public String paymentInfo_OK(Integer id) {
return "線程池:" + Thread.currentThread().getName() + "\tpaymentInfo_OK, id=" + id;
}
public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
int time = 3000;
Thread.sleep(time);
return "線程池:" + Thread.currentThread().getName() + "\tpaymentInfo_TimeOut, id=" + id + "\t耗時:" + time;
}
}
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_OK(id);
log.info("result={}", result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
String result = paymentService.paymentInfo_TimeOut(id);
log.info("result={}", result);
return result;
}
}
先啓動cloud-eureka-server7001模塊,再啓動cloud-provider-hystrix-payment8001模塊,通過瀏覽器訪問http://localhost:8001/payment/hystrix/ok/1和http://localhost:8001/payment/hystrix/timeout/1查看效果,此時兩個請求都可以正常訪問,只是timeout那個請求慢一點。下面,我們就基於這兩個請求進行擴展,演示Hystrix裏的功能。
2.高併發測試
使用高併發測試工具Apache Jmeter進行測試。
在測試計劃上右鍵→添加→線程(用戶)→線程組,添加一個線程組,命名爲Spring Cloud,線程數爲200,Ramp-Up時間爲1,循環次數爲100,其餘參數保持默認即可,保存線程組。在Spring Cloud線程組上右鍵→添加→取樣器→HTTP請求,命名cloud-provider-hystrix-payment8001,服務器名稱或IP爲localhost,端口號爲8001,HTTP請求爲GET請求,路徑爲http://localhost:8001/payment/hystrix/timeout/1,點擊保存,先試試這個超時的請求。點擊菜單欄裏的綠色箭頭髮起請求。此時,再通過瀏覽器訪問http://localhost:8001/payment/hystrix/ok/1會發現,也被拖慢了。
新建cloud-consumer-feign-hystrix-order80模塊,修改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">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-hystrix-order80</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
添加application.yml配置文件。
server:
port: 80
# 這裏只把feign做客戶端用,不註冊進eureka
eureka:
client:
# 表示是否將自己註冊進EurekaServer默認爲true
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
spring:
application:
name: cloud-consumer-feign-hystrix-order
添加主啓動類。
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class, args);
}
}
添加業務類(Service和Controller)。
package com.atguigu.springcloud.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException;
}
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.service.PaymentHystrixService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class OrderHystrixController {
@Resource
PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_OK(id);
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
return paymentHystrixService.paymentInfo_TimeOut(id);
}
}
先後啓動cloud-eureka-server7001,cloud-provider-hystrix-payment8001,cloud-consumer-feign-hystrix-order80模塊。
瀏覽器訪問http://localhost:8001/payment/hystrix/ok/1、http://localhost:8001/payment/hystrix/timeout/1、http://localhost/consumer/payment/hystrix/ok/1都可以正常訪問,當訪問http://localhost/consumer/payment/hystrix/timeout/1提示超時了,這是因爲Feign的原因,Feign默認連接超時爲1秒,默認讀取資源超時是1秒,這個暫且先不管,當然,也可以根據前一篇OpenFeign的筆記,修改超時時間。
這時候啓動Apache JMeter,向http://localhost:8001/payment/hystrix/timeout/1請求發送高併發訪問,通過瀏覽器訪問http://localhost/consumer/payment/hystrix/ok/1,會發現直接超時了,此時模擬的情況就是:大量的請求打到生產者上,此時,通過Feign再來一個外部請求,就會出現超時,在不跑壓測的時候,這個請求http://localhost/consumer/payment/hystrix/ok/1的響應時間還是很快速的。
原因:8001服務接口被大量請求佔用,處理速度跟不上,Tomcat的工作線程已經佔滿了,後來到的線程只能等待,這就導致瀏覽器訪問本該立刻返回的請求http://localhost/consumer/payment/hystrix/ok/1出現了超時的現象。
真是因爲有這種故障或者不佳的情況出現,我們才需要降級、容錯、限流等技術來處理這個問題。
- 生產者8001超時了,消費者80不能一直等待,必須有服務降級
- 生產者8001宕機了,消費者80不能一直等待,必須有服務降級
- 生產者8001正常,消費者80自身故障或要求等待時間小於生產者處理時間,消費者80自己處理服務降級
3.服務降級
先從服務生產者端改造,找到paymentInfo_TimeOut()方法,因爲這個方法的執行之間有點長,我們給它加一個標準,當方法執行不滿足標準要求時,就讓目標方法快速失敗,繼而去執行服務降級的方法,不再繼續等待目標方法的返回。
package com.atguigu.springcloud.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
public String paymentInfo_OK(Integer id) {
return "線程池:" + Thread.currentThread().getName() + "\tpaymentInfo_OK, id=" + id;
}
// @HystrixCommand註解表示,當目標方法不滿足commandProperties指定的參數時,終止當前方法,繼而執行fallbackMethod指定的方法
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
int time = 5000;
// int a = 1 / 0;// 當程序報錯,也會觸發服務降級方法
Thread.sleep(time);
return "線程池:" + Thread.currentThread().getName() + "\tpaymentInfo_TimeOut, id=" + id + "\t耗時:" + time;
}
// 服務降級的方法
// 需要注意的是,服務降級方法要和目標方法的方法簽名保持一致,即參數和返回值要一致,否則會提示找不到服務降級方法
public String paymentInfo_TimeOutHandler(Integer id) {
return "線程池:" + Thread.currentThread().getName() + "\tpaymentInfo_TimeOutHandler, id=" + id + "\t運行了服務降級方法";
}
}
在主啓動類上添加@EnableCircuitBreaker註解開啓斷路器功能,啓動EurekaMain7001和PaymentHystrixMain8001模塊,瀏覽器訪問http://localhost:8001/payment/hystrix/timeout/1進行測試,方法體業務耗時5秒鐘,調用方最多就能等3秒鐘,當時間超過3秒,還沒有返回,直接執行了paymentInfo_TimeOutHandler()方法,在瀏覽器端,我們可以看到具體輸出。
上面,我們把服務生產者的服務降級做了處理,下面我們對服務消費者做處理,需要注意的是:服務降級既可以放在生產者,也可以放在消費者,更多的情況是放在消費者端。
在主啓動類上添加@EnableHystrix註解。業務類,仿照服務生產者端的寫法,做同樣的修改。
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class OrderHystrixController {
@Resource
PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_OK(id);
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
// @HystrixCommand註解表示,當目標方法不滿足commandProperties指定的參數時,終止當前方法,繼而執行fallbackMethod指定的方法
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMehtod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
// int a = 1 / 0; // 當程序報錯,也會觸發服務降級方法
return paymentHystrixService.paymentInfo_TimeOut(id);
}
// 服務降級的方法
// 需要注意的是,服務降級方法要和目標方法的方法簽名保持一致,即參數和返回值要一致,否則會提示找不到服務降級方法
public String paymentTimeOutFallbackMehtod(@PathVariable("id") Integer id) {
return "消費者調用生產者繁忙,請稍等後再試";
}
}
此時,服務生產者端,timeout()方法服務運行時間是5s,加了服務降級策略後,timeout()方法如果在3s內不能執行完,就執行服務降級方法,服務消費者端的要求更苛刻,消費者最多等2.5s,在2.5s不能返回,就執行降級方法。通過瀏覽器訪問http://localhost/consumer/payment/hystrix/timeout/1,在Network裏觀察,發現2秒多點就觸發了服務降級方法,這是因爲,我們沒有設置Ribbon的默認超時時間(建立連接超時1s,讀取資源超時1s)是2s,小於HystrixProperty設置的2.5秒,先觸發了Ribbon的超時,代碼報錯,然後觸發了降級方法。我們修改application.yml配置文件,修改Ribbon超時時間。
# 設置feign客戶端超時時間,OpenFeign默認支持Ribbon
ribbon:
ReadTimeout: 5000 # 建立連接後,讀取資源所用時間
ConnectTimeout: 5000 # 建立連接所用時間
此時,再發送http://localhost/consumer/payment/hystrix/timeout/1請求,通過Network查看時間,發現返回時間是2.5秒多點,此時是HystrixProperty設置的2.5秒起作用了。並且,瀏覽器輸出的信息是客戶端中降級方法的輸出。
再來演示一個情況,將客戶端HystrixProperty的時間改成3.5s,一般也不會這麼設置,因爲客戶端的3.5s都大於服務端的3s了,這裏僅僅做一個測試使用,通過了瀏覽器訪問http://localhost/consumer/payment/hystrix/timeout/1,觀察Network的時間,3s就返回了,而且瀏覽器展現的內容,是服務端降級方法的輸出。
這個地方的超時時間有點亂,我感覺可以這麼理解,看這幾個超時時間,以超時時間最小的爲標準,只要達到最小的超時時間,就會引起服務降級,服務降級的引起是由最小的超時時間決定的。
服務降級加在生產者的時候,直接調用服務生產者,是生產者Controller中HystrixProperty的3s觸發降級。
服務降級加在消費者的時候,直接調用服務消費者:
- 如果沒有修改過Ribbon的默認超時,並且消費者調用超過默認的2s,此時,觸發服務降級的是min(Ribbon的2s,消費者HystrixProperty指定的時間),誰的值小,就是由誰引發的服務降級。
- 如果修改過Ribbon的默認超時,這裏假設修改的值非常大,排除Ribbon調用超時引起的服務降級問題,那麼,服務降級的觸發條件是消費者HystrixProperty指定的時間,如果此時服務生產者也在HystrixProperty中指定了時間,那麼以min(消費者HystrixProperty指定的時間,生產者HystrixProperty指定的時間)作爲降級時間,哪個值小,就是由哪個觸發的降級,從而走哪個降級方法。
回頭看一下配置過的服務降級方法,如果有10個方法需要服務降級,那麼就需要額外配置10個服務降級方法,有沒有辦法用一個通用的方法處理服務降級,其餘的再做自定義配置呢?服務降級方法和業務方法放在一起,導致業務類裏混亂不堪,是否可以將降級方法拿出來呢?
添加一個全局的服務降級方法,在class上添加@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod"),表明這個類中的所有帶@HystrixCommand的方法,如果方法上沒有特殊指明,那麼,方法發生降級的時候,就走這個paymentGlobalFallbackMethod()方法。
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
// 指定全局降級方法,對帶了@HystrixCommand註解的方法起作用
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystrixController {
@Resource
PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_OK(id);
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
// @HystrixCommand註解表示,當目標方法不滿足commandProperties指定的參數時,終止當前方法,繼而執行fallbackMethod指定的方法
// @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMehtod", commandProperties = {
// @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3500")
// })
// 帶了@HystrixCommand註解表示,走Hystrix的服務降級策略,而且此時沒有指明自定義的callback,就走默認的callback
@HystrixCommand(commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2500")})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
// int a = 1 / 0; // 當程序報錯,也會觸發服務降級方法
return paymentHystrixService.paymentInfo_TimeOut(id);
}
// 服務降級的方法
// 需要注意的是,服務降級方法要和目標方法的方法簽名保持一致,即參數和返回值要一致,否則會提示找不到服務降級方法
public String paymentTimeOutFallbackMehtod(@PathVariable("id") Integer id) {
return "消費者調用生產者繁忙,請稍等後再試";
}
// 全局服務降級方法,@HystrixCommand中,如果沒有指定fallbackMethod的,就走全局服務降級方法
public String paymentGlobalFallbackMethod() {
return "全局服務降級方法,請稍後再試";
}
}
瀏覽器訪問http://localhost/consumer/payment/hystrix/timeout/1,依舊可以按照我們指定的超時規則進行服務降級,服務降級時,執行的方法是paymentGlobalFallbackMethod()。
爲了實現解耦,可以藉助@FeignClient註解中的fallback參數。新建一個PaymentFallbackService.java的類,實現PaymentHystrixService接口,重寫接口裏的方法,這裏重寫的兩個方法,就是用於服務降級執行的方法,最後,將PaymentFallbackService類名,寫到@FeignClient註解中的fallback參數上即可完成業務方法與降級方法解耦的效果。
package com.atguigu.springcloud.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException;
}
package com.atguigu.springcloud.service;
import org.springframework.stereotype.Component;
/**
* 當PaymentHystrixService裏的調用出錯了,會執行這裏對應的方法,從而把業務方法和降級方法解耦
*/
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "paymentInfo_OK()方法對應的降級方法";
}
@Override
public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
return "paymentInfo_TimeOut()方法對應的降級方法";
}
}
修改application.yml,開啓Hystrix,默認是false的,這個地方和前面的超時配置好像有點衝突,如果開啓了Hystrix,超時哪塊不起作用,不知道什麼原因。
feign:
hystrix:
enabled: true
先通過瀏覽器訪問http://localhost/consumer/payment/hystrix/ok/1,可以通過服務生產者,拿到生產者返回的值,此時,將生產者關掉,模擬生產者宕機的情況,再次訪問http://localhost/consumer/payment/hystrix/ok/1,會看到它執行了PaymentFallbackService裏的paymentInfo_OK()方法,現在服務降級方法和業務方法就分離開了。
4.服務熔斷
熔斷機制是應對雪崩效應的中微服務鏈路保護機制,當扇出立案率的某個微服務出錯或者響應時間太長時,會進行服務降級,進而熔斷該結點微服務的調用,快速返回錯誤的響應信息。當檢測到該結點微服務調用響應正常後,恢復調用鏈路。
在Spring Cloud框架裏,熔斷機制通過Hystrix實現,Hystrix會監控微服務之間的調用情況,當失敗的調用達到一定閾值,會觸發熔斷機制(默認是5秒內20次失敗),熔斷機制的註解是@HystrixCommand。
服務熔斷相關內容擴展:https://martinfowler.com/bliki/CircuitBreaker.html。
在cloud-provider-hystrix-payment8001的PaymentService裏添加方法來演示服務熔斷。
// 服務熔斷,這裏配置的HystrixProperty可以在https://github.com/Netflix/Hystrix/wiki/Configuration查看,也可以查看HystrixCommandProperties類
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否開啓斷路器
// 一個rolling window內請求的次數,當請求次數達到10,才計算失敗率,從而判斷是否滿足斷路的條件
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
// 觸發斷路後的10s都會直接失敗,超過10s後,嘗試一次恢復
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
// 失敗率達到60%的時候,進行斷路
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("id不能爲負數");
}
String uuid = UUID.randomUUID().toString();
return Thread.currentThread().getName() + "\t調用成功,UUID=" + uuid;
}
// 服務降級方法
public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id) {
return "id不能爲負數,請稍後再試,id=" + id;
}
有了Service,也要有Controller,才能實現調用,在cloud-provider-hystrix-payment8001的PaymentController中添加方法。
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("result={}", result);
return result;
}
啓動EurekaMain7001和PaymentHystrixMain8001模塊。瀏覽器發送http://localhost:8001/payment/circuit/1請求,可以拿到正確的返回值,嘗試發送大量的http://localhost:8001/payment/circuit/-1請求,再次發送一個http://localhost:8001/payment/circuit/1請求,對於這個正常的請求,依舊走了服務降級的方法,說明此時斷路器是斷開的,還沒有恢復,再次發送幾個正確的請求後,熔斷器恢復,可以處理正常請求。
當發生熔斷的時候,會記錄下熔斷的時間,根據這裏的配置,在後續的10s內,請求都會走服務降級的方法,10s過後,嘗試進行服務調用,如果能調通,恢復調用鏈路,如果不能調通,繼續保持熔斷,並重新記錄時間。
熔斷分3個類型:熔斷打開、熔斷半開、熔斷關閉。
- 熔斷打開:Hystrix發生熔斷,新的請求不在進行服務調用,而是直接運行服務降級方法,記錄下發生熔斷的時間,當熔斷持續時間到達設置的sleepWindowInMilliseconds時,進入熔斷半開
- 熔斷半開:部分請求進行服務調用,如果調用成功,熔斷關閉,如果調用失敗,繼續保持熔斷打開狀態,重新記錄時間
- 熔斷關閉:正常進行服務調用
5.服務限流
在後續的Alibaba的Sentinel中說明。
4.Hystrix工作流程
來到Hystrix的頁面,點擊How it Works。
這裏插播一個小問題,如果發現GitHub上的圖片不顯示了,那麼可以嘗試這個方法能不能解決。
訪問https://www.ipaddress.com/,將顯示圖片的域名帖進去,這裏我的是raw.Githubusercontent.com,會得到一個IP地址。把這個IP地址和域名寫到本地hosts裏,再次刷新頁面,就可以顯示圖片了,原因好像是DNS污染或DNS存在緩存。
如果還是不能訪問的小夥伴,就先看這個圖吧,我貼上來。整個流程一共是9個步驟:
- 創建HystrixCommand或HystrixObservableCommand對象
- 執行Command命令
- 檢查是否有可用緩存,如果有,直接返回cache的內容,如果沒有,走第4步
- 檢查熔斷器的開閉,如果開啓,運行服務降級方法,走第8步,如果沒有開啓,走第5步
- 檢查線程池、隊列、信號量是否充足,如果不充足,運行服務降級方法,走第8步,如果充足,走第6步
- 執行HystrixObservableCommand的construct()方法或者HystrixCommand的run()方法
- 根據執行的結果,判斷是否執行成功,是否出現超時,並反饋給迴路中的健康判斷功能,從而影響斷路器的開閉
- 熔斷器斷開、資源不足、執行失敗、執行超時等情況出現,走服務降級方法獲取返回值
- 一切正常,斷路器爲關閉狀態,正常調用服務拿到返回值
5.服務監控Hystrix Dashboard
除了隔離依賴服務的調用以外,Hystrix還提供了準實時的調用監控(Hystrix Dashboard),Hystrix會持續記錄所有通過Hystrix發起的請求的執行信息,並以統計報表和圖形的形式展示給用戶,包括每秒鐘執行請求多少成功,多少失敗等。Netflix通過hystrix-metrics-event-stream項目實現了對以上指標的監控 。Spring Cloud也提供了Hystrix Dashboard的整合,對監控內容轉換成可視化界面。
新建cloud-consumer-hystrix-dashboard9001模塊,修改pom.xml,引入了spring-cloud-starter-netflix-hystrix-dashboard座標。
<?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">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-hystrix-dashboard9001</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
添加application.yml配置文件,指明端口號。
server:
port: 9001
添加主啓動類,添加@EnableHystrixDashboard和@EnableCircuitBreaker註解,添加一個bean,用於指定監控路徑,否則監控不到內容。
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
要想實現監控的功能,需要在pom.xml中有spring-boot-starter-actuator的座標。啓動HystrixDashboardMain9001模塊,瀏覽器訪問http://localhost:9001/hystrix可以看到界面。
爲了演示監控的效果,即9001模塊監控8001模塊的運行情況,修改cloud-provider-hystrix-payment8001模塊,檢查8001模塊的pom.xml裏,是否有spring-boot-starter-actuator座標,如果沒有添加上,這個必須要有。
修改PaymentHystrixMain8001的主啓動類,注入一個Bean,用於指定監控路徑,否則會報Unable to connect Command Metric Stream的錯誤,注意修改的是8001的主啓動類。
package com.atguigu.springcloud;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
/**
* 此配置是爲了服務監控而配置,與服務容錯本身無關,Spring Cloud升級後的坑
* 因爲SpringBoot裏ServletRegistrationBean默認路徑不是 “/hystrix.stream"
* 所以要在自己的項目裏配置上下的servlet纔可以
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet() ;
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
修改完成後,啓動Eureka7001註冊中心,Provider8001服務生產者,Dashboard9001服務監控,在http://localhost:9001/hystrix頁面的輸入框中,輸入http://localhost:8001/hystrix.stream,即要監控的服務是哪一個,Delay默認2000即可,Title隨便起一個名字。點擊“Monitor Stream”開始監控,新開一個窗口,多次訪問http://localhost:8001/payment/circuit/1,在Circuit標籤下可以看到有一個折線和一個圓點,折線是記錄最近2分鐘內訪問流量的相對變化,訪問流量大的時候,折線上升,圓點變大,同時,這個圓點還會根據健康狀態改變顏色,健康度從綠色→黃色→橙色→紅色依次遞減,還有一個Circuit,表示是否熔斷,如果是Closed表示未熔斷,如果是Open表示熔斷了。
在監控頁面的右上角,有7種顏色,從左到右分別對應:請求成功數量、請求熔斷數量、錯誤請求數量,超時請求數量,線程池拒絕請求數量,請求失敗數量,最近10秒的錯誤率。
更多Dashboard的內容參考:https://github.com/Netflix-Skunkworks/hystrix-dashboard/wiki