【微服务】Feign 整合 Sentinel,深入探索 Sentinel 的隔离和熔断降级规则,以及授权规则和自定义异常返回结果

文章目录

  • 前言
  • [一、Feign 整合 Sentinel](#一、Feign 整合 Sentinel)
    • [1.1 实现步骤](#1.1 实现步骤)
    • [1.2 FallbackFactory 示例](#1.2 FallbackFactory 示例)
  • [二、Sentinel 实现隔离](#二、Sentinel 实现隔离)
    • [2.1 隔离的实现方法](#2.1 隔离的实现方法)
    • [2.2 Sentinel 实现线程隔离示例](#2.2 Sentinel 实现线程隔离示例)
  • 三、熔断降级规则
    • [3.1 熔断降级原理及其流程](#3.1 熔断降级原理及其流程)
    • [3.2 熔断策略 ------ 慢调用](#3.2 熔断策略 —— 慢调用)
    • [3.3 熔断策略 ------ 异常比例和异常数](#3.3 熔断策略 —— 异常比例和异常数)
  • 四、授权规则
    • [4.1 什么是授权规则](#4.1 什么是授权规则)
    • [4.2 授权规则示例](#4.2 授权规则示例)
  • 五、自定义异常返回结果

前言

在前文中,介绍了 Sentinel 的流控模式和流控效果,然而限流只是一种预防措施,虽然可以尽量避免因为并发问题而引起的服务故障,但服务仍然可能因其他因素而发生故障。为了将这些故障控制在一定范围内,以避免雪崩效应的发生,我们需要依赖线程隔离(舱壁模式)和熔断降级机制。

无论是线程隔离还是熔断降级,它们都是为了保护客户端(调用方)免受服务故障的影响。

在微服务之间的调用通常依赖于 Open Feign,因此我们首先需要将Feign与 Sentinel进行有效整合。

本文将探讨 Feign 如何与 Sentinel 整合,以及 Sentinel 的隔离、熔断降级规则以及授权规则等关键概念。

一、Feign 整合 Sentinel

1.1 实现步骤

在 Spring Cloud 中,微服务之间的调用通常依赖于 Feign 来实现。要在微服务架构中保护客户端,需要将 Feign 和 Sentinel 整合在一起。以下是将 Feign 与 Sentinel 整合的步骤,以一个名为 cloud-demo 的微服务案例为例:

1. 修改 order-serviceapplication.yml 文件,启用 Feign 对 Sentinel 的支持:

yaml 复制代码
feign:
  sentinel:
    enabled: true # 启用 Feign 对 Sentinel 的支持

通过这个配置,我们告诉 Feign 在进行远程调用时要与 Sentinel 一起工作,以确保客户端受到适当的保护。

2. 编写调用失败后的降级逻辑:

当远程调用失败时,可以实现降级逻辑。有两种方式可供选择:

  • 方式一:FallbackClass,FallbackClass 是 Feign 的一种直接降级处理机制。它涉及创建一个实现原始Feign接口的类,并在该类的方法中定义降级逻辑。但是这种方式对远程调用的异常无法进行处理。
  • 方式二:FallbackFactory(降级工厂),FallbackFactory 提供了更灵活的处理远程服务调用失败的方式。它允许我们动态创建 Feign 接口的降级实例,并获取特定的异常信息。

1.2 FallbackFactory 示例

在接下来的部分,我们将深入研究如何使用 FallbackFactory 来处理远程调用的异常,并为客户端提供更好的降级体验。

步骤一 :在 feign-api 模块中创建一个UserClientFallbackFactory类,实现FallbackFactory接口,并重写 create 方法:

java 复制代码
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable throwable) {
        return new UserClient() {
            @Override
            public User findById(Long id) {
                log.error("查询用户失败!", throwable);
                return new User();
            }
        };
    }
}

这段代码将在 order-service 调用 user-service 失败后自动调用,在控制台会输出错误日志,并返回一个空的 User 对象。

步骤二 :在 config 中将 UserClientFallbackFactory 类注册为一个 Bean:

java 复制代码
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
    return new UserClientFallbackFactory();
}

步骤三 :在 feignUserClient 接口的@FeignClient 注解中指定 fallbackFactory UserClientFallbackFactory

java 复制代码
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class) 
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

通过这些步骤,就可以使用 FallbackFactory 处理远程服务调用失败,捕获异常信息,并提供更好的降级体验。

二、Sentinel 实现隔离

在 Sentinel 中,隔离有两种主要实现方法,即信号量隔离和线程隔离。其中默认采用的是信号量隔离:

2.1 隔离的实现方法

信号量隔离:

信号量隔离是一种资源隔离方式,它通过设置每个资源(或接口)的许可证数量来控制并发访问。当并发请求到达时,如果资源的许可证数量已经用尽,新的请求将被阻塞,以保护资源不被过度访问。信号量隔离适用于需要控制并发访问的场景,例如数据库连接、外部API调用等。

优点

  • 轻量级,无额外开销
  • 适用于高频调用和高扇出场景

缺点

  • 不支持主动超时
  • 不支持异步调用

场景:适用于高频调用和高扇出的场景,其中资源隔离较为轻量。

线程隔离:

线程隔离是一种资源隔离方式,它将不同的资源请求隔离到不同的线程池中执行,以确保它们不会相互影响。每个线程池负责执行特定资源的请求,如果一个请求由于某种原因导致线程阻塞或异常,不会影响其他资源的请求。线程隔离适用于需要独立线程执行的场景,例如耗时操作、阻塞调用等。

优点

  • 支持主动超时
  • 支持异步调用

缺点

  • 线程的额外开销较大

场景:线程隔离适用于低扇出场景,其中资源隔离更为重要,或需要支持异步调用和主动超时的情况。

选择隔离的实现方法取决于具体需求和应用场景。根据不同的场景和资源调用特点,可以灵活选择信号量隔离或线程隔离,以保障系统的稳定性和性能。

2.2 Sentinel 实现线程隔离示例

回顾在使用 Sentinel 控制台设置限流规则的时候,发现有两种阈值类型:

  • QPS: 就是每秒的请求数,在快速入门中已经演示过
  • 线程数: 是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现舱壁模式。

下面是一个示例,演示如何在 Sentinel 的控制台中实现线程隔离的设置。

1. 给 UserClient 的查询用户接口设置流控规则,线程数不能超过 2。

2. 然后利用 JMeter 测试。

设置线程数为 10:

设置 HTTP 请求:

启动 JMeter:

可以发现最终 10 个线程的请求只通过了其中两个。

查看 Sentinel 控制台的实时监控:

三、熔断降级规则

3.1 熔断降级原理及其流程

熔断降级原理:

熔断降级是解决雪崩问题的重要手段,其原理是由断路器统计服务调用的异常比例和慢请求比例,如果超出阈值则会熔断该服务。具体原理如下:

  1. 关闭状态(Closed):初始状态下,断路器处于关闭状态,所有请求都会被允许访问服务。
  2. 熔断状态(Open):当服务调用的异常比例或慢请求比例超出阈值时,断路器会进入熔断状态,拦截一定比例的请求,这些请求将快速失败,不会真正访问服务。
  3. 半开状态(Half-Open):在一段时间后,断路器会进入半开状态,允许部分请求访问服务,用于测试服务是否已经恢复正常。
  4. 关闭状态(Closed)或继续熔断状态(Open):根据半开状态下的请求成功与否,断路器会决定是继续保持熔断状态还是恢复到关闭状态。

熔断降级流程:

熔断降级的流程如下图所示:

流程说明如下:

  1. 断路器初始处于 Closed 状态,允许所有请求访问服务。
  2. 当服务调用失败次数或慢请求比例达到阈值,断路器进入 Open 状态,拦截所有请求,快速失败。
  3. 在一段时间后,断路器进入 Half-Open 状态,允许部分请求访问服务,用于测试服务是否已经恢复正常。
  4. 如果在 Half-Open 状态下的请求成功,则断路器进入 Closed 状态,允许所有请求访问服务。
  5. 如果在 Half-Open 状态下的请求仍然失败,断路器继续保持 Open 状态,直到下一次尝试进入 Half-Open 状态。

熔断降级通过这种状态机实现,可以帮助服务在异常情况下避免雪崩效应,提高系统的可用性和稳定性。

3.2 熔断策略 ------ 慢调用

断路器熔断策略有三种:慢调用、异常比例、异常数。

慢调用就是当业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。

例如,通过 Sentinel 控制台设置慢调用降级策略:

说明:

当 RT 超过 500ms 的调用就是慢调用,统计最近 10000ms 内的请求,如果请求量超过 5 次,并且慢调用比例不低于 0.5,则触发熔断,熔断时长为 5 秒。然后进入 Half-open 状态,放行一次请求做测试。

现在有一个需求:就是给 UserClient 的查询用户接口设置降级规则,慢调用的 RT 阈值为 50ms,统计时间为 1 秒,最小请求数量为 5,失败阈值比例为 0.4,熔断时长为 5:

为了触发慢调用规则,我们需要修改UserService中的业务,增加业务耗时:

java 复制代码
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id ) throws InterruptedException {
    if(id == 1){
        // 休眠,触发慢调用熔断策略
        Thread.sleep(60);
    }
    return userService.queryById(id);
}

重启 user-service 服务后,我们可以通过快速刷新浏览器,来触发这个熔断机制:

当触发了容器之后,服务其他 ID 的接口,也会被熔断:

3.3 熔断策略 ------ 异常比例和异常数

异常比例或异常数都是统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。

同样可以通过 Sentinel 控制台进行设置:

说明:

统计最近 1000ms 内的请求,如果请求量超过 10 次,并且异常比例不低于 0.5,则触发熔断,熔断时长为5秒。然后进入Half-open状态,放行一次请求做测试。

下面以异常比例为例:

例如现在有一个需求:就是给 UserClient 的查询用户接口设置降级规则,统计时间为 1 秒,最小请求数量为 5,失败阈值比例为 0.4,熔断时长为 5s:

为了触发异常统计,同样需要修改UserService中的业务,抛出异常:

java 复制代码
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
    if(id == 1){
        // 休眠,触发慢调用熔断策略
        Thread.sleep(60);
    } else if (id == 2) {
        throw new RuntimeException("演示触发异常比例熔断!");
    }
    return userService.queryById(id);
}

此时,访问 ID 为 2 的用户就会抛出异常。

重启 user-service 服务后,我们可以通过快速刷新浏览器,来触发这个熔断机制:

四、授权规则

4.1 什么是授权规则

授权规则用于对调用方的来源进行控制,通常分为白名单和黑名单两种方式:

  • 白名单:将指定的来源(origin)加入白名单,允许这些调用者访问服务。
  • 黑名单:将指定的来源(origin)加入黑名单,不允许这些调用者访问服务。

在 Sentinel 控制台中,可以配置授权规则,如下所示:

现在,我们可以从浏览器和网关两个路径去服务 order-service服务:

如果现在需要限定只允许从网关来的请求访问 order-service服务,那么流控应用中就填写网关的名称。

4.2 授权规则示例

下面将演示如何通过 Sentinel 的授权规则来限制对 order-service 服务的请求只能来自网关 gateway

Sentinel 是通过 RequestOriginParser 这个接口的 parseOrigin 来获取请求的来源的:

java 复制代码
public interface RequestOriginParser {    
	// 从请求request对象中获取origin,获取方式自定义
    String parseOrigin(HttpServletRequest request);
 }

RequestOriginParser 是 Sentinel 提供的接口,用于解析请求的来源(origin)。这接口定义了一个方法 parseOrigin,我们需要实现这个方法,以自定义方式从请求对象中获取请求的来源信息。

因此,我们尝试从request中获取一个名为origin的请求头,作为origin的值,作为判断请求是否来源于网关的依据。实现的步骤如下:

  1. 首先,在gateway服务的 application.yml 文件中,利用网关的过滤器给所有的请求都添加一个名为 gatewayorigin 请求头:
yml 复制代码
spring:
  cloud:
    gateway:
      default-filters: # 默认过滤器,会对所有的路由请求都生效
        - AddRequestHeader=origin, gateway # Sentinel 授权规则,只有从网关服务的才合法,通过添加请求头标识
  1. 然后在 Sentinel 控制台中添加授权规则,指定流控应用为 gateway
  1. order-service 中实现一个 HeadOriginParser 类,实现 RequestOriginParser 接口,用来获取请求源:
java 复制代码
@Component
public class HeadOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest httpServletRequest) {
        // 1. 获取请求头
        String origin = httpServletRequest.getHeader("origin");
        // 2. 非空判断
        if (StringUtils.isEmpty(origin)) {
            return "blank";
        }
        return origin;
    }
}

如果不存在origin 这个字段的请求头,直接返回"blank",否则直接返回 origin 的值,然后交给 Sentinel 进行判断请求源。

  1. 重启 order-servicegateway 服务,进行演示:

此时,如果我们之间通过浏览器访问 order-service 的接口,发现就被禁止访问了:

如果通过 gateway 网关,就能够成功访问了:

五、自定义异常返回结果

默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方,但是通过上面所有的例子,我们发现都只返回了一种结果,那就是"Blocked by Sentinel (flow limiting)"。如果我们想要知道微服务调用失败的具体原因,就需要对异常进行自定义处理:

如果要实现对 Sentinel 的异常实现自定义,那么就需要实现 BlockExceptionHandler 接口,它是 Sentinel 提供的一个接口,用于自定义处理调用失败时的异常情况。

而关于 BlockException这个类,包含很多个子类,分别对应不同的场景:

异常 说明
FlowException 限流异常
ParamFlowException 热点参数限流的异常
DegradeException 降级异常
AuthorityException 授权规则异常
SystemBlockException 系统规则异常

我们可以通过这些子类来判断在调用微服务失败的时候,具体出现了哪种情况,然后通过自定义异常进行结果的返回,下面是一个实现自定义异常的代码示例:

java 复制代码
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        String msg = "未知异常";
        int status = 429;

        if (e instanceof FlowException) {
            msg = "请求被限流了";
        } else if (e instanceof ParamFlowException) {
            msg = "请求被热点参数限流";
        } else if (e instanceof DegradeException) {
            msg = "请求被降级了";
        } else if (e instanceof AuthorityException) {
            msg = "没有权限访问";
            status = 401;
        }

        response.setContentType("application/json;charset=utf-8");
        response.setStatus(status);
        response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
    }
}

在上述示例中,创建了一个名为 CustomBlockExceptionHandler 的自定义异常处理类,实现了 BlockExceptionHandler 接口。在重写的 handle 方法中,根据不同的 BlockException 类型来确定异常消息和状态码,然后将这些信息返回给调用方。

例如,在上面配置了授权规则的情况下,直接通过浏览器访问 order-service 服务:

此时,就能够清楚的知道微服务调用失败的原因了。

相关推荐
吾日三省吾码4 小时前
JVM 性能调优
java
弗拉唐5 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi776 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3436 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀6 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20206 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深6 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
shuangrenlong7 小时前
slice介绍slice查看器
java·ubuntu
牧竹子7 小时前
对原jar包解压后修改原class文件后重新打包为jar
java·jar