06-CircuitBreaker断路器

1、介绍

Spring Cloud Circuit breaker provides an abstraction across different circuit breaker implementations. It provides a consistent API to use in your applications allowing you the developer to choose the circuit breaker implementation that best fits your needs for your app.

Spring Cloud断路器提供了一个跨不同断路器的抽象实现。它提供了在您的应用程序中使用的一致的API,允许您的开发人员选择最适合您的应用程序需要的断路器实现。

CircuitBreaker通过具有三种正常状态的有限状态机实现:CLOSED,OPEN和HALF_OPEN以及两个特殊状态DISABLED和FORCED_OPEN。当熔断器关闭时,所有的请求都会通过熔断器。如果失败率超过设定的阈值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率,如果失败率超过阈值,则变为打开状态,如果失败率低于阈值,则变为关闭状态。

断路器状态机

支持断路器实现有以下几种

  • Netfix Hystrix (Spring Cloud官方已经不予支持)
  • Resilience4J (支持)
  • Sentinel
  • Spring Retry

2、快速开始

pom.xml
maven依赖配置文件,如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.mindasoft</groupId>
    <artifactId>spring-cloud-circuitbreaker-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-circuitbreaker-consumer</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>2021.0.1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>nexus-aliyun</id>
            <name>Nexus aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <layout>default</layout>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>nexus-aliyun</id>
            <name>Nexus aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <releases>
                <enabled>true</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>

</project>

比上个项目多了如下配置:

  <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
        </dependency>

使用resilience4j 实现断路器。

application.xml

server.port=9004

# 服务注册中心地址
eureka.client.service-url.defaultZone=http://localhost:9000/eureka/

# 服务名称
spring.application.name=circuitbreaker-customer

SpringCloudCircuitBreakerConsumerApplication

package com.mindasoft;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;

@EnableDiscoveryClient // Eureka Discovery Client 标识
@SpringBootApplication
public class SpringCloudCircuitBreakerConsumerApplication {

    // 开启负载均衡的功能
    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(3)).build())
                .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
                .build());
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudCircuitBreakerConsumerApplication.class, args);
    }

}

ConsumerController

package com.mindasoft.consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * Created by huangmin on 2017/11/10 14:50.
 */
@RestController
public class ConsumerController {

    private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerController.class);

    @Autowired
    private RestTemplate restTemplate; // HTTP 访问操作类
    @Autowired
    private CircuitBreakerFactory circuitBreakerFactory;

    @RequestMapping("/hello")
    public String hello() {
        String providerMsg = circuitBreakerFactory.create("hello")
                .run(() -> restTemplate.getForObject("http://PROVIDER-SERVICE/hello", String.class),
                        throwable -> "CircuitBreaker fallback msg");

        return "Hello,I'm Customer! msg from provider : <br/><br/> " + providerMsg;
    }

}

启动eureka server和本项目,访问http://127.0.0.1:9004/hello
页面打印如下:

Hello,I'm Customer! msg from provider :

CircuitBreaker fallback msg

说明进入到了断路throwable 处理逻辑。

3、Resilience4J介绍

Resilience4j是一款轻量级,易于使用的容错库,其灵感来自于Netflix Hystrix,但是专为Java 8和函数式编程而设计。轻量级,因为库只使用了Vavr,它没有任何其他外部依赖下。相比之下,Netflix Hystrix对Archaius具有编译依赖性,Archaius具有更多的外部库依赖性,例如Guava和Apache Commons Configuration。

Resilience4j提供了以下的核心模块和拓展模块:

核心模块:

  • resilience4j-circuitbreaker: Circuit breaking
  • resilience4j-ratelimiter: Rate limiting
  • resilience4j-bulkhead: Bulkheading
  • resilience4j-retry: Automatic retrying (sync and async)
  • resilience4j-cache: Result caching
  • resilience4j-timelimiter: Timeout handling

Resilience4j记录请求状态的数据结构和Hystrix不同,Hystrix是使用滑动窗口来进行存储的,而Resilience4j采用的是Ring Bit Buffer(环形缓冲区)。Ring Bit Buffer在内部使用BitSet这样的数据结构来进行存储,BitSet的结构如下图所示:
环形缓冲区

每一次请求的成功或失败状态只占用一个bit位,与boolean数组相比更节省内存。BitSet使用long[]数组来存储这些数据,意味着16个值(64bit)的数组可以存储1024个调用状态。

计算失败率需要填满环形缓冲区。例如,如果环形缓冲区的大小为10,则必须至少请求满10次,才会进行故障率的计算,如果仅仅请求了9次,即使9个请求都失败,熔断器也不会打开。但是CLOSE状态下的缓冲区大小设置为10并不意味着只会进入10个 请求,在熔断器打开之前的所有请求都会被放入。

当故障率高于设定的阈值时,熔断器状态会从由CLOSE变为OPEN。这时所有的请求都会抛出CallNotPermittedException异常。当经过一段时间后,熔断器的状态会从OPEN变为HALF_OPEN,HALF_OPEN状态下同样会有一个Ring Bit Buffer,用来计算HALF_OPEN状态下的故障率,如果高于配置的阈值,会转换为OPEN,低于阈值则装换为CLOSE。与CLOSE状态下的缓冲区不同的地方在于,HALF_OPEN状态下的缓冲区大小会限制请求数,只有缓冲区大小的请求数会被放入。

