天天看點

05、SpringCloud之Hystrix元件學習筆記

文章目錄

  • ​​前言​​
  • ​​一、服務雪崩​​
  • ​​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、引出服務雪崩

分布式場景下:

05、SpringCloud之Hystrix元件學習筆記

在高并發場景下:由于服務之間會進行調用,一旦某個服務不可用,那麼就會出現服務雪崩

一旦服務鍊路中出現了某個服務不可用,那麼就會影響整個鍊路,進而出現不可預計的問題!

05、SpringCloud之Hystrix元件學習筆記

服務雪崩的本質:由于調用的服務方不可用,就會導緻對應的線程沒有及時回收。

解決關鍵:不管是調用成功還是失敗,隻要線程可以及時回收,就可以解決服務雪崩。

1.2、雪崩三階段

1、服務不可用:硬體故障/程式Bug/緩存擊穿/使用者大量請求。

2、調用端重試加大流量:使用者重試/代碼邏輯重試。

3、服務調用者不可用:同步等待造成的資源耗盡。

1.3、如何解決服務雪崩

方案描述:

1、應用擴容:加機器或更新硬體。

2、流控:限流/關閉重試。

3、緩存預加載。

4、服務降級:服務接口拒絕服務/頁面拒絕服務/延遲持久化/随機拒絕服務。

5、服務熔斷。

方案一:修改調用的逾時時長(不推薦)

思路:将服務間的調用逾時時長改小,這樣就可以讓線程及時回收,保證服務可用

優點:非常簡單,也可以有效的解決服務雪崩

缺點:不夠靈活,有的服務需要更長的時間去處理(寫庫,整理資料)

方案二:設定攔截器(設定斷路器)

思路:在調用遠端服務前來設定一個攔截器來進行服務狀态判斷。

05、SpringCloud之Hystrix元件學習筆記

二、認識Hystrix

2.1、服務熔斷概念及斷路器

問題描述:當下遊服務因某種原因突然變得不可用或響應過慢,上遊服務為保證自己整體服務的可用性,不再繼續調用目标服務,直接傳回,快速釋放資源,如果目标服務情況好轉則恢複調用。

解決方案:斷路器模式。

斷路器原理:當遠端服務被調用時,斷路器将監視這個調用,如調用時間太長,斷路器将會介入并中斷調用。 斷路器将監視所有遠端資源的調用,如對某個遠端資源的調用失敗次數足夠多,那麼斷路器會出現并采取快速失敗,阻止将來調用失敗的遠端資源

狀态圖:

05、SpringCloud之Hystrix元件學習筆記

解析:

斷路器最開始處于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>      
05、SpringCloud之Hystrix元件學習筆記

注冊中心使用之前案例中的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>      
05、SpringCloud之Hystrix元件學習筆記

①配置檔案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​

05、SpringCloud之Hystrix元件學習筆記

①配置檔案: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、啟動服務,引入服務調用失敗問題

啟動一個注冊中心以及剛剛建立的兩個服務:

05、SpringCloud之Hystrix元件學習筆記

通路一下(正常):http://localhost:8082/customerRent

05、SpringCloud之Hystrix元件學習筆記

然後我們把RentCar服務關閉掉之後,再次通路:

05、SpringCloud之Hystrix元件學習筆記

若是服務不可用,那麼就會出現服務調用失敗的情況,對于在高并發情況下若是頻繁出現這種情況則會導緻服務雪崩,進而出現大問題!

那麼如何解決呢?

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的熔斷器

05、SpringCloud之Hystrix元件學習筆記
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屬性來指定對應的熔斷方法

05、SpringCloud之Hystrix元件學習筆記
@FeignClient(value = "rent-car-service", fallback = CustomerRentHystrix.class)      

此時我們再來測試一下!

05、SpringCloud之Hystrix元件學習筆記

四、手寫斷路器

4.1、斷路器設計

本質就是在目前遠端調用發起前對其進行代理:

05、SpringCloud之Hystrix元件學習筆記

時間視窗滑動模型圖:

05、SpringCloud之Hystrix元件學習筆記
05、SpringCloud之Hystrix元件學習筆記

斷路器狀态介紹以及不同的狀态轉變方案:三個狀态​

​closed、half open、open​

關:服務正常調用 A---》B 
開:在一段時間内,調用失敗次數達到閥值(5s 内失敗 3 次)(5s 失敗 30 次的)則斷路器打開,直接 return 
半開:斷路器打開後,過一段時間,讓少許流量嘗試調用 B 服務,如果成功則斷路器關閉, 使服務正常調用,如果失敗,則繼續半開      

注意點:

1、一個服務一個斷路器執行個體。

2、其他手寫時的相關問題。

斷路器執行個體中的屬性:①斷路器目前的狀态。②目前的錯誤次數。

三種狀态如何切換?

預設剛開始是closed(也就是正常去進行遠端調用狀态),一旦通路失敗了一次,此時就會變為open狀态,那麼在open狀态過程中會直接傳回對應的斷路器結果,在一定的時間視窗(指定秒數)到達之後【多線程添加一個定時器】,此時狀态會進入到半開狀态,那麼就會放一些流量出來去嘗試通路服務提供方,若是發現此時通路成功!那麼狀态依舊會修改為closed。

為什麼要使用一個定時器來進行定期清除呢?一些大量并發場景下,需要使用一個定時器來進行對失敗次數清零。

4.2、實作斷路器功能

首先準備好在3.1中的調用服務新案例,然後我們基于此來實作一個斷路器:

05、SpringCloud之Hystrix元件學習筆記

實作完成之後如下:

05、SpringCloud之Hystrix元件學習筆記

①狀态枚舉:

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、斷路器測試

初始情況:啟動三個服務,分别是注冊中心,服務提供者以及服務消費方(也就是我們自定義實作斷路器)

05、SpringCloud之Hystrix元件學習筆記

通路下網址路徑:http://localhost:8082/customerRent

05、SpringCloud之Hystrix元件學習筆記

關閉服務提供方:

05、SpringCloud之Hystrix元件學習筆記

再此嘗試通路:可以看到我們實作的熔斷器起了效果

05、SpringCloud之Hystrix元件學習筆記

最終我們重新開機服務提供方:

05、SpringCloud之Hystrix元件學習筆記
05、SpringCloud之Hystrix元件學習筆記

可以看到也能夠進行通路!

五、Hystrix配置

詳細配置:​​hystrix 配置​​
05、SpringCloud之Hystrix元件學習筆記
05、SpringCloud之Hystrix元件學習筆記

對于配置中的隔離方式政策介紹如下:隔離政策包含thread線程以及semphore信号量隔離

05、SpringCloud之Hystrix元件學習筆記

線程隔離(場景:通路量比較大):

說明:按照 group(10 個線程)劃分服務提供者,使用者請求的線程 和做遠端的線程不一樣。
好處:當 B 服務調用失敗了 或者請求 B 服務的量太大了 不會對 C 服務造成影響 使用者通路比較大的情 況下使用比較好 異步的方式。
缺點:具有線程切換的開銷,對機器性能影響。
應用場景 調用第三方服務 并發量大的情況下      

SEMAPHORE 信号量隔離(場景:通路量比較小):

說明:每次請進來 有一個原子計數器 做請求次數的++ 當請求完成以後 --。
好處:對 cpu 開銷小。
缺點:并發請求不易太多 當請求過多 就會拒絕請求 做一個保護機制。
場景:使用内部調用 ,并發小的情況下。
源碼入門 HystrixCommand AbstractCommand HystrixThreadPool      

參考資料

[1]. ​​hystrix 配置​​

[2]. ​​Hystrix的原理及使用​​