前言
分布式系統面臨的問題-----服務雪崩
多個微服務之間調用的時候,假設微服務A調用微服務B和微服務C,微服務B和微服務C又調用其它的微服務,這就是所謂的“扇出”。如果扇出的鍊路上某個微服務的調用響應時間過長或者不可用,對微服務A的調用就會占用越來越多的系統資源,進而引起系統崩潰,所謂的“雪崩效應”。是以需要有一種熔斷機制來保護微服務的鍊路。
熔斷機制概述
熔斷機制是應對雪崩效應的一種微服務鍊路保護機制。當扇對外連結路的某個微服務出錯不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的調用,快速傳回錯誤的響應資訊。當檢測到該節點微服務調用響應正常後,恢複調用鍊路。
在Spring Cloud架構裡,熔斷機制通過Hystrix實作。Hystrix會監控微服務間調用的狀況,當失敗的調用到一定門檻值,預設是5秒内20次調用失敗,就會啟動熔斷機制。熔斷機制的注解@HystrixCommand.
涉及到斷路器的三個重要參數:快照時間窗、請求總數閥值、錯誤百分比閥值。
- 快照時間窗:斷路器确定是否打開需要統計一些請求和錯誤資料,而統計的時間範圍就是快照時間窗,預設為最近的10秒。
- 請求總數閥值:在快照時間窗内,必須滿足請求總數閥值才有資格熔斷。預設為20,意味着在10秒内,如果該hystrix指令的調用次數不足20次,即使所有的請求都逾時或其他原因失敗,斷路器都不會打開。
- 錯誤百分比閥值:當請求總數在快照時間窗内超過了閥值,比如發生了30次調用,如果在這30次調用中,有15次發生了逾時異常,也就是超過50%的錯誤百分比,在預設設定50%閥值情況下,這時候就會将斷路器打開。
Hystrix介紹
什麼是Hystrix
Hystrix是一個用于處理分布式系統的延遲和容錯的開源庫,在分布式系統裡,許多依賴不可避免的會調用失敗,比如逾時、異常等,Hystrix能夠保證在一個依賴出問題的情況下,不會導緻整體服務失敗,避免級聯故障,以提高分布式系統的彈性。“斷路器”本身是一種開關裝置,當某個服務單元發生故障之後,通過斷路器的故障監控(類似熔斷保險絲),向調用方傳回一個符合預期的、可處理的備選響應(FallBack),而不是長時間的等待或者抛出調用方無法處理的異常,這樣就保證了服務調用方的線程不會被長時間、不必要地占用,進而避免了故障在分布式系統中的蔓延,乃至雪崩。
Hystrix的設計目的如下:
- 通過第三方用戶端庫通路依賴項(通常通過網絡),進而保護和控制延遲和故障。
- 停止複雜分布式系統中的級聯故障。
- 快速故障并快速恢複。
- 如果可能,後退并優雅地降級。
- 實作近實時監控、警報和操作控制。
複雜分布式體系結構中的應用程式有幾十個依賴項,每一個依賴項都不可避免地會在某一點發生故障。如果主機應用程式沒有與這些外部故障隔離開來,那麼它就有可能被這些外部故障帶走。
例如,對于一個依賴于30個服務的應用程式,其中每個服務都有99.99%的正常運作時間,下面是我們可以期望的:
99.9930 = 99.7% uptime
0.3% of 1 billion requests = 3,000,000 failures
即使所有依賴項都有很好的正常運作時間,每月也會有2小時以上的停機時間。而實際情況通常可能會更糟。
即使所有依賴項都表現良好,如果我們不設計整個系統的恢複能力,數十項服務中每項服務的總停機率甚至為0.01%,也可能導緻每月數小時的停機。
當一切正常時,請求流可以如下所示:
當許多後端系統中的一個變得潛在時,它會阻止整個使用者請求:
在高流量的情況下,單個後端依賴性變得潛在可能會導緻所有伺服器上的所有資源都處于飽和狀态。
應用程式中通過網絡或進入用戶端庫的每一點都可能導緻網絡請求,這是潛在故障的根源。比失敗更糟糕的是,這些應用程式還可能導緻服務之間的延遲增加
備份隊列、線程和其他系統資源,進而在整個系統中造成更多級聯故障。
當通過第三方用戶端執行網絡通路時,這些問題會更加嚴重——每個服務都像一個“黑匣子”,實作細節是隐藏的,可以随時更改,每個用戶端庫的網絡或資源配置都不同,通常很難監控和更改。
更糟糕的是傳遞依賴關系,它們在應用程式未明确調用的情況下執行可能昂貴或容易出錯的網絡調用。
網絡連接配接失敗或降級。服務和伺服器出現故障或速度變慢。新庫或服務部署會改變行為或性能特征。用戶端庫存在錯誤。
所有這些都代表了需要隔離和管理的故障和延遲,以使單個故障依賴項不會影響整個應用程式或系統。
Hystrix的設計原則是什麼?
Hystrix通過以下方式工作:
- 防止任何單個依賴項用完所有容器(如Tomcat)使用者線程。
- 減少負載并快速失敗,而不是排隊。
- 在可行的情況下提供回退,以保護使用者不發生故障。
- 使用隔離技術(如隔闆、泳道和斷路器模式)來限制任何單一依賴的影響。
- 通過近實時名額、監控和警報優化發現時間
- 通過配置更改的低延遲傳播和Hystrix大多數方面的動态屬性更改支援,優化恢複時間,這允許我們使用低延遲回報環路進行實時操作修改。
- 保護整個依賴關系用戶端執行過程中的故障,而不僅僅是網絡流量中的故障。
Hystrix如何實作目标?
Hystrix通過以下方式做到這一點:
- 将對外部系統(或“依賴項”)的所有調用包裝在HystrixCommand或HystrixWatableCommand對象中,這些對象通常在單獨的線程中執行(這是指令模式的一個示例)。
- 逾時調用所需時間超過我們定義的門檻值。有一個預設值,但對于大多數依賴項,我們可以通過“屬性”自定義設定逾時,使其略高于每個依賴項的99.5%性能。
- 為每個依賴項維護一個小的線程池(或信号量),如果它變滿,則發往該依賴項的請求将立即被拒絕,而不是排隊。
- 衡量成功、失敗(用戶端抛出的異常)、逾時和線程拒絕。
- 如果服務的錯誤百分比超過門檻值,則手動或自動跳閘斷路器以在一段時間内停止對特定服務的所有請求。
- 當請求失敗、被拒絕、逾時或短路時,執行回退邏輯。
- 近實時監控名額和配置更改。
當我們在使用Hystix包裝每個底層依賴項時,上圖中所示的架構會發生變化,以類似下圖。每個依賴項彼此隔離,在延遲發生時會飽和的資源受到限制,并包含在fallack邏輯中,該邏輯決定在依賴項中發生任何類型的故障時做出什麼響應:
下面通過具體測試對Hytrix的降級熔斷實作進行一個簡單的模拟。
項目準備
本次實踐模拟因為采用的服務注冊中心為Eureka,并通過OpenFeign實作用戶端對服務端的調用,以及引用Hytrix來實作服務的降級熔斷效果,是以引入的項目依賴主要如下,服務提供端的依賴不用引入OpenFeign:
<?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>SpringCloudAlibaba</artifactId>
<groupId>com.yy</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<description>hystrix學習測試消費端-80端口</description>
<artifactId>cloud-hystrix-consumer-80</artifactId>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.yy</groupId>
<artifactId>cloud-common</artifactId>
<version>${myproject.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
然後配置基本的yml檔案資訊,指明eureka服務位址以及服務名稱,是以在服務提供端8005上的配置資訊為:
server:
port: 8005
spring:
application:
name: cloud-hystrix-provider
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:7001/eureka
這裡的eureka服務端為7001,不做詳細建構展示。用戶端作為調用服務的一端(80端口)與上面配置相似。然後在服務提供端編寫量兩個業務類一個正常擷取資料,一個延時擷取。
@Service
public class TestService {
public String ok(Integer id) {
return "時間為:" + DateUtil.format(LocalDateTime.now(), "yyyy年MM月dd日 HH:mm:ss") + "時,線程池:" + Thread.currentThread().getName() + "通路的id為:" + id;
}
public String wait(Integer id) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "時間為:" + DateUtil.format(LocalDateTime.now(), "yyyy年MM月dd日 HH:mm:ss") + "時,等待後的線程池:" + Thread.currentThread().getName() + "通路的id為:" + id + "順利執行";
}
}
後編寫對應的controller api業務實作接口,并且在80的用戶端上利用openfeign進行業務調用。
這樣當我們正常調用服務時,資料擷取正常。
但是當微服務在遇到高并發時,就會出現服務延遲響應的時候,或者服務異常出錯,這時就像我們模拟的等待業務,當這個業務被調用時就會出現逾時異常,直接在頁面中抛出錯誤頁面,使用者體驗極差。
這時候我們就需要使用服務降級處理,讓異常資訊能夠被背景優雅的處理。這裡利用Hytrix可以實作兩種方法的處理:
- 服務提供方處理
- 消費端處理
下面接着看~
Hytrix實作服務降級
單側服務端處理
首先需要在啟動類上添加@EnableHystrix注解來開啟Hyrtix,使其能在項目中生效。
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableHystrix
public class HystrixProviderApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixProviderApplication.class, args);
}
然後在需要設定服務降級的業務方法上添加降級處理方法,一方面處理可能出現的延時異常,另一方面即使出現業務異常也能傳回我們自定義的方法去處理。
/**
* 出現系統逾時或異常過後做服務降級,單側服務端(提供方)降級
*
* @param id
* @return
*/
@HystrixCommand(
fallbackMethod = "fallBackHandle",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
value = "5000")}) //設定逾時處理,使該服務在5s内均可成功
public String wait(Integer id) {
//自定義錯誤,模拟業務異常
int a =10/0;
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "時間為:" + DateUtil.format(LocalDateTime.now(), "yyyy年MM月dd日 HH:mm:ss") + "時,等待後的線程池:" + Thread.currentThread().getName() + "通路的id為:" + id + "順利執行";
}
//指定的兜底方法,方法名必須與fallbackMethod指定的一緻
public String fallBackHandle(Integer id) {
return "接口異常後,走現在的回調方法" + Thread.currentThread().getName() + "系統繁忙";
}
這樣我們再次調用服務後,如果出現的延時情況小于設定的逾時範圍,也不會直接抛出逾時異常,而是正常等待後處理業務。
但是也會存在一個問題,如果僅在服務端處理延時問題的話,客戶調用端不同時處理的話仍然會出現延時異常,服務端正常。
如果業務調用中确實出現異常無法處理了比如:int a =10/0,也會有我們自定的兜底方法來處理臨時的問題。而不會直接出現錯誤頁面error page。
單側用戶端處理
在用戶端的降級處理與服務端相似,不同點在與服務端在業務實作類上處理,用戶端在controller層處理。
而且需要在yml檔案中添加開啟hytrix服務降級配置。
feign:
#開啟hystrix服務降級
circuitbreaker:
enabled: true
因為使用openfeign實作服務調用,是以并不存在其他業務類來處理服務降級了。
/**
* @author young
* @date 2022/12/24 20:55
* @description: 單側服務降級接口
*/
@Slf4j
@RequestMapping("consumer")
@RestController
public class ConsumerHystrixController {
@Resource
private ProviderHystrixService providerHystrixService;
@GetMapping("/test/ok/{id}")
public Result<String> providerOk(@PathVariable("id") Integer id){
return providerHystrixService.selectOne(id);
}
//配置服務降級,單側用戶端降級
@HystrixCommand(fallbackMethod = "down",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
value = "1500")
})
@GetMapping("/test/wait/{id}")
public Result<String> providerWait(@PathVariable("id") Integer id){
return providerHystrixService.selectOneWait(id);
}
/**
* 執行降級後的方法,傳回對象Result<String>要一緻,否則報錯
* Command type literal pos: unknown; Fallback type literal pos: unknown
*/
public Result<String> down(@PathVariable("id") Integer id){
String s ="我消費者端扛不住了,要開始降級處理了,處理id為"+id;
return Result.ok(s);
}
}
同樣需要在住啟動類上添加Hytrix使用注解,然後編寫自定義的兜底方法來避免業務直接出現error page。
需要注意的地方在于,如果兜底方法的傳回值一定是與處理的業務接口api/方法一緻的,如果我們在用戶端有統一傳回結果類處理,那麼回調兜底的方法也須一緻處理。否則會出現異常,這樣雖然我們服務端方法執行延遲3秒,并且用戶端的服務請求處理的延時範圍<=1.5秒,是以按道理用戶端通路也會出現延時異常,但是也會被降級處理,傳回自定義的消息。
雖然問題處理完成了,但是時也會出現一個很頭疼的問題:随着業務降級處理增多,每一個業務實作類,或者用戶端的調用api接口都需要一個兜底的類去處理,這樣就造成了大量的代碼和業務邏輯耦合高,不符合代碼設計原則。是以Hytrix還有另外的兩種降級處理可供使用:
- 全局服務降級
- 通配服務降級
全局服務降級
顧名思義就是在整個服務api層面進行降級維護處理。這樣大部分服務就能夠節省大量備援的服務降級類,部分熱點api接口另作其他獨立的降級處理即可。這裡我們針對用戶端來實作全局的降級處理效果。
主要通過@DefaultProperties注解來實作
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DefaultProperties {
String groupKey() default "";
String threadPoolKey() default "";
HystrixProperty[] commandProperties() default {};
HystrixProperty[] threadPoolProperties() default {};
Class<? extends Throwable>[] ignoreExceptions() default {};
HystrixException[] raiseHystrixExceptions() default {};
String defaultFallback() default "";
}
重新建構一個controller類後,分别建立三個業務實作的api,分别是資料正常擷取,延時擷取,以及異常。然後定義全局的處理方法,并且在類上添加@DefaultProperties(defaultFallback = "方法名")注解來實作回調方法對全局的服務處理。
/**
* @author young
* @date 2022/12/25 17:07
* @description: 全局配置服務降級
*/
@Slf4j
@RestController
@RequestMapping("global")
@DefaultProperties(defaultFallback = "global")
public class GlobalHystrixController {
@Resource
private ProviderHystrixService providerHystrixService;
@GetMapping("/test/ok/{id}")
public Result<String> providerOk(@PathVariable("id") Integer id){
return providerHystrixService.selectOne(id);
}
//全局配置服務降級
@HystrixCommand
@GetMapping("/test/wait/{id}")
public Result<String> providerWait(@PathVariable("id") Integer id){
return providerHystrixService.selectOneWait(id);
}
/**
* 出現異常可以進行全局處理,成功跳轉到全局處理方法
* @return
*/
@HystrixCommand
@GetMapping("ee")
public Result<String> er(){
int a = 10/0;
return Result.ok("不行");
}
/**
* 傳回類型必須與之前的服務接口傳回類型一緻Result<T>,否則報錯!
* @return
*/
public Result<String> global(){
return Result.ok("全局服務降級配置效果,消費者端扛不住了,要開始降級處理了");
}
}
這樣就能對大部分的服務異常情況做全局處理了。
通配服務降級
通配服務降級可以讓我們實作動态擷取降級服務調用的方法,這樣也能降低服務方法與降級兜底方法的耦合,大大提高處理效率。這種降級方式主要用于用戶端,通過Hytrix與feign聯合進行處理。
既然feign通過接口的方式對服務端api接口進行調用,那麼,我們在feign接口進行處理即可。通過feign接口實作類來對服務端的每個接口統一進行一對一的異常降級處理。
/**
* Author young
* Date 2022/12/25 17:23
* Description: 通過新的類實作feign管理的服務提供端接口,動态擷取降級服務調用的方法
*/
@Component
public class ProviderFeignClientServiceImpl implements ProviderHystrixService{
@Override
public Result<String> selectOne(Integer id) {
return Result.ok("資料不會說謊,降級正常擷取一個id的業務類,id為"+id);
}
@Override
public Result<String> selectOneWait(Integer id) {
return Result.ok("降級線程等待後擷取id的業務接口,id為"+id);
}
}
同時在feign的接口類上指明将包含降級處理的這個實作類作為用于處理所有回調的類。
/**
* @author young
* @date 2022/12/24 20:21
* @description:
*/
/*添加fallback通配的服務降級方法類*/
@FeignClient(value = "CLOUD-HYSTRIX-PROVIDER",fallback = ProviderFeignClientServiceImpl.class)
public interface ProviderHystrixService {
@GetMapping("/hystrix/use/{id}")
Result<String> selectOne(@PathVariable("id") Integer id);
@GetMapping("/hystrix/wait/{id}")
Result<String> selectOneWait(@PathVariable("id") Integer id);
}
這個時候如果服務提供端直接當機了,也能對相關服務進行對應的降級處理。
但是并不代表它都能處理,如果用戶端出現異常,或者服務端延時未降級處理仍然會報錯!寫在接口裡隻能處理服務提供端異常 ,對于用戶端沒有用,服務端報錯:全局降級 > 服務端指定方法降級 > 用戶端實作FeignFallback方法降級。用戶端内方法報錯如果沒有指定方法降級或全局降級會直接抛出異常。是以在筆者看來,這個降價方法更像是單獨處理當機的,如果要避免用戶端,服務提供段報錯還需要和其他降級方法聯合使用才行,大體感覺屬實有些雞肋……也有可能是筆者了解不夠深入吧。
Hytrix實作服務熔斷
關于服務熔斷主要已經在前言上詳細闡述了,這裡主要補充幾點:
Hytrix的熔斷類型:
- 熔斷打開:
在固定時間内(Hystrix預設是10秒),接口調用出錯比率達到一個門檻值(Hystrix預設為50%),會進入熔斷開啟狀态。進入熔斷狀态後,後續對該服務接口的調用不再經過網絡,直接執行本地的fallback方法。
- 熔斷關閉:
服務沒有故障時,熔斷器所處的狀态,對調用方的調用不做任何限制。
- 半熔斷狀态:
在進入熔斷開啟狀态一段時間之後(Hystrix預設是5秒),熔斷器會進入半熔斷狀态。部分請求根據規則調用目前服務,如果請求成功且符合規則則認為目前服務恢複正常,關閉熔斷
斷路器開啟關閉的條件:
- 當滿足一定門檻值的時候(預設10s内超過20個請求次數)
- 當失敗率達到一定的時候(預設10s内超過50%請求失敗)
到達以上門檻值,斷路器将會開啟,開啟後所有請求都不會進行轉發,一段時間後(預設5s),這個時候斷路器是半開狀态,會讓其中一個請求進行轉發,成功:斷路器關閉,失敗:繼續開啟。
斷路器打開後:
- 再有請求調用的時候,不會調用主邏輯,直接調用降級fallback,通過斷路器,實作自動發現錯誤并将降級邏輯切換為主邏輯,減少響應延遲的效果。
- 斷路器打開對主邏輯進行熔斷後,hystrix會啟動一個休眠時間窗,在這個時間窗内降級邏輯是臨時的成為主邏輯,當休眠時間窗到期,斷路器進入半開狀态,釋放一次請求到原來的主邏輯,如果此次請求正常傳回,斷路器将繼續閉合,主邏輯恢複,如果這次請求依然有問題,斷路器繼續進入打開狀态,休眠時間窗重新計時。
它的實作與服務降級差不多,同樣需要在熔斷後使用一個兜底的回調方法來處理業務熔斷後的處理邏輯。
/**
* 用于測試服務熔斷的模拟業務
*
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "circuitFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),//是否開啟斷路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//請求次數
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),//時間視窗期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),//失敗率達到多少後跳閘
})
public String getCircuitBreaker(Integer id) {
if (id < 0) {
throw new RuntimeException("id不能為負數");
}
String uuid = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "編号為:" + uuid;
}
/**
* 回調方法
*
* @param id
* @return
*/
public String circuitFallback(Integer id) {
return "id不能為負數,請稍後再試" + id;
}
這種情況下我們已經對該方法設定了斷路器,并且設定10次請求内如果失敗請求(回調)率超過6成,那麼會發生熔斷,需要隔10s的視窗期後才能恢複正常。
正常情況下正确的通路傳回的是:
失敗的情況下返會回調方法的傳回資訊:
但是如果多次錯誤後失敗率到達設定後就會發生熔斷,此時就算我們請求正确資訊也會導緻請求失敗,觸發回調方法,需要過了時間間隔期後才能再次正确請求。
Hrtrix的學習總結告一段落,雖然Hrtrix已經停止維護了,但是它的設計理念還是很值得學習的,包括後續阿裡的Sentinel。邊學習邊總結吧~