[SpringCloud]Hystrix
服務雪崩
在多個微服務的系統中,假設有ABC三個服務,並且A服務中呼叫了B服務,B服務中呼叫了C服務,組成了一個鏈路,而如果這個鏈路中的某個服務呼叫響應時間長或者不可用,對A的呼叫就會佔用越來越多的系統資源,進而會造成系統崩潰。這就是所謂的雪崩。
Hystrix
所以需要一個能夠對故障和延遲進行隔離和管理,以便於單個依賴關係的失敗,不會拖垮整個系統的工具。Hystrix由此誕生,它是一個用於處理分散式系統的延遲和容錯的開源庫,在分散式系統中,許多依賴會因為各種原因導致呼叫超時或者呼叫失敗,Hystrix能夠保證一個依賴出問題的情況下,不會導致整體服務出現失敗,避免級聯故障。
服務熔斷
服務熔斷可以看做是家庭用的保險絲,當某個服務出現不可用或者響應超時的情況下,暫停對這個服務的呼叫。
服務降級
服務降級是從整個系統的負荷情況出發和考慮的,對於某些負荷比較高的情況,為了預防某個業務出現負荷過載或者響應慢的情況,在其內部暫時捨棄會一些非核心的介面或者資料的請求,而是直接返回一個提前準備好的FallBack錯誤處理資訊,用來保證整個系統的穩定性和可用性。
使用Hystrix實現服務降級
接口占用伺服器資源的情況
這裡先不將Hystrix引入工程,而是測試一下同一個服務中一個介面訪問量激增導致另一個介面訪問變慢的情況。這裡參考尚矽谷的教程,分為一個支付模組,一個訂單模組,案例中是訂單模組呼叫支付模組。
建立一個不帶Hystrix的支付模組
服務中主要是兩個方法,一個是正常的方法,功能很簡單,就是返回一個字串,一個是異常測試方法,也是返回一個字串,不同的是,裡面進行睡眠3秒。如下。
@Service public class PaymentHystrixServiceImpl implements PaymentHystrixService{ @Override public String paymentOk(Integer id) { return "Normal method -- "+id; } @Override public String paymentError(Integer id) { try { TimeUnit.SECONDS.sleep(3); }catch (InterruptedException e){ e.printStackTrace(); } return "Error method -- "+id; } }
Controller層也很簡單,建立兩個介面來呼叫這兩個方法。
@Controller
public class PaymentHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@RequestMapping("/payment/hystrix/ok/{id}")
@ResponseBody
public String paymentOk(@PathVariable("id")Integer id){
return paymentHystrixService.paymentOk(id);
}
@RequestMapping("/payment/hystrix/error/{id}")
@ResponseBody
public String paymentError(@PathVariable("id")Integer id){
return paymentHystrixService.paymentError(id);
}
}
可以看出,對於異常方法,會出現等待情況,而正常方法應該是立即返回的。現在對異常方法進行壓力測試,這裡用到Apache JMeter工具。
然而事實上測試可以發現,儘管只是對異常方法進行壓力測試,但是正常方法呼叫還是會出現一定程度的延遲。這就是因為異常方法佔用了伺服器資源(在這裡是Tomcat執行緒池中的工作執行緒已經被佔用完畢),導致其他請求也變慢了。
服務降級配置
pom中加入Hystrix的依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
主啟動類配置斷路器註解
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentMain8001 {
public static void main(String[] args){
SpringApplication.run(PaymentMain8001.class,args);
}
}
配置Fallback方法
在超時的方法上設定如下,需要注意的是,fallbackMethod欄位對應的方法名和引數列表必須指定的Fallback方法的一致。
@Service
public class PaymentHystrixServiceImpl implements PaymentHystrixService{
@Override
public String paymentOk(Integer id) {
return "Normal method -- ok";
}
@Override
@HystrixCommand(fallbackMethod = "paymentErrorHandler",commandProperties =
{@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000")})
public String paymentError(Integer id) {
try {
TimeUnit.SECONDS.sleep(5);
}catch (InterruptedException e){
e.printStackTrace();
}
return "Error method -- error";
}
public String paymentErrorHandler(Integer id){
return "Fallback method -- error";
}
}
當再次訪問異常介面時,因為Hystrix配置的超時時間是3秒,而方法中睡眠了5秒,所以自然就要執行到Fallback方法。另外值得一提的是,不光是超時,如果是方法裡的拋異常,比如int a = 15/0,也會執行這個方法。這裡測試的是服務端的降級,其實在客戶端也可以配置,並且一般來說,這個配置是放在客戶端的。
客戶端配置降級
由於客戶端需要呼叫支付模組,所以需要加入OpenFeign的依賴,其次就是配置OpenFeign和Eureka。
<!-- openfeign -->
<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>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
之後就是Application.yml的配置了。
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://localhost:7001/eureka
feign:
hystrix:
# 開啟降級保護
enabled: true
接下來寫Service層,但是由上面的支付模組的例子可以看出,Fallback方法和業務方法耦合在同一個類裡面,所以在這裡可以稍微對程式碼進行一下優化。那就是用一個專門的類來設定降級方法,這個選項可以在@OpenFeignClient裡配置。如下:
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentOk(@PathVariable("id")Integer id);
@GetMapping("/payment/hystrix/error/{id}")
public String paymentError(@PathVariable("id")Integer id);
}
PaymentFallbackService就是這個類,這個類是繼承於PaymentHystrixService的。
@Component
public class PaymentFallbackService implements PaymentHystrixService{
@Override
public String paymentOk(Integer id) {
return "Fallback_ok";
}
@Override
public String paymentError(Integer id) {
return "Fallback_error";
}
}
在這之後就是控制層,控制層比較簡單。
@RestController
public class OrderHystrixController {
@Resource
private PaymentHystrixService service;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentOk(@PathVariable("id")Integer id){
return service.paymentOk(id);
}
@GetMapping("/consumer/payment/hystrix/error/{id}")
public String paymentError(@PathVariable("id")Integer id){
return service.paymentError(id);
}
}
再進行測試,可以發現執行的降級方法就是PaymentFallbackService中的方法。並且,就算支付模組是關閉狀態,依舊可以執行。
服務熔斷
服務熔斷就是在達到最大訪問量的時候,直接拒絕訪問,呼叫服務降級的方法並返回友好提示。當服務正常響應的時候,恢復鏈路。
熔斷配置
在Service類中新增熔斷方法,新增熔斷配置的註解。
//服務熔斷
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),//是否開啟斷路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),//請求次數
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "1000"),//時間視窗期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60")//失敗率達到多少後跳閘
})
public String paymentCircuitBreaker(@PathVariable("id")Integer id){
if (id<0){
throw new RuntimeException("id 不能為負數");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"呼叫成功,流水號:"+serialNumber;
}
public String paymentCircuitBreakerFallback(@PathVariable("id")Integer id){
return "id 不能負數,請稍後再試";
}
熔斷器的開啟和關閉,是按照如下步驟來決定的。
1,併發此時是否達到我們指定的閾值
2,錯誤百分比,比如我們配置了60%,那麼如果併發請求中,10次有6次是失敗的,就開啟斷路器
3,上面的條件符合,斷路器改變狀態為open(開啟)
4,這個服務的斷路器開啟,所有請求無法訪問
5,在我們的時間視窗期,期間,嘗試讓一些請求通過(半開狀態),如果請求還是失敗,證明斷路器還是開啟狀態,服務沒有恢復
如果請求成功了,證明服務已經恢復,斷路器狀態變為close關閉狀態
現在來測試該方法,可以發現,在我們瘋狂往介面塞為負數的ID之後,然後停止換成正數之後,發現正數也不正常了,這表示熔斷器起作用了。
熔斷整體流程
1請求進來,首先查詢快取,如果快取有,直接返回,如果快取沒有,-->2
2,檢視斷路器是否開啟,如果開啟的,Hystrix直接將請求轉發到降級返回,然後返回。如果斷路器是關閉的,判斷執行緒池等資源是否已經滿了,如果已經滿了,也會走降級方法。如果資源沒有滿,判斷我們使用的什麼型別的Hystrix,決定呼叫構造方法還是run方法,然後處理請求,然後Hystrix將本次請求的結果資訊彙報給斷路器,因為斷路器此時可能是開啟的(因為斷路器開啟也是可以接收請求的)斷路器收到資訊,判斷是否符合開啟或關閉斷路器的條件,如果本次請求處理失敗,又會進入降級方法
如果處理成功,判斷處理是否超時,如果超時了,也進入降級方法
最後,沒有超時,則本次請求處理成功,將結果返回給controller層。
Hystrix服務監控--HystrixDashBoard
還是老套路,引入依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
yml中配置埠即可。然後主啟動類需要加上@EnableHystrixDashboard,這裡需要注意一點就是每一個被監控的服務都必須加入actuator依賴。
<!--監控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
此外,還需要在被監控服務中加入如下程式碼,原因註釋中有所說明。
/**
* 為了服務監控來配置,與服務容器本身無關,而是SpringCloud升級後的坑。
* ServletRegistrationBean因為Springboot的預設路徑不是/hystrix.stream
* 所以只要在自己的專案中配置下面的Servlet就行。
* */
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet servlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(servlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
然後啟動被監控服務,將其url加入到HystrixDashboard中。比如這裡是8001埠,那麼url就是http://localhost:8001/hystrix.stream。開啟監控。
曲線表示流量變化。其他元素意義如下圖所示。
測試HystrixDashboard
還是跟之前一樣,瘋狂網介面塞負數,然後觀察Dashboard的變化。可以發現,到了一定程度,介面就有綠色close狀態變為紅色open狀態。