文章目錄
- 前言
- 一、服務雪崩
- 1.1、引出服務雪崩
- 1.2、雪崩三階段
- 1.3、如何解決服務雪崩
- 方案一:修改調用的逾時時長(不推薦)
- 方案二:設定攔截器(設定斷路器)
- 二、認識Hystrix
- 2.1、服務熔斷概念及斷路器
- 2.2、Spring Cloud Hystrix介紹
- 三、快速入門Hystrix
- 3.1、搭建基礎服務(服務提供方以及消費方)
- 3.2、啟動服務,引入服務調用失敗問題
- 3.3、解決方案:使用Hystrix熔斷器
- 四、手寫斷路器
- 4.1、斷路器設計
- 4.2、實作斷路器功能
- 4.3、斷路器測試
- 五、Hystrix配置
- 參考資料
前言
本節配套案例代碼:Gitee倉庫、Github倉庫
所有部落格檔案目錄索引:部落格目錄索引(持續更新)
學習視訊:動力節點最新SpringCloud視訊教程|最适合自學的springcloud+springcloudAlibaba
PS:本章節中部分圖檔是直接引用學習課程課件,如有侵權,請聯系删除。
一、服務雪崩
1.1、引出服務雪崩
分布式場景下:
在高并發場景下:由于服務之間會進行調用,一旦某個服務不可用,那麼就會出現服務雪崩
一旦服務鍊路中出現了某個服務不可用,那麼就會影響整個鍊路,進而出現不可預計的問題!
服務雪崩的本質:由于調用的服務方不可用,就會導緻對應的線程沒有及時回收。
解決關鍵:不管是調用成功還是失敗,隻要線程可以及時回收,就可以解決服務雪崩。
1.2、雪崩三階段
1、服務不可用:硬體故障/程式Bug/緩存擊穿/使用者大量請求。
2、調用端重試加大流量:使用者重試/代碼邏輯重試。
3、服務調用者不可用:同步等待造成的資源耗盡。
1.3、如何解決服務雪崩
方案描述:
1、應用擴容:加機器或更新硬體。
2、流控:限流/關閉重試。
3、緩存預加載。
4、服務降級:服務接口拒絕服務/頁面拒絕服務/延遲持久化/随機拒絕服務。
5、服務熔斷。
方案一:修改調用的逾時時長(不推薦)
思路:将服務間的調用逾時時長改小,這樣就可以讓線程及時回收,保證服務可用
優點:非常簡單,也可以有效的解決服務雪崩
缺點:不夠靈活,有的服務需要更長的時間去處理(寫庫,整理資料)
方案二:設定攔截器(設定斷路器)
思路:在調用遠端服務前來設定一個攔截器來進行服務狀态判斷。
二、認識Hystrix
2.1、服務熔斷概念及斷路器
問題描述:當下遊服務因某種原因突然變得不可用或響應過慢,上遊服務為保證自己整體服務的可用性,不再繼續調用目标服務,直接傳回,快速釋放資源,如果目标服務情況好轉則恢複調用。
解決方案:斷路器模式。
斷路器原理:當遠端服務被調用時,斷路器将監視這個調用,如調用時間太長,斷路器将會介入并中斷調用。 斷路器将監視所有遠端資源的調用,如對某個遠端資源的調用失敗次數足夠多,那麼斷路器會出現并采取快速失敗,阻止将來調用失敗的遠端資源
狀态圖:
解析:
斷路器最開始處于closed狀态,一旦檢測到的錯誤到達一定數量,斷路器便轉為open狀态(斷路器打開);
此時到達reset timeout時間會轉移到half open狀态;
嘗試放行一部分請求到後端,一旦檢測成功便回歸到closed狀态,即恢複服務
斷路器實作方案:阿裡的Sentinel、netflix的Hystric。
2.2、Spring Cloud Hystrix介紹
熔斷器,也叫斷路器!(正常情況下 斷路器是關的 隻有出了問題才打開)用來保護微服務不雪崩的方法。思想和我們上面畫的攔截器一樣。
Hystrix 是 Netflix 公司開源的一個項目,它提供了熔斷器功能,能夠阻止分布式系統中出現關聯故障。Hystrix 是通過隔離服務的通路點阻止關聯故障的,并提供了故障的解決方案,從 而提高了整個分布式系統的彈性。
例如:微網誌 彈性雲擴容 Docker K8s。
三、快速入門Hystrix
3.1、搭建基礎服務(服務提供方以及消費方)
項目版本:SpringBoot:2.3.12.RELEASE、SpringCloud:Hoxton.SR12
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
注冊中心使用之前案例中的Eureka,然後在04-hystrix中建立兩個服務來進行demo展示。
1、建立借車服務:
01-rent-car-service
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
①配置檔案application.yml
server:
port: 8081
spring:
application:
name: rent-car-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
②在啟動器中添加開啟EurekaClient:
@EnableEurekaClient
③添加控制器:controller/RentController.java
package com.changlu.rentcarservice.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description:
* @Author: changlu
* @Date: 9:18 PM
*/
@RestController
public class RentCarController {
@GetMapping("/rent")
public String rent() {
return "租車成功!";
}
}
2、建立消費者服務:
02-customer-service
①配置檔案:application.yml:
server:
port: 8082
spring:
application:
name: customer-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
②在啟動器中添加服務發現注解以及掃描feign包注解
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.changlu.customerservice.feign") //開啟feign包掃描
③建立feign包,添加租車服務的接口方法
package com.changlu.customerservice.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @Description:
* @Author: changlu
* @Date: 9:30 PM
*/
@FeignClient("rent-car-service") //對應服務名
public interface CustomerRentFeign {
@GetMapping("/rent")
public String rent();
}
④建立控制器:
controller/CustomerController.java
package com.changlu.customerservice.controller;
import com.changlu.customerservice.feign.CustomerRentFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description:
* @Author: changlu
* @Date: 9:28 PM
*/
@RestController
public class CustomerController {
@Autowired
private CustomerRentFeign customerRentFeign;//遠端調用
@GetMapping("/customerRent")
public String customerRent() {
System.out.println("來進行通路租車了!");
//進行一個遠端調用
String rent = customerRentFeign.rent();
return rent;
}
}
至此兩個服務目前已經搭建完成!
3.2、啟動服務,引入服務調用失敗問題
啟動一個注冊中心以及剛剛建立的兩個服務:
通路一下(正常):http://localhost:8082/customerRent
然後我們把RentCar服務關閉掉之後,再次通路:
若是服務不可用,那麼就會出現服務調用失敗的情況,對于在高并發情況下若是頻繁出現這種情況則會導緻服務雪崩,進而出現大問題!
那麼如何解決呢?
3.3、解決方案:使用Hystrix熔斷器
引入過程:
①引入Hystrix依賴:其實不引入也是可以的,因為feign依賴中就自帶hystrix依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
②在配置中開啟Hystrix熔斷器:在Hoxton.SR12版本中預設是關閉的
feign:
hystrix:
enabled: true # 熔斷器開啟
③編寫對應feign的熔斷器
package com.changlu.customerservice.feign.hystrix;
import com.changlu.customerservice.feign.CustomerRentFeign;
import org.springframework.stereotype.Component;
/**
* @Description: 消費者-借車熔斷器
* @Author: changlu
* @Date: 9:19 AM
*/
@Component
public class CustomerRentHystrix implements CustomerRentFeign {
@Override
public String rent() {
return "租車成功!(熔斷器)";
}
}
④在對應的feign中添加相應的fallback屬性來指定對應的熔斷方法
@FeignClient(value = "rent-car-service", fallback = CustomerRentHystrix.class)
此時我們再來測試一下!
四、手寫斷路器
4.1、斷路器設計
本質就是在目前遠端調用發起前對其進行代理:
時間視窗滑動模型圖:
斷路器狀态介紹以及不同的狀态轉變方案:三個狀态
closed、half open、open
關:服務正常調用 A---》B
開:在一段時間内,調用失敗次數達到閥值(5s 内失敗 3 次)(5s 失敗 30 次的)則斷路器打開,直接 return
半開:斷路器打開後,過一段時間,讓少許流量嘗試調用 B 服務,如果成功則斷路器關閉, 使服務正常調用,如果失敗,則繼續半開
注意點:
1、一個服務一個斷路器執行個體。
2、其他手寫時的相關問題。
斷路器執行個體中的屬性:①斷路器目前的狀态。②目前的錯誤次數。
三種狀态如何切換?
預設剛開始是closed(也就是正常去進行遠端調用狀态),一旦通路失敗了一次,此時就會變為open狀态,那麼在open狀态過程中會直接傳回對應的斷路器結果,在一定的時間視窗(指定秒數)到達之後【多線程添加一個定時器】,此時狀态會進入到半開狀态,那麼就會放一些流量出來去嘗試通路服務提供方,若是發現此時通路成功!那麼狀态依舊會修改為closed。
為什麼要使用一個定時器來進行定期清除呢?一些大量并發場景下,需要使用一個定時器來進行對失敗次數清零。
4.2、實作斷路器功能
首先準備好在3.1中的調用服務新案例,然後我們基于此來實作一個斷路器:
實作完成之後如下:
①狀态枚舉:
package com.changlu.myhystrix.hystrix.model;
/**
* @Description:
* @Author: changlu
* @Date: 9:54 AM
*/
public enum HystrixStatus {
//定義三種狀态:關閉、開啟、半開
CLOSE,
OPEN,
HALF_OPEN
}
②斷路器注解:
package com.changlu.myhystrix.hystrix.anno;
import java.lang.annotation.*;
/**
* @Description:
* @Author: changlu
* @Date: 9:59 AM
*/
@Target(ElementType.METHOD) //面向方法
@Retention(RetentionPolicy.RUNTIME) //運作時
@Documented
@Inherited
public @interface MyHystrix {
}
③斷路器切面:
package com.changlu.myhystrix.hystrix.aspect;
import com.changlu.myhystrix.hystrix.HystrixPlus;
import com.changlu.myhystrix.hystrix.model.HystrixStatus;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
* @Description: 熔斷器切面
* @Author: changlu
* @Date: 10:00 AM
*/
@Component
@Aspect
public class HystrixAspect {
//切面表達式
// public static final String POINT_COT = "execution (* com.changlu.myhystrix.controller.CustomerController.customerRent(..))";
//定義一個斷路器Map
private static Map<String, HystrixPlus> hystrixMap = new HashMap<>();
static {
hystrixMap.put("rent-car-service", new HystrixPlus());
}
//随機器工具
public static ThreadLocal<Random> randomThreadLocal = ThreadLocal.withInitial(()->new Random());
//根據注解來進行切面處理
@Around(value = "@annotation(com.changlu.myhystrix.hystrix.anno.MyHystrix)")
public Object hystrixAround(ProceedingJoinPoint joinPoint) {
//結果集
Object res = null;
//根據目前的服務名來擷取到對應的斷路器
HystrixPlus hystrix = hystrixMap.get("rent-car-service");
HystrixStatus status = hystrix.getStatus();
switch (status) {
case CLOSE:
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
//進行計數,并且響應結果
hystrix.addFailCount();
return "熔斷器傳回結果";
}
case OPEN://打開狀态,表示不能調用
return "熔斷器傳回結果";
case HALF_OPEN:
Random random = randomThreadLocal.get();
int num = random.nextInt(5);//[0-4]
//友善回收
randomThreadLocal.remove();
//放行部分流量
if (num == 1) {
try {
res = joinPoint.proceed();
//調用成功,斷路器關閉
hystrix.setStatus(HystrixStatus.CLOSE);
//進行喚醒清理程式
synchronized (hystrix.getLock()) {
hystrix.getLock().notifyAll();
}
return res;
} catch (Throwable throwable) {
throwable.printStackTrace();
return "熔斷器傳回結果";
}
}
default:
return "熔斷器傳回結果";
}
}
}
④斷路器實作:
package com.changlu.myhystrix.hystrix;
import com.changlu.myhystrix.hystrix.model.HystrixStatus;
import lombok.Data;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description:
* @Author: changlu
* @Date: 10:04 AM
*/
@Data
public class HystrixPlus {
//時間視窗
private static final Integer WINDOW_TIME = 20;
//失敗次數
private static final Integer MAX_FAIL_COUNT = 3;
//定義一個狀态
private HystrixStatus status = HystrixStatus.CLOSE;
//錯誤次數計數器
private AtomicInteger currentFailCount = new AtomicInteger(0);
//定義一個線程池
private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
4,
8,
30,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//鎖
private Object lock = new Object();
{
//送出定期清零報錯次數
poolExecutor.execute(()->{
while (true) {
try {
TimeUnit.SECONDS.sleep(WINDOW_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
//根據目前的狀态來判斷是否要進行清理
if (this.status.equals(HystrixStatus.CLOSE)) {
this.currentFailCount.set(0);
}else {
// 半開或者開 不需要去記錄次數 這個線程可以不工作
// 學過生産者 消費者模型 wait notifyAll condition singleAll await 它們隻能随機喚醒某一個線程
// lock鎖 源碼 CLH 隊列 放線程 A B C D E park unpark 可以 喚醒指定的某一個線程
// LockSupport.park();
// LockSupport.unpark();
try {
//進行阻塞,防止大量占據cpu
this.lock.wait();
System.out.println("開始進行失敗次數清零操作");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
//增加錯誤次數,若是錯誤此時達到瓶頸,那麼就需要将目前狀态轉為open狀态并送出定時任務來進行修改為half open狀态,并且清零
public void addFailCount() {
int i = currentFailCount.incrementAndGet();
if (i >= MAX_FAIL_COUNT) {
//将目前熔斷器狀态設定開啟狀态
this.status = HystrixStatus.OPEN;
poolExecutor.execute(()->{
try {
TimeUnit.SECONDS.sleep(WINDOW_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (this.status != HystrixStatus.CLOSE) {
//設定半開狀态并且計數清零
this.status = HystrixStatus.HALF_OPEN;
this.currentFailCount.set(0);
}
});
}
}
}
4.3、斷路器測試
初始情況:啟動三個服務,分别是注冊中心,服務提供者以及服務消費方(也就是我們自定義實作斷路器)
通路下網址路徑:http://localhost:8082/customerRent
關閉服務提供方:
再此嘗試通路:可以看到我們實作的熔斷器起了效果
最終我們重新開機服務提供方:
可以看到也能夠進行通路!
五、Hystrix配置
詳細配置:hystrix 配置
對于配置中的隔離方式政策介紹如下:隔離政策包含thread線程以及semphore信号量隔離
線程隔離(場景:通路量比較大):
說明:按照 group(10 個線程)劃分服務提供者,使用者請求的線程 和做遠端的線程不一樣。
好處:當 B 服務調用失敗了 或者請求 B 服務的量太大了 不會對 C 服務造成影響 使用者通路比較大的情 況下使用比較好 異步的方式。
缺點:具有線程切換的開銷,對機器性能影響。
應用場景 調用第三方服務 并發量大的情況下
SEMAPHORE 信号量隔離(場景:通路量比較小):
說明:每次請進來 有一個原子計數器 做請求次數的++ 當請求完成以後 --。
好處:對 cpu 開銷小。
缺點:并發請求不易太多 當請求過多 就會拒絕請求 做一個保護機制。
場景:使用内部調用 ,并發小的情況下。
源碼入門 HystrixCommand AbstractCommand HystrixThreadPool
參考資料
[1]. hystrix 配置
[2]. Hystrix的原理及使用