1. 程式人生 > 其它 >(十二)、Hystrix服務降級

(十二)、Hystrix服務降級

目錄

分散式系統面臨的問題

複雜分散式體系結構中的應用程式有數十個依賴關係,每個依賴關係在某些時候將不可避免地失敗。

服務雪崩

多個微服務之間呼叫的時候,假設微服務A呼叫微服務B和微服務C,微服務B和微服務C又呼叫其它的微服務,這就是所謂的“扇出”。如果扇出的鏈路上某個微服務的呼叫響應時間過長或者不可用,對微服務A的呼叫就會佔用越來越多的系統資源,進而引起系統崩潰,所謂的“雪崩效應”.
對於高流量的應用來說,單一的後避依賴可能會導致所有伺服器上的所有資源都在幾秒鐘內飽和。比失敗更糟糕的是,這些應用程式還可能導致服務之間的延遲增加,備份佇列,執行緒和其他系統資源緊張,導致整個系統發生更多的級聯故障。這些都表示需要對故障和延遲進行隔離和管理,以便單個依賴關係的失敗,不能取消整個應用程式或系統。

所以,通常當你發現一個模組下的某個例項失敗後,這時候這個模組依然還會接收流量,然後這個有問題的模組還呼叫了其他的模組,這樣就會發生級聯故障,或者叫雪崩。

Hystrix是什麼

Hystrix是一個用於處理分散式系統的延遲和容錯的開源庫,在分散式系統裡,許多依賴不可避免的會呼叫失敗,比如超時、異常等,Hystrix能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,避免級聯故障,以提高分散式系統的彈性

"斷路器”本身是一種開關裝置,當某個服務單元發生故障之後,通過斷路器的故障監控(類似熔斷保險絲),向呼叫方返回一個符合預期的、可處理的備選響應(FallBack),而不是長時間的等待或者丟擲呼叫方無法處理的異常,這樣就保證了服務呼叫方的執行緒不會被長時間、不必要地佔用,從而避免了故障在分散式系統中的蔓延,乃至雪崩。

Hystrix停更進維

能幹嘛

  • 服務降級
  • 服務熔斷
  • 接近實對的監控

官網資料

https://github.com/Netflix/Hystrix/wiki/How-To-Use

Hystrix官宣,停更進維

https://github.com/Netflix/Hystrix

  • 被動修bugs
  • 不再接受合併請求
  • 不再發布新版本

Hystrix的服務降級熔斷限流概念初講

服務降級

伺服器忙,請稍後再試,不讓客戶端等待並立刻返回一個友好提示,fallback

哪些情況會出發降級

  • 程式執行導常
  • 超時
  • 服務熔斷觸發服務降級
  • 執行緒池/訊號量打滿也會導致服務降級

服務熔斷

類比保險絲達到最大服務訪問後,直接拒絕訪問,拉閘限電,然後呼叫服務降級的方法並返回友好提示。

服務的降級 -> 進而熔斷 -> 恢復呼叫鏈路

服務限流

秒殺高併發等操作,嚴禁一窩蜂的過來擁擠,大家排隊,一秒鐘N個,有序進行。

Hystrix支付微服務構建

將cloud-eureka-server7001改配置成單機版

1.新建cloud-provider-hygtrix-payment8001

2.POM

<?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>cloud2021</artifactId>
        <groupId>com.ylc.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-hygtrix-payment8001</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--hystrix-->
        <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>
        <!--web-->
        <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><!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
            <groupId>com.ylc.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </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>

3.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,http://eureka7002.com:7002/eureka
      defaultZone: http://eureka7001.com:7001/eureka

4.主啟動類

package com.ylc.cloud;

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);
    }
}

5.業務類

services

package com.ylc.cloud.services;

import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    public String paymentInfo_OK(Integer id)
    {
        return "執行緒池:  "+Thread.currentThread().getName()+"  paymentInfo_OK,id:  "+id+"\t"+"O(∩_∩)O哈哈~";
    }

    public String paymentInfo_TimeOut(Integer id)
    {
        try {
            TimeUnit.MILLISECONDS.sleep(3000);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        return "執行緒池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗時(秒): 3";
    }
}