除此以外,熔断器还会有两种特殊状态:DISABLED(始终允许访问)和FORCED_OPEN(始终拒绝访问)。这两个状态不会生成熔断器事件(除状态装换外),并且不会记录事件的成功或者失败。退出这两个状态的唯一方法是触发状态转换或者重置熔断器。

熔断器关于线程安全的保证措施有以下几个部分:

  • 熔断器的状态使用AtomicReference保存的
  • 更新熔断器状态是通过无状态的函数或者原子操作进行的
  • 更新事件的状态用synchronized关键字保护
  • 意味着同一时间只有一个线程能够修改熔断器状态或者记录事件的状态。

使用注解配置@CircuitBreaking

首先在POM文件中添加:

        <!-- resilience spring boot integration with annotation requires spring aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

spring-boot-starter-aop必须添加,否则annotation不会生效。

application.xml

server.port=9004

# 服务注册中心地址
eureka.client.service-url.defaultZone=http://localhost:9000/eureka/

# 服务名称
spring.application.name=circuitbreaker-customer

# 断路器配置
resilience4j.circuitbreaker:
  configs:
    default:
      ringBufferSizeInClosedState: 1 # 熔断器关闭时的缓冲区大小
      ringBufferSizeInHalfOpenState: 1 # 熔断器半开时的缓冲区大小
      waitDurationInOpenState: 60000 # 熔断器从打开到半开需要的时间
      failureRateThreshold: 100 # 熔断器打开的失败阈值
      eventConsumerBufferSize: 5 # 事件缓冲区大小
      registerHealthIndicator: true # 健康监测
      automaticTransitionFromOpenToHalfOpenEnabled: true # 是否自动从打开到半开,不需要触发
  instances:
    providerService: # 断路器策略的命名
      baseConfig: default #继承基础配置

直接在配置文件中,添加断路器配置。JavaConfig模式的配置我们就去掉了。

SpringCloudCircuitBreakerConsumerApplication

package com.mindasoft;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient // Eureka Discovery Client 标识
@SpringBootApplication
public class SpringCloudCircuitBreakerConsumerApplication {

    // 开启负载均衡的功能
    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudCircuitBreakerConsumerApplication.class, args);
    }

}

我们将远程调用服务,独立出来。
ProviderService

public interface ProviderService {

    String hello();
}

package com.mindasoft.serivce;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * 功能描述:
 *
 * @author huangmin
 * @date 2022/3/1 10:04 上午
 */
@Service
public class ProviderServiceImpl implements ProviderService {
    @Autowired
    private RestTemplate restTemplate; // HTTP 访问操作类

    @Override
    @CircuitBreaker(name = "providerService", fallbackMethod = "fallback")
    public String hello() {
        String providerMsg = restTemplate.getForObject("http://PROVIDER-SERVICE/hello", String.class);
        return providerMsg;
    }

    private String fallback(Exception ex) {
        return "Recovered: " + ex.toString();
    }

}

ConsumerController也需要修改一下:

@RestController
public class ConsumerController {

    private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerController.class);

    @Autowired
    private ProviderService providerService;

    @RequestMapping("/hello")
    public String hello() {

        String providerMsg = providerService.hello();
        return "Hello,I'm Customer! msg from provider : <br/><br/> " + providerMsg;
    }

}

改造完成,然后启动服务,访问hello时,得到以下结果:

# 第一次访问:
Hello,I'm Customer! msg from provider :

Recovered: java.lang.IllegalStateException: No instances available for PROVIDER-SERVICE

#第二次访问:
Hello,I'm Customer! msg from provider :

Recovered: io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'providerService' is OPEN and does not permit further calls

其他功能

resilience4j还有其他功能,如断路器一样使用注解@Retry、@Bulkhead、@RateLimiter、@Timelimiter,简单的配置如下。

resilience4j.retry:
    configs:
        default:
            maxAttempts: 3
            waitDuration: 100
    instances:
        providerService:
            baseConfig: default

resilience4j.bulkhead:
    configs:
        default:
            maxConcurrentCalls: 100
    instances:
        providerService:
            maxWaitDuration: 10ms
            maxConcurrentCalls: 20

resilience4j.thread-pool-bulkhead:
    configs:
        default:
            maxThreadPoolSize: 4
            coreThreadPoolSize: 2
            queueCapacity: 2
    instances:
        providerService:
            baseConfig: default
        providerServiceB:
            maxThreadPoolSize: 1
            coreThreadPoolSize: 1
            queueCapacity: 1

resilience4j.ratelimiter:
    configs:
        default:
            registerHealthIndicator: false
            limitForPeriod: 10
            limitRefreshPeriod: 1s
            timeoutDuration: 0
            eventConsumerBufferSize: 100
    instances:
        providerService:
            baseConfig: default
        providerServiceB:
            limitForPeriod: 6
            limitRefreshPeriod: 500ms
            timeoutDuration: 3s

resilience4j.timelimiter:
    configs:
        default:
            cancelRunningFuture: false
            timeoutDuration: 2s
    instances:
        providerService:
            baseConfig: default

posted @ 2022-03-09 23:09  技术那点事儿  阅读(604)  评论(0编辑  收藏  举报