Spring Boot - 利用Resilience4j-RateLimiter进行流量控制和服务降级
Resilience4J 是一个针对 Java 8 应用程序的轻量级容错和弹性库。它设计用于在分布式系统中的服务之间提供弹性和容错性。Resilience4J 的名字来源于它提供的核心功能,即让系统(服务)能够“弹性”(resilient)地应对各种失败情况,包括网络问题、第三方服务故障等。
Resilience4J 提供了以下功能:
Resilience4J 的一大特点是它的轻量级特性,它只使用了 Vavr 库(一个函数式编程库),没有其他外部库依赖。这使得它在集成到现有系统时非常方便,且性能开销小。
Resilience4J 设计上易于配置,支持通过代码、配置文件或运行时参数进行配置。它也支持通过 actuator 模块与 Spring Boot 的监控和管理特性集成。
由于 Resilience4J 的这些特性和优势,它在现代分布式系统和微服务架构中得到了广泛应用,尤其是在需要高可用性和弹性的环境中。
https://resilience4j.readme.io/
https://github.com/resilience4j/resilience4j
Resilience4j中的CircuitBreaker是一个核心工具,用于保护分布式系统免受故障的影响。
其工作原理主要通过一个有限状态机实现,包括CLOSED(关闭)、OPEN(打开)和HALF-OPEN(半开)三种状态。
Resilience4j 是一个为Java 8和函数式编程设计的轻量级容错库。它的主要目的是帮助开发者在分布式系统中实现弹性和容错性。
Resilience4j 提供了多种容错机制,包括断路器(CircuitBreaker)、限流器(RateLimiter)、重试(Retry)、隔离策略(Bulkhead)和超时控制(TimeLimiter)等。其中,CircuitBreaker 是 Resilience4j 的一个重要组成部分。
CircuitBreaker,即断路器,其设计原理来源于电路中的断路器,当电流超过设定值时,断路器会自动断开,以保护电路免受过大电流的损害。在软件系统中,断路器用于保护系统免受某个组件或服务故障的影响。
Resilience4j 的 CircuitBreaker 实现原理如下:
断路器的状态:CircuitBreaker 具有三种正常状态:CLOSED(关闭)、OPEN(打开)和 HALFOPEN(半开),以及两个特殊状态:DISABLED(禁用)和 FORCEDOPEN(强制打开)。这些状态通过有限状态机进行管理。
打开和关闭逻辑:当被保护的服务或资源发生故障或长时间不可用时,断路器会迅速切换到 OPEN 状态,阻止更多的请求发送到该服务或资源。在 OPEN 状态下,系统会定期发送测试请求,以检查故障是否已经解决。如果测试请求成功,断路器会切换到 HALFOPEN 状态,允许一个请求发送到该服务或资源。如果这个请求成功,断路器会切换到 CLOSED 状态,否则会重新切换到 OPEN 状态。
故障率计算:为了判断是否打开断路器,需要收集一定数量的请求数据。在 Resilience4j 中,需要至少填充一个环形缓冲区(Ring Bit Buffer),才能开始计算故障率。环形缓冲区的大小决定了需要多少次请求才能进行故障率的计算。
环形缓冲区:Resilience4j 使用环形缓冲区来存储请求状态的数据结构,这与 Hystrix 使用的滑动窗口不同。环形缓冲区使用位集合(BitSet)实现,每个位代表一个请求的状态(成功或失败)。环形缓冲区的大小决定了能够存储的请求数量。例如,一个大小为 10 的缓冲区可以存储 1024 个请求状态。
配置选项:Resilience4j 提供了丰富的配置选项,如故障率阈值、打开状态下的等待时间、半开状态下允许的最大请求数等,开发者可以根据需求进行灵活配置。
通过上述原理,Resilience4j 的 CircuitBreaker 能够有效地保护分布式系统免受故障的影响,提高系统的可用性和健壮性。
服务 A 调用服务 B,但不幸的是,服务 B 不可用或无法响应。因此,服务 A 可能会等待服务 B 的响应或处理遇到的异常。后续针对服务 B 的请求也会遇到类似的挑战,从而导致糟糕的用户体验。
在这种情况下,断路器可以通过在特定时间内停止请求发送,等待超时结束,启用有限数量的请求来检查服务 B 是否正常工作。如果这些请求成功,微服务就可以继续正常运行。如果没有,它将再次开始超时。
断路器具有三种状态:Closed、Open 和 Half_Open。
有 2 个服务,名为地址服务和订单服务
首先构建地址服务,因为它是一个依赖服务.
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>3.0.5version>
<relativePath/>
parent>
<groupId>com.edugroupId>
<artifactId>address-serviceartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>address-servicename>
<properties>
<java.version>17java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.h2databasegroupId>
<artifactId>h2artifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
package com.edu.addressservice.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "addresses")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String postalCode;
private String state;
private String city;
}
package com.edu.addressservice.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.edu.addressservice.model.Address;
@Repository
public interface AddressRepository extends JpaRepository<Address, Integer> {
Optional<Address> findByPostalCode(String postalCode);
}
public interface AddressService {
Address getAddressByPostalCode(String postalCode);
}
@Service
public class AddressServiceImpl implements AddressService {
@Autowired
private AddressRepository addressRepository;
public Address getAddressByPostalCode(String postalCode) {
return addressRepository.findByPostalCode(postalCode)
.orElseThrow(() -> new RuntimeException("Address Not Found: " + postalCode));
}
}
package com.edu.addressservice.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.edu.addressservice.model.Address;
import com.edu.addressservice.service.AddressService;
@RestController
@RequestMapping("addresses")
public class AddressController {
@Autowired
private AddressService addressService;
@GetMapping("/{postalCode}")
public Address getAddressByPostalCode(@PathVariable("postalCode") String postalCode) {
return addressService.getAddressByPostalCode(postalCode);
}
}
用@PostConstruct
注解的方法。 Spring 将在初始化 bean 属性并填充数据后调用该方法。
package com.edu.addressservice.config;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import com.edu.addressservice.model.Address;
import com.edu.addressservice.repository.AddressRepository;
import jakarta.annotation.PostConstruct;
@Configuration
public class DataSetup {
@Autowired
private AddressRepository addressRepository;
@PostConstruct
public void setupData() {
addressRepository.saveAll(Arrays.asList(
Address.builder().id(1).postalCode("1000001").state("SD").city("JN")
.build(),
Address.builder().id(2).postalCode("1100000").state("JS").city("NJ").build(),
Address.builder().id(3).postalCode("2100001").state("ZJ").city("HZ")
.build()));
}
}
server:
port: 9090
spring:
application:
name: address-service
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
datasource:
url: jdbc:h2:mem:address-db
username: admin
password: 1111
driverClassName: org.h2.Driver
h2:
console:
enabled: true
运行并访问链接http://localhost:9090/addresses/1000001
,预期响应应如下所示
这样就完成了地址服务的构建。
重点关注是如何配置断路器,并通过执行器监控其状态的。
public interface Type {
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "orders")
public class Order implements Type {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
private String orderNumber;
private String postalCode;
private String shippingState;
private String shippingCity;
}
@Data
public class Failure implements Type {
private final String msg;
}
package com.edu.orderservice.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.edu.orderservice.model.Order;
@Repository
public interface OrderRepository extends JpaRepository<Order, Integer> {
Optional<Order> findByOrderNumber(String orderNumber);
}
业务逻辑都在这里了。
如何调用外部API---------------------> Spring 提供的 RestTemplate
package com.edu.orderservice.service;
import com.edu.orderservice.model.Type;
public interface OrderService {
Type getOrderByPostCode(String orderNumber);
}
package com.edu.orderservice.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.edu.orderservice.dto.AddressDTO;
import com.edu.orderservice.model.Failure;
import com.edu.orderservice.model.Order;
import com.edu.orderservice.model.Type;
import com.edu.orderservice.repository.OrderRepository;
import com.edu.orderservice.service.OrderService;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private RestTemplate restTemplate;
private static final String SERVICE_NAME = "order-service";
private static final String ADDRESS_SERVICE_URL = "http://localhost:9090/addresses/";
@Override
@CircuitBreaker(name = SERVICE_NAME, fallbackMethod = "fallbackMethod")
public Type getOrderByPostCode(String orderNumber) {
Order order = orderRepository.findByOrderNumber(orderNumber)
.orElseThrow(() -> new RuntimeException("Order Not Found: " + orderNumber));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<AddressDTO> entity = new HttpEntity<>(null, headers);
ResponseEntity<AddressDTO> response = restTemplate.exchange(
(ADDRESS_SERVICE_URL + order.getPostalCode()), HttpMethod.GET, entity,
AddressDTO.class);
AddressDTO addressDTO = response.getBody();
if (addressDTO != null) {
order.setShippingState(addressDTO.getState());
order.setShippingCity(addressDTO.getCity());
}
return order;
}
private Type fallbackMethod(Exception e) {
return new Failure("Address service is not responding properly");
}
}
@CircuitBreaker
属性
name
”被分配为“order-service”,表名“order-service”实例的每个配置都适用于该方法。fallbackMethod
”属性,目的是在依赖服务(地址服务)未正确响应时调用降级方法。@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
package com.edu.orderservice.config;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import com.edu.orderservice.model.Order;
import com.edu.orderservice.repository.OrderRepository;
import jakarta.annotation.PostConstruct;
@Configuration
public class DataSetup {
@Autowired
private OrderRepository orderRepository;
@PostConstruct
public void setupData() {
orderRepository.saveAll(Arrays.asList(
Order.builder().id(1).orderNumber("0c70c0c2").postalCode("1000001").build(),
Order.builder().id(2).orderNumber("7f8f9f15").postalCode("1100000").build(),
Order.builder().id(3).orderNumber("394627b2").postalCode("2100001").build()));
}
}
server:
port: 1010
spring:
application:
name: order-service
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
datasource:
url: jdbc:h2:mem:order-db
username: root
password: 123
driverClassName: org.h2.Driver
h2:
console:
enabled: true
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: health
health:
circuitbreakers:
enabled: true
resilience4j:
circuitbreaker:
instances:
order-service:
sliding-window-type: COUNT_BASED
failure-rate-threshold: 50
minimum-number-of-calls: 5
automatic-transition-from-open-to-half-open-enabled: true
wait-duration-in-open-state: 5s
permitted-number-of-calls-in-half-open-state: 3
sliding-window-size: 10
register-health-indicator: true
配置中是针对 Resilience4j
库的配置
下面是对 order-service
断路器实例每个配置项的解释:
COUNT_BASED
(基于次数)和 TIME_BASED
(基于时间)。综上所述,这些配置定义了 order-service
断路器的行为。断路器将监控故障率和调用次数,以确定何时打开和何时过渡到半开放状态,为系统提供一种自我保护机制,以防止级联失败。
依次运行address-service和order-service,访问链接http://localhost:1010/orders?orderNumber=0c70c0c2
如果地址服务未正确响应(服务已关闭),我们将收到以下响应
这样完成了订单服务的构建 。
两个服务确保都已运行,访问链接 http://localhost:1010/actuator/health
查看断路器详细信息
我们可以看到以下几个关键配置:
bufferedCalls
(缓冲调用次数)为0,这意味着从order-service到address-service没有发起任何API调用。failedCalls
(失败调用次数)也为0,这表示所有调用都成功了,没有失败的调用。failureRate
(失败率)为-1.0%,这是一个异常的值,因为失败率不可能是负数。通常,失败率应该是成功率的一部分,即0%表示100%的成功率。这里的情况可能是因为计算失败率时使用了0作为分母,导致了负数的产生。state
(状态)为"CLOSED"(关闭),这表明circuitBreaker(断路器)目前是关闭的,没有触发熔断机制。调用order-service API 2次http://localhost:1010/orders?orderNumber=0c70c0c2
,
关闭address-service,调用order-service API 3次http://localhost:1010/orders?orderNumber=0c70c0c2
,然后刷新执行器链接
我们注意到Circuit Breaker被触发。原因是“failureRate”现在大于“failure-rate-threshold”
HALF_OPEN状态下,允许““permitted-number-of-calls-in-half-open-state”请求(我们将其值配置为3),然后再次计算失败率,如果失败率仍然大于“failure-rate-threshold”,断路器将再次被触发。
继续调用 order-service API 3 次 http://localhost:1010/orders?orderNumber=0c70c0c2
,然后刷新执行器链接
运行address-service,然后继续调用order-service API 3次http://localhost:1010/orders?orderNumber=0c70c0c2
,刷新执行器链接
断路器已关闭 。
Resilience4j的circuit breaker模式特别有用,它能够在服务调用失败达到一定次数后,自动断开电路,避免进一步的调用,从而保护应用程序不受故障服务的拖累。当服务恢复后,电路会重新闭合,允许正常的调用再次发生。