controller

package com.ylc.cloud.controller;

import com.ylc.cloud.services.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)
    {
        String result = paymentService.paymentInfo_TimeOut(id);
        log.info("*****result: "+result);
        return result;
    }
}

6.正常測試

啟動eureka7001

啟動cloud-provider-hystrix-payment8001

訪問

success的方法 - http://localhost:8001/payment/hystrix/ok/1
每次呼叫耗費5秒鐘 - http://localhost:8001/payment/hystrix/timeout/1

JMeter高併發壓測後卡頓

上述在非高併發情形下,還能勉強滿足

JMeter官網

開啟Jmeter,來20000個併發壓死8001,20000個請求都去訪問paymentInfo_TimeOut服務

1.測試計劃中右鍵新增-》執行緒-》執行緒組(執行緒組202102,執行緒數:200,執行緒數:100,其他引數預設)

2.剛剛新建執行緒組202102,右鍵它-》新增-》取樣器-》Http請求-》基本 輸入http://localhost:8001/payment/hystrix/ok/1

3.點選綠色三角形圖示啟動。

看演示結果:拖慢,原因:tomcat的預設的工作執行緒數被打滿了,沒有多餘的執行緒來分解壓力和處理。

Jmeter壓測結論

上面還是服務提供者8001自己測試,假如此時外部的消費者80也來訪問,那消費者只能乾等,最終導致消費端80不滿意,服務端8001直接被拖慢。

訂單微服務呼叫支付服務出現卡頓

1.新建 - cloud-consumer-feign-hystrix-order80

2.pom

<?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>cloud2021</artifactId>
        <groupId>com.ylc.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-feign-hystrix-order80</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--hystrix-->
        <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>
        <!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.ylc.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web-->
        <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.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>

3.YML

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

4.主啟動類

package com.ylc.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 */
@SpringBootApplication
@EnableFeignClients
//@EnableHystrix
public class OrderHystrixMain80
{
    public static void main(String[] args)
    {
        SpringApplication.run(OrderHystrixMain80.class,args);
    }
}

5.業務類

services

package com.ylc.cloud.services;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 */
@Component
@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);
}

controller

package com.ylc.cloud.controller;

import com.ylc.cloud.services.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
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 OrderHystirxController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id)
    {
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_TimeOut(id);
        return result;
    }
}

6.正常測試

http://localhost/consumer/payment/hystrix/ok/1

7.高併發測試

2W個執行緒壓8001

消費端80微服務再去訪問正常的Ok微服務8001地址

http://localhost/consumer/payment/hystrix/ok/32

消費者80被拖慢

原因:8001同一層次的其它介面服務被困死,因為tomcat執行緒池裡面的工作執行緒已經被擠佔完畢。

正因為有上述故障或不佳表現才有我們的降級/容錯/限流等技術誕生

降級容錯解決的維度要求

超時導致伺服器變慢(轉圈) - 超時不再等待

出錯(宕機或程式執行出錯) - 出錯要有兜底

解決:

  • 對方服務(8001)超時了,呼叫者(80)不能一直卡死等待,必須有服務降級。
  • 對方服務(8001)down機了,呼叫者(80)不能一直卡死等待,必須有服務降級。
  • 對方服務(8001)OK,呼叫者(80)自己出故障或有自我要求(自己的等待時間小於服務提供者),自己處理降級。

Hystrix之服務降級支付側fallback

降級配置 - @HystrixCommand

8001先從自身找問題

設定自身呼叫超時時間的峰值,峰值內可以正常執行,超過了需要有兜底的方法處埋,作服務降級fallback。

8001fallback

業務類啟用 - @HystrixCommand報異常後如何處理

—旦呼叫服務方法失敗並丟擲了錯誤資訊後,會自動呼叫@HystrixCommand標註好的fallbackMethod呼叫類中的指定方法

package com.ylc.cloud.services;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    public String paymentInfo_OK(Integer id)
    {
        return "執行緒池:  "+Thread.currentThread().getName()+"  paymentInfo_OK,id:  "+id+"\t"+"O(∩_∩)O哈哈~";
    }

    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler"/*指定善後方法名*/,commandProperties = {
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
    })
    public String paymentInfo_TimeOut(Integer id)
    {
        try {
            TimeUnit.MILLISECONDS.sleep(3000);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        return "執行緒池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗時(秒): 3";
    }

    //用來善後的方法
    public String paymentInfo_TimeOutHandler(Integer id)
    {
        return "執行緒池:  "+Thread.currentThread().getName()+"  8001系統繁忙或者執行報錯,請稍後再試,id:  "+id+"\t"+"o(╥﹏╥)o";
    }
}

主啟動類啟用

新增新註解@EnableCircuitBreaker

測試

我們能接受3秒鐘,它執行5秒鐘,超時異常

localhost:8001/payment/hystrix/timeout/1

上面故意製造兩種異常:

  1. int age = 10/0,計算異常
  2. 我們能接受3秒鐘,它執行5秒鐘,超時異常。

當前服務不可用了,做服務降級,兜底的方案都是paymentInfo_TimeOutHandler

Hystrix之服務降級訂單側fallback

0訂單微服務,也可以更好的保護自己,自己也依樣畫葫蘆進行客戶端降級保護

題外話,切記 - 我們自己配置過的熱部署方式對java程式碼的改動明顯

但對@HystrixCommand內屬性的修改建議重啟微服務

YML

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/


#開啟
feign:
  hystrix:
    enabled: true

主啟動增加註解@EnableHystrix

業務類

@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
    String result = paymentHystrixService.paymentInfo_TimeOut(id);
    return result;
}

//善後方法
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
    return "我是消費者80,對方支付系統繁忙請10秒鐘後再試或者自己執行出錯請檢查自己,o(╥﹏╥)o";
}

OpenFegin預設請求時間超過一秒就會報出異常了

@FeignClient標記的那個service介面下所有的方法進行了hystrix包裝(類似於在這些方法上加了一個@HystrixCommand),這些方法會應用一個預設的超時時間為1s,所以你的service方法也有一個1s的超時時間,service1s就會報異常,controller立馬進入備用方法,controller上那個3秒那超時時間就沒有效果了

改變這個預設超時時間方法:

所以在YML中配置

#開啟
feign:
  hystrix:
    enabled: false


ribbon:
  ReadTimeout: 5000
  ConnectTimeout: 5000

Hystrix之全域性服務降級DefaultProperties

目前問題1 每個業務方法對應一個兜底的方法,程式碼膨脹

解決方法

1:1每個方法配置一個服務降級方法,技術上可以,但是不聰明

1:N除了個別重要核心業務有專屬,其它普通的可以通過@DefaultProperties(defaultFallback = “”)統一跳轉到統一處理結果頁面

通用的和獨享的各自分開,避免了程式碼膨脹,合理減少了程式碼量

加了DefaultProperties屬性註解,並且沒有寫方法具體名字,就用全域性統一的

package com.ylc.cloud.controller;

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 com.ylc.cloud.services.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
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
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystirxController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id)
    {
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand//用全域性的fallback方法
//    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
//            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="4000")
//    })

    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_TimeOut(id);
        return result;
    }

    //善後方法
    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
        return "我是消費者80,對方支付系統繁忙請10秒鐘後再試或者自己執行出錯請檢查自己,o(╥﹏╥)o";
    }

    // 下面是全域性fallback方法
    public String payment_Global_FallbackMethod()
    {
        return "Global異常處理資訊,請稍後再試,/(ㄒoㄒ)/~~";
    }
}

Hystrix之通配服務降級FeignFallback

目前問題2 統一和自定義的分開,程式碼混亂

服務降級,客戶端去呼叫服務端,碰上服務端宕機或關閉

本次案例服務降級處理是在客戶端80實現完成的,與服務端8001沒有關係,只需要為Feign客戶端定義的介面新增一個服務降級處理的實現類即可實現解耦

未來我們要面對的異常

  • 執行

  • 超時

  • 宕機

修改cloud-consumer-feign-hystrix-order80

根據cloud-consumer-feign-hystrix-order80已經有的PaymentHystrixService介面,
重新新建一個類(AaymentFallbackService)實現該介面,統一為接口裡面的方法進行異常處理

PaymentFallbackService類實現PaymentHystrixService介面

package com.ylc.cloud.services;

import org.springframework.stereotype.Component;

@Component
public class PaymentFallbackService implements  PaymentHystrixService{
    @Override
    public String paymentInfo_OK(Integer id)
    {
        return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id)
    {
        return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
    }
}

YML

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/


#開啟
feign:
  hystrix:
    enabled: true


ribbon:
  ReadTimeout: 5000
  ConnectTimeout: 5000

PaymentHystrixService介面

@Component
@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);
}

測試

正常訪問測試 - http://localhost/consumer/payment/hystrix/ok/1

故意關閉微服務8001

客戶端自己呼叫提示 - 此時服務端provider已經down了,但是我們做了服務降級處理,讓客戶端在服務端不可用時也會獲得提示資訊而不會掛起耗死伺服器。

  • 寫在controller類內部的處理,並加上預設或者指定降級處理。如果沒有繼承service的降級處理,他處理的是服務端與客戶端異常
  • 寫在controller類內部的處理,並加上預設或者指定降級處理。如果存在一個繼承service的降級處理,他處理的是客戶端自身的異常
  • 繼承service的降級處理,他處理的是服務端的異常
  • 他們倆都能處理服務端異常,但是如果都存在,則service優先處理服務端出現的異常。

如果是配置了全域性的fallback 會優先進入全域性配置的方法裡面,但是單獨方法配置的fallback會進feign方法裡賣弄

Hystrix之服務熔斷理論

熔斷機制概述

熔斷機制是應對雪崩效應的一種微服務鏈路保護機制。當扇出鏈路的某個微服務出錯不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的呼叫,快速返回錯誤的響應資訊。當檢測到該節點微服務呼叫響應正常後,恢復呼叫鏈路。

在Spring Cloud框架裡,熔斷機制通過Hystrix實現。Hystrix會監控微服務間呼叫的狀況,當失敗的呼叫到一定閾值,預設是5秒內20次呼叫失敗,就會啟動熔斷機制。熔斷機制的註解是@HystrixCommand

1.呼叫失敗會觸發降級,而降級會呼叫fallback方法

2.但無論如何降級的流程一定會先呼叫正常方法再呼叫fallback方法

3假如單位時間內呼叫失敗次數過多,也就是降級次數過多,則觸發熔斷

4熔斷以後就會跳過正常方法直接呼叫fallback方法

5所謂“熔斷後服務不可用”就是因為跳過了正常方法直接執行fallback

降級是思想,熔斷是對降級的具體實現,但是降級的實現並不止熔斷這一種

Martin Fowler的相關論文

Hystrix之服務熔斷案例

Hutool國產工具類

修改cloud-provider-hystrix-payment8001

  //=====服務熔斷
    @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",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 paymentCircuitBreaker(@PathVariable("id") Integer id) {
        if(id < 0) {
            throw new RuntimeException("******id 不能負數");
        }
        String serialNumber = IdUtil.simpleUUID();

        return Thread.currentThread().getName()+"\t"+"呼叫成功,流水號: " + serialNumber;
    }
    public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
        return "id 不能負數,請稍後再試,/(ㄒoㄒ)/~~   id: " +id;
    }

HystrixCommandProperties配置類

package com.netflix.hystrix;

...

public abstract class HystrixCommandProperties {
    private static final Logger logger = LoggerFactory.getLogger(HystrixCommandProperties.class);

    /* defaults */
    /* package */ static final Integer default_metricsRollingStatisticalWindow = 10000;// default => statisticalWindow: 10000 = 10 seconds (and default of 10 buckets so each bucket is 1 second)
    private static final Integer default_metricsRollingStatisticalWindowBuckets = 10;// default => statisticalWindowBuckets: 10 = 10 buckets in a 10 second window so each bucket is 1 second
    private static final Integer default_circuitBreakerRequestVolumeThreshold = 20;// default => statisticalWindowVolumeThreshold: 20 requests in 10 seconds must occur before statistics matter
    private static final Integer default_circuitBreakerSleepWindowInMilliseconds = 5000;// default => sleepWindow: 5000 = 5 seconds that we will sleep before trying again after tripping the circuit
    private static final Integer default_circuitBreakerErrorThresholdPercentage = 50;// default => errorThresholdPercentage = 50 = if 50%+ of requests in 10 seconds are failures or latent then we will trip the circuit
    private static final Boolean default_circuitBreakerForceOpen = false;// default => forceCircuitOpen = false (we want to allow traffic)
    /* package */ static final Boolean default_circuitBreakerForceClosed = false;// default => ignoreErrors = false 
    private static final Integer default_executionTimeoutInMilliseconds = 1000; // default => executionTimeoutInMilliseconds: 1000 = 1 second
    private static final Boolean default_executionTimeoutEnabled = true;

    ...
}

circuitBreaker.requestVolumeThreshold:滑動視窗大小,即觸發熔斷的最小請求數量,預設為 20。舉個例子,一共只有 19 個請求落在視窗內,全都失敗了,也不會觸發熔斷

PaymentController

    @GetMapping("/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id") Integer id)
    {
        String result = paymentService.paymentCircuitBreaker(id);
        log.info("****result: "+result);
        return result;
    }

測試

自測cloud-provider-hystrix-payment8001

正確 - http://localhost:8001/payment/circuit/1

錯誤 - http://localhost:8001/payment/circuit/-1

多次錯誤,再來次正確,但錯誤得顯示

一旦失敗次數達到闕值,就會熔斷,哪怕再次請求正確資料,當過了時間視窗期,再次嘗試才會恢復

重點測試 - 多次錯誤,然後慢慢正確,發現剛開始不滿足條件,就算是正確的訪問地址也不能進行
隔了10秒才慢慢恢復

Hystrix之服務熔斷總結

熔斷型別

  • 熔斷開啟:請求不再進行呼叫當前服務,內部設定時鐘一般為MTTR(平均故障處理時間),當開啟時長達到所設時鐘則進入半熔斷狀態。
  • 熔斷關閉:熔斷關閉不會對服務進行熔斷。
  • 熔斷半開:部分請求根據規則呼叫當前服務,如果請求成功且符合規則則認為當前服務恢復正常,關閉熔斷。

官網斷路器流程圖

涉及到斷路器的三個重要引數:

  • 快照時間窗:斷路器確定是否開啟需要統計一些請求和錯誤資料,而統計的時間範圍就是快照時間窗,預設為最近的10秒。
  • 請求總數閥值:在快照時間窗內,必須滿足請求總數閥值才有資格熔斷。預設為20,意味著在10秒內,如果該hystrix命令的呼叫次數不足20次7,即使所有的請求都超時或其他原因失敗,斷路器都不會開啟。
  • 錯誤百分比閥值:當請求總數在快照時間窗內超過了閥值,比如發生了30次呼叫,如果在這30次呼叫中,有15次發生了超時異常,也就是超過50%的錯誤百分比,在預設設定50%閥值情況下,這時候就會將斷路器開啟。

斷路器開啟或者關閉的條件

  • 到達以下閥值,斷路器將會開啟:

    • 當滿足一定的閥值的時候(預設10秒內超過20個請求次數)

    • 當失敗率達到一定的時候(預設10秒內超過50%的請求失敗)

  • 當開啟的時候,所有請求都不會進行轉發

  • 一段時間之後(預設是5秒),這個時候斷路器是半開狀態,會讓其中一個請求進行轉發。如果成功,斷路器會關閉,若失敗,繼續開啟。

斷路器開啟之後

1:再有請求呼叫的時候,將不會呼叫主邏輯,而是直接呼叫降級fallback。通過斷路器,實現了自動地發現錯誤並將降級邏輯切換為主邏輯,減少響應延遲的效果。

2:原來的主邏輯要如何恢復呢?

對於這一問題,hystrix也為我們實現了自動恢復功能。

當斷路器開啟,對主邏輯進行熔斷之後,hystrix會啟動一個休眠時間窗,在這個時間窗內,降級邏輯是臨時的成為主邏輯,當休眠時間窗到期,斷路器將進入半開狀態,釋放一次請求到原來的主邏輯上,如果此次請求正常返回,那麼斷路器將繼續閉合,主邏輯恢復,如果這次請求依然有問題,斷路器繼續進入開啟狀態,休眠時間窗重新計時。
All配置

@HystrixCommand(fallbackMethod = "fallbackMethod", 
                groupKey = "strGroupCommand", 
                commandKey = "strCommand", 
                threadPoolKey = "strThreadPool",
                
                commandProperties = {
                    // 設定隔離策略,THREAD 表示執行緒池 SEMAPHORE:訊號池隔離
                    @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
                    // 當隔離策略選擇訊號池隔離的時候,用來設定訊號池的大小(最大併發數)
                    @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
                    // 配置命令執行的超時時間
                    @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
                    // 是否啟用超時時間
                    @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
                    // 執行超時的時候是否中斷
                    @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
                    
                    // 執行被取消的時候是否中斷
                    @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
                    // 允許回撥方法執行的最大併發數
                    @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
                    // 服務降級是否啟用,是否執行回撥函式
                    @HystrixProperty(name = "fallback.enabled", value = "true"),
                    // 是否啟用斷路器
                    @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                    // 該屬性用來設定在滾動時間窗中,斷路器熔斷的最小請求數。例如,預設該值為 20 的時候,如果滾動時間窗(預設10秒)內僅收到了19個請求, 即使這19個請求都失敗了,斷路器也不會開啟。
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
                    
                    // 該屬性用來設定在滾動時間窗中,表示在滾動時間窗中,在請求數量超過 circuitBreaker.requestVolumeThreshold 的情況下,如果錯誤請求數的百分比超過50, 就把斷路器設定為 "開啟" 狀態,否則就設定為 "關閉" 狀態。
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                    // 該屬性用來設定當斷路器開啟之後的休眠時間窗。 休眠時間窗結束之後,會將斷路器置為 "半開" 狀態,嘗試熔斷的請求命令,如果依然失敗就將斷路器繼續設定為 "開啟" 狀態,如果成功就設定為 "關閉" 狀態。
                    @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
                    // 斷路器強制開啟
                    @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
                    // 斷路器強制關閉
                    @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
                    // 滾動時間窗設定,該時間用於斷路器判斷健康度時需要收集資訊的持續時間
                    @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
                    
                    // 該屬性用來設定滾動時間窗統計指標資訊時劃分"桶"的數量,斷路器在收集指標資訊的時候會根據設定的時間窗長度拆分成多個 "桶" 來累計各度量值,每個"桶"記錄了一段時間內的採集指標。
                    // 比如 10 秒內拆分成 10 個"桶"收集這樣,所以 timeinMilliseconds 必須能被 numBuckets 整除。否則會拋異常
                    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
                    // 該屬性用來設定對命令執行的延遲是否使用百分位數來跟蹤和計算。如果設定為 false, 那麼所有的概要統計都將返回 -1。
                    @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
                    // 該屬性用來設定百分位統計的滾動視窗的持續時間,單位為毫秒。
                    @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
                    // 該屬性用來設定百分位統計滾動視窗中使用 “ 桶 ”的數量。
                    @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
                    // 該屬性用來設定在執行過程中每個 “桶” 中保留的最大執行次數。如果在滾動時間窗內發生超過該設定值的執行次數,
                    // 就從最初的位置開始重寫。例如,將該值設定為100, 滾動視窗為10秒,若在10秒內一個 “桶 ”中發生了500次執行,
                    // 那麼該 “桶” 中只保留 最後的100次執行的統計。另外,增加該值的大小將會增加記憶體量的消耗,並增加排序百分位數所需的計算時間。
                    @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
                    
                    // 該屬性用來設定採集影響斷路器狀態的健康快照(請求的成功、 錯誤百分比)的間隔等待時間。
                    @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
                    // 是否開啟請求快取
                    @HystrixProperty(name = "requestCache.enabled", value = "true"),
                    // HystrixCommand的執行和事件是否列印日誌到 HystrixRequestLog 中
                    @HystrixProperty(name = "requestLog.enabled", value = "true"),

                },
                threadPoolProperties = {
                    // 該引數用來設定執行命令執行緒池的核心執行緒數,該值也就是命令執行的最大併發量
                    @HystrixProperty(name = "coreSize", value = "10"),
                    // 該引數用來設定執行緒池的最大佇列大小。當設定為 -1 時,執行緒池將使用 SynchronousQueue 實現的佇列,否則將使用 LinkedBlockingQueue 實現的佇列。
                    @HystrixProperty(name = "maxQueueSize", value = "-1"),
                    // 該引數用來為佇列設定拒絕閾值。 通過該引數, 即使佇列沒有達到最大值也能拒絕請求。
                    // 該引數主要是對 LinkedBlockingQueue 佇列的補充,因為 LinkedBlockingQueue 佇列不能動態修改它的物件大小,而通過該屬性就可以調整拒絕請求的佇列大小了。
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
                }
               )
public String doSomething() {
	...
}

Hystrix工作流程最後總結

服務限流 - 後面高階篇講解alibaba的Sentinel說明

官方解釋

Hystrix圖形化Dashboard搭建

概述

除了隔離依賴服務的呼叫以外,Hystrix還提供了準實時的呼叫監控(Hystrix Dashboard),Hystrix會持續地記錄所有通過Hystrix發起的請求的執行資訊,並以統計報表和圖形的形式展示給使用者,包括每秒執行多少請求多少成功,多少失敗等。

Netflix通過hystrix-metrics-event-stream專案實現了對以上指標的監控。Spring Cloud也提供了Hystrix Dashboard的整合,對監控內容轉化成視覺化介面。
儀表盤9001

1新建cloud-consumer-hystrix-dashboard9001

2.POM

<?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>cloud2021</artifactId>
        <groupId>com.ylc.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-hystrix-dashboard9001</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <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-actuator</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>

3.YML

server:
  port: 9001

4.主啟動類

package com.ylc.cloud;

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);
    }
}

5.所有Provider微服務提供類(8001/8002/8003)都需要監控依賴配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

6.啟動cloud-consumer-hystrix-dashboard9001該微服務後續將監控微服務8001

瀏覽器輸入http://localhost:9001/hystrix

Hystrix圖形化Dashboard監控實戰

修改cloud-provider-hystrix-payment8001

注意:新版本Hystrix需要在主啟動類PaymentHystrixMain8001中指定監控路徑

package com.ylc.cloud;

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);
    }

    /**
     *此配置是為了服務監控而配置,與服務容錯本身無關,springcloud升級後的坑
     *ServletRegistrationBean因為springboot的預設路徑不是"/hystrix.stream",
     *只要在自己的專案裡配置上下面的servlet就可以了
     *否則,Unable to connect to Command Metric Stream 404
     */
    @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;
    }
}

監控測試

啟動1個eureka

啟動8001,9001

觀察監控視窗

9001監控8001 - 填寫監控地址 - http://localhost:8001/hystrix.streamhttp://localhost:9001/hystrix頁面的輸入框。

測試地址

測試通過

先訪問正確地址,再訪問錯誤地址,再正確地址,會發現圖示斷路器都是慢慢放開的。

如何看?

  • 7色

  • 1圈

實心圓:共有兩種含義。它通過顏色的變化代表了例項的健康程度,它的健康度從綠色<黃色<橙色<紅色遞減。

該實心圓除了顏色的變化之外,它的大小也會根據例項的請求流量發生變化,流量越大該實心圓就越大。所以通過該實心圓的展示,就可以在大量的例項中快速的發現故障例項和高壓力例項。

  • 1線

曲線:用來記錄2分鐘內流量的相對變化,可以通過它來觀察到流量的上升和下降趨勢。

  • 整圖說明
  • 整圖說明2