SpringCloud 微服务实战7 - SpringCloud Alibaba Sentinel 实现熔断与限流

一、Sentinel简介

1、Sentinel 是什么

官网: https://github.com/alibaba/Sentinel/

中文wiki:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

我们之前讲解过 Hystrix ,它也实现了降级与熔断,但是它有如下缺点:

  • 需要我们程序员自己手动搭建监控平台
  • 没有一套 web 界面可以给我们进行更加细粒度话的配置:流控、速率控制、服务熔断、服务降级。。。。。。

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性:

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
  • 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

2、Sentinel 能干嘛

二、安装 Sentinel 控制台

Sentinel 由两部分组成:后台、前台8080。

Sentinel 分为两个部分:

  • 和辛苦(Java 客户端)不依赖任何框架/库,能够运行于所有 java 运行时环境,同时对 Dubbo/Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 SpringBoot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

1、安装步骤

下载

下载地址:https://github.com/alibaba/Sentinel/releases ,目前最新版本为 1.8.2

运行

前提:Java8 环境OK、8080端口不能被占用

输入以下命令:

java -jar sentinel-dashboard-1.8.2.jar

出现如下界面说明启动成功 

访问

默认 Sentinel 用户名和密码: sentinel 、sentinel

在浏览器输入 http://localhost:8080/ 

三、初始化演示工程

启动 Nacos 8848。

新建 Module: cloudalibaba-sentinel-service8401

pom

    <dependencies>
        <!-- alibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- alibaba sentinel-datasource-nacos,后续持久化用到 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <!-- alibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!-- openfeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 引入自己的 cloud-api-commons 模块-->
        <dependency>
            <groupId>com.cloud.study</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- SpringBoot 整合 web组件+actuator -->
        <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>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

yaml

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #nacos 服务注册中心
    sentinel:
      transport:
        dashboard: localhost:8080  #sentinel dashboard 地址
        port: 8719   #默认 8719端口,假如被占用会自动从 8719开始依次+1扫描,直至找未被占用的端口

#暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include:  "*"

主启动

@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
    public static void main(String[] args) {
        SpringApplication.run(MainApp8401.class, args);
    }
}

业务测试类

@RestController
public class FlowLimitController {

    @GetMapping(value = "/testA")
    public String testA(){
        return "-------testA";
    }

    @GetMapping(value = "/testB")
    public String testB(){
        return "-------testB";
    }
}

启动 8401 

启动 Sentinel 8080

此时 微服务 8401 已经入驻进 Nacos

进入 Sentinel Dashboard 空空如也,这是因为 Sentinel 采用的懒加载,只有访问页面的时候才会出现。分别访问 http://localhost:8401/testA、http://localhost:8401/testB 

四、流控制规则

1、基本介绍

资源名:唯一名称,默认请求路径。

针对来源: Sentinel 可以针对调用者进行限流,填写微服务名,默认 default(不区分来源)。

阈值类型/单机阈值:

  • QPS(每秒请求数量):当调用该 api 的 QPS 达到阈值的时候,进行限流。
  • 线程数:当调用该 api 的线程数量达到阈值的时候,进行限流。

是否集群:不需要集群。

流控模式:

  • 直接:api 达到限流条件时,直接限流。
  • 关联:当关联的资源达到阈值时,就限流自己。
  • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api 级别的针对来源】

流控效果:

  • 快速失败:直接失败,抛异常。
  • warm up:根据 codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
  • 排队等待:匀速排队,让请求以均匀的速度通过,阈值类型必须设置为 QPS,否则无效。

2、流控-QPS直接失败

上图表示一秒钟内查询1次OK(http://localhost:8401/testA),若超过一次就直接快速失败,报默认错误。

 

思考:现在限流的出错页面是系统自带了,我们能不能使用自己自定义的出错页面?? 

3、流控-线程数直接失败

修改之前的流控配置,将阈值类型改成:并发线程数。

上图表示当处理线程数是1时OK,当处理线程数超过1时,报默认错误。

此时,快速的刷新 http://localhost:8401/testA 页面,并不会报错。因为此时1个线程可以处理我们的请求。

我们修改下 controller  ,增加延时1秒内只能处理一个请求

    @GetMapping(value = "/testA")
    public String testA() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        return "-------testA";
    }

此时打开两个 http://localhost:8401/testA 页面,并快速的刷新,就会出现报错。

4、流控-关联

当关联的资源达到阈值时,就限流自己。

比如我们的例子中,当与testA关联的testB达到阈值后,就限流A自己。

设置效果:当关联资源 testB 的qps 阈值超过1时,就限流 testA的访问。

测试

使用 postMan 新建一个请求,测试后能正常访问

点击小图片出现run按钮界面

点击 run按钮,配置循环次数和时间间隔

此时点击 【run测试】按钮 ,立刻访问 testA,发现A 被限流。

等过了一会 postMan 跑完之后,testA又可以正常访问。

由此可以总结出:当testB达到阈值后,testA被限制了。 

5、流控-预热

公式:阈值除以 coldFactor(默认值为3),经过预热时长后才会达到阈值。

Warm Up方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过“冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

coldFactor 默认值为3,即请求QPS从 【阈值/3】 开始,经过预热时长设定置 QPS 阈值。

例如下图,就表示  testA 刚刚开始时,阈值为 3(10/3),阈值慢慢增加,等到5秒后,阈值为10。

我们按照上图进行设置,一直快速刷新 http://localhost:8401/testA 页面。刚开始的时候会出现限流的报错页面,后来慢慢的页面出现的次数少了,等5秒后,就不会再出现报错页面了。

应用场景:如秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阈值。

6、流控-排队等待

匀速排队,让请求以均匀的速度通过,阈值类型必须设置成 QPS,否则无效。

设置含义: /testA 每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒。

五、降级规则

1.基本介绍

熔断降级 · alibaba/Sentinel Wiki · GitHub

Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或者异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他资源而导致级联错误。

当资源被迫降级后,在接下来的降级时间窗口期内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)。

注意,Sentinel 的断路器是没有半开状态的。半开的状态由系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器。具体可参考 Hystrix。

Sentinel 提供以下几种熔断策略:

  •  慢调用比例(SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT即最大响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长请求数目大于设置的最小请求数,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF_OPEN),若接下来的一个请求小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用则会再次熔断。

  • 异常比例(Error_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF_OPEN),若接下来的请求成功完成(没有错误)则结束熔断,否则会再次熔断。异常比例的阈值范围是【0.0 ~ 1.0】,代表 0% ~ 100%。

  • 异常数(ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

 注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常。

2、降级策略实战 - 慢调用比例

在controller 中加入 testD

    @GetMapping(value = "/testD")
    public String testD(){
        //暂停一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "-------testD";
    }

配置降级策略:当1秒内有超过5次的请求,且RT超过200毫秒的请求占比超过50%,则会熔断3秒钟。

Jmeter 压力测试,1秒内有10个线程进行 testD 请求

结论:

当启动 Jemeter 的时候,浏览器访问 访问 test 会出现如下提示。

3、降级策略实战 - 异常比例

修改 testD 方法

    @GetMapping(value = "/testD")
    public String testD(){
        log.info("testD 测试异常比例");
        int age = 10/0;
        return "-------testD";
    }

 配置降级策略:当1秒内有超过5次的请求,且异常的比例超过 30%,则会熔断3秒钟。

Jmeter 压力测试,1秒内有10个线程进行 testD 请求,按照【3、降级策略实战 - 异常比例】进行配置。

当启动 Jemeter 的时候,浏览器访问 访问 test 会出现如下提示。

4、降级策略实战 - 异常数

 配置降级策略:当1秒内有超过5次的请求,且异常数量超过2个,则会熔断3秒钟。

Jmeter 压力测试,1秒内有10个线程进行 testD 请求,按照【3、降级策略实战 - 异常比例】进行配置。

当启动 Jemeter 的时候,浏览器访问 访问 test 会出现如下提示。

六、热点 key 限流

热点参数限流 · alibaba/Sentinel Wiki · GitHub

何为热点?热点即经常访问的数据。很多时候我们系统统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品ID 为参数,统计一段时间内最常购买的商品ID并进行限制。
  • 用户ID 为参数,针对一段时间内频繁访问的用户ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

兜底方法分为系统默认和用户自定义两种。之前我们测试的时候,都是用 sentinel 系统默认的提示:Blocked by Sentinel(flow limiting)。我们可以使用 @SentinelResource 注解来实现。

1.基本配置测试

下面来进行配置,首先修改 controller

    @GetMapping(value = "/testHotKey")
    @SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
    public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                             @RequestParam(value = "p2",required = false) String p2){
        return "------ testHotKey";
    }

    /**
     * 参数与 testHotKey 一致,并增加 BlockException
     */
    public String deal_testHotKey(String p1, String p2, BlockException exception){
        return "deal_testHotKey 繁忙";
    }

 sentinel 配置:testHotKey 资源,在1秒内超过1个一个请求(带p1参数),则会进行限流。

注意:资源名要是 @SentinelResource 的值,参数索引是从0开始,比如方法 

testHotKey(@RequestParam(value = "p1",required = false) String p1,@RequestParam(value = "p2",required = false) String p2)

第0个参数是 p1,第1个参数是p2。

测试:

  • 浏览器快速刷新 http://localhost:8401/testHotKey?p1=a&p2=b,多刷新几次就会出现如下错误。

  • 浏览器快速刷新不带p1参数的链接,比如 http://localhost:8401/testHotKey?p2=b,都能正常访问。

2、额外参数配置

上个案例演示了第一个参数 p1,当 QPS 超过1秒1次点击后马上被限流。我们期望 p1 参数,当它是某个特殊值的时候,它的限流和平时不一样。加入 p1 的值为 5 时,它的阀值可以达到200。

在 Sentinel 控制台进行配置

当快速刷新 http://localhost:8401/testHotKey?p1=1 时,会出现报错页面。而当快速刷新 http://localhost:8401/testHotKey?p1=5 时,页面正常。

注意:参数类型必须是基本数据类型和String

另外,当出现 运行时异常时,@SentinelResource 的 blockHandler 的兜底方法不会进入,会直接显示错误页面。

七、系统规则

热点参数限流 · alibaba/Sentinel Wiki · GitHub

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、cpu十一率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能在最大的吞吐量的同时保证系统整体的稳定性。

系统规则支持以下模式:

  • Load 自适应(仅对 Unix/linux 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps*minRt 估算得出。设定参考值一般是 CPU usage * 2.5 。
  • CPU usage(1.5.0+版本):当系统的 CPU 使用率超过阈值即触发系统保护(取值范围 0.0 - 1.0),比较灵敏。
  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的QPS达到阈值即触发系统保护。

比如我们配置如下规则,入口 QPS 阈值为 1 

此时快速系统中的任何链接(例如 testA、testB)时,都会触发限流。 

八、@SentinelResource

1、按资源名称限流 + 后续处理

1.1 修改 cloudalibaba-sentinel-service8401 模块

在 cloudalibaba-sentinel-service8401 模块中新增一个 controller

@RestController
@Slf4j
public class RateLimitController {
    @GetMapping("/byResource")
    @SentinelResource(value = "byResource", blockHandler = "handleException")
    public R byResource() {
        return new R(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
    }

    public R handleException(BlockException exception){
        return new R(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
    }
}

1.2 Sentinel 控制台配置

按照 @SentinelResource 中的资源名称配置我们的限流规则

1.3 测试

一秒点击一下(http://localhost:8401/byResource)OK。

超过上述,疯狂点击,返回了自己定义的限流处理信息,限流发生。

1.4 额外问题 

关闭服务 8401 后,过会后刷新 Sentinel Dashboard,发现我们配置的流控规则消失了。

2、按照 url 地址限流 + 后续处理

2.1 修改 cloudalibaba-sentinel-service8401 模块

在 cloudalibaba-sentinel-service8401 模块RateLimitController  增加 byUrl 方法

    @GetMapping("/byUrl")
    @SentinelResource(value = "byUrl")
    public R byUrl() {
        return new R(200, "按url限流测试OK", new Payment(2021L, "serial002"));
    }

2.2 Sentinel 控制台配置

我们在资源名中配置的是 url (/byUrl

2.3 测试 

一秒点击一下(http://localhost:8401/byUrl)OK。

超过上述,疯狂点击,返回了Sentinel自带的限流处理结果。

3、上面兜底方案面临的问题

系统默认的兜底方案,没有体现我们自己的业务要求。

依照现有条件,我们自定义的处理方案又和业务代码耦合在一起,不直观。

每个业务方法都添加一个兜底方法,代码会膨胀。

全局统一的处理方法没有体现。

4、客户自定义限流处理逻辑

4.1 创建 CustomerBlockHandler 类用于自定义限流处理逻辑

/**
 * 全局统一的兜底处理类
 */
public class CustomerBlockHandler {

    // 要求:
    // 1 当前方法的返回值和参数要跟原方法一致
    // 2 参数列表与原方法匹配,并在最后添加一个BlockException 类型参数
    // 3 需要用static修饰
    public static R handleException1(BlockException exception){
        return new R(444, "用户自定义,全局 handleException ---- 1");
    }

    // 要求:
    // 1 当前方法的返回值和参数要跟原方法一致
    // 2 参数列表与原方法匹配,并在最后添加一个BlockException 类型参数
    // 3 需要用static修饰
    public static R handleException2(BlockException exception){
        return new R(444, "用户自定义,全局 handleException ---- 2");
    }
}

4.2 RateLimitController 修改

@RestController
@Slf4j
public class RateLimitController {
    @GetMapping("/byResource")
    @SentinelResource(value = "byResource", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException1")
    public R byResource() {
        return new R(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
    }

    @GetMapping("/byUrl")
    @SentinelResource(value = "byUrl", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
    public R byUrl() {
        return new R(200, "按url限流测试OK", new Payment(2021L, "serial002"));
    }
}

4.3 Sentinel 控制台配置

byUrl使用的是按 url 配置,byResource 是按照资源名配置,如下: 

4.4 测试

疯狂点击 http://localhost:8401/byResource,返回了我们自定义的结果。

疯狂点击 http://localhost:8401/byUrl,返回了Sentinel自带的限流处理结果,这说明自定义处理方法只针对资源名称的配置才会生效

我们修改 byUrl 的 Sentinel 控制台配置,按照资源名称配置

 疯狂点击 http://localhost:8401/byUrl,出现了我们自定义的处理页面。

5、更多注解属性说明

https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81

Sentinel 主要由三个核心API:

  • SphU 定义资源
  • Tracer 定义统计
  • ContextUtil 定义了上下文

@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。@SentinelResource 注解包含以下属性:

  • value:资源名称,必需项
  • entryType:entry 类型,可选项(默认为 EntryType.OUT)
  • blockHandler/blockHandlerClass:blockHandler处理对应 BlockException 的函数名称,可选项。blockHandler 函数访问需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在容一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应类的 Class 对象。注意,对应的函数必需为 static 函数,否则无法解析。
  • fallback/fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有异常(除了 exceptionsToIgnore 里面排除的异常类型)进行处理。

fallback 函数签名和位置要求:

  • 返回值类型必须和原函数一致
  • 方法参数列表需要和原函数一致或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
  • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定fallbackClass 为对应类的 Class 对象,注意对应的函数必须为 static 函数,否则无法解析。
  • defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选性,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有异常的类型(除了exceptionsToIgnore 里面排除的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback ,则只有 fallback 会生效。

defaultFallback 函数签名要求:

  • 返回值类型必须和原函数一致
  • 方法参数列表需要 为空或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
  • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定fallbackClass 为对应类的 Class 对象,注意对应的函数必须为 static 函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

1.8.0 版本开始, defaultFallback 支持在类级别进行配置。1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,不能针对业务异常进行处理

特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallback 和 defaultFallback,则被限流降级时会将 BlockException 直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)。

九、服务熔断功能

sentinel 整合 Ribbon + openFeign + fallback

2、Ribbon 系列

启动 nacos 和 sentinel

提供者 9003/9004

9003和9004工程内容几乎一样,只是配置文件端口不一样

新建 module :cloudalibaba-provider-payment9003

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>cloud-study</artifactId>
        <groupId>com.cloud.study</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-provider-payment9003</artifactId>

    <dependencies>
        <!-- alibaba-nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 引入自己的 cloud-api-commons 模块-->
        <dependency>
            <groupId>com.cloud.study</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <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>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

 pom

server:
  port: 9003

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

#暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include:  "*"

主启动

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9003.class, args);
    }
}

业务类

@RestController
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    public static HashMap<Long, Payment> hashMap = new HashMap<>();
    static {
        hashMap.put(1L, new Payment(1L, "1213312qe123123123123111"));
        hashMap.put(2L, new Payment(2L, "1213312qe123123123123112"));
        hashMap.put(3L, new Payment(3L, "1213312qe123123123123113"));
    }

    @GetMapping("/paymentSQL/{id}")
    public R<Payment> paymentSQL(@PathVariable("id") Long id){
        Payment payment = hashMap.get(id);
        return new R<>(200,"from mysql, serverPort:" + serverPort, payment);
    }
}

消费者84 

新建 module:cloudalibaba-consumer-order84

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>cloud-study</artifactId>
        <groupId>com.cloud.study</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-consumer-order84</artifactId>

    <dependencies>
        <!-- alibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- alibaba sentinel-datasource-nacos,后续持久化用到 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <!-- alibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!-- openfeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 引入自己的 cloud-api-commons 模块-->
        <dependency>
            <groupId>com.cloud.study</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- SpringBoot 整合 web组件+actuator -->
        <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>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

yaml

server:
  port: 84

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #nacos 服务注册中心
    sentinel:
      transport:
        dashboard: localhost:8080  #sentinel dashboard 地址
        port: 8719   #默认 8719端口,假如被占用会自动从 8719开始依次+1扫描,直至找未被占用的端口

service-url:
  nacos-user-service: http://nacos-payment-provider

#暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include:  "*"

主启动

@SpringBootApplication
@EnableDiscoveryClient
public class OrderMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain84.class, args);
    }
}

 配置类

@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

controller

@RestController
@Slf4j
public class CircleBreakerController {
    @Value("${service-url.nacos-user-service}")
    private String SERVICE_URL;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback")
    public R<Payment> fallback(@PathVariable("id") Long id){
        if (id == 4) {
            throw new IllegalArgumentException("非法参数异常......");
        }
        R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
        if (r.getData() == null) {
            throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
        }
        return r;
    }
}

测试:

  • 访问 http://localhost:84/consumer/fallback/1 正常,刷新几次发现ribbon负载均衡是生效的
  • 访问 http://localhost:84/consumer/fallback/4 出现报错页面,不友好

消费者84 - @SentinelResource 只配置 fallback

fallback 只负责运行时异常

修改 controller 代码

@RestController
@Slf4j
public class CircleBreakerController {
    @Value("${service-url.nacos-user-service}")
    private String SERVICE_URL;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback", fallback = "handlerFallback")
    public R<Payment> fallback(@PathVariable("id") Long id){
        if (id == 4) {
            throw new IllegalArgumentException("非法参数异常......");
        }
        R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
        if (r.getData() == null) {
            throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
        }
        return r;
    }

    public R handlerFallback(@PathVariable("id") Long id, Throwable e) {
        return new R(444,"兜底异常 handlerFallback,exception 内容  " + e.getMessage(), new Payment(id, "null"));
    }
}

测试:

  • 访问 http://localhost:84/consumer/fallback/4 异常信息是我们自定义的

消费者84 - @SentinelResource 只配置 blockHandler

blockHandler 只负责 sentinel 控制台配置违规

修改 controller 代码

    @GetMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback", blockHandler = "blockHandler")
    public R<Payment> fallback(@PathVariable("id") Long id){
        if (id == 4) {
            throw new IllegalArgumentException("非法参数异常......");
        }
        R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
        if (r.getData() == null) {
            throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
        }
        return r;
    }

    public R blockHandler(@PathVariable("id") Long id, BlockException e) {
        return new R(444,"blockHandler-sentinel 限流,无此流水:BlockException  " + e.getMessage(), new Payment(id, "null"));
    }

在sentinel 控制台给 fallback 配置流控规则

测试(验证了 @SentinelResource的blockHandler 只负责 sentinel 控制台配置违规):

  • 访问一次 http://localhost:84/consumer/fallback/4 出现报错页面
  • 快速刷新几次 http://localhost:84/consumer/fallback/4 出现我们自定义的报错

消费者84 - @SentinelResource 配置 fallback、blockHandler 

修改 controller

    @GetMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")
    public R<Payment> fallback(@PathVariable("id") Long id){
        if (id == 4) {
            throw new IllegalArgumentException("非法参数异常......");
        }
        R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
        if (r.getData() == null) {
            throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
        }
        return r;
    }

    //fallback 兜底函数
    public R handlerFallback(@PathVariable("id") Long id, Throwable e) {
        return new R(444,"兜底异常 handlerFallback,exception 内容  " + e.getMessage(), new Payment(id, "null"));
    }

    // sentinel 配置违规兜底
    public R blockHandler(@PathVariable("id") Long id, BlockException e) {
        return new R(444,"blockHandler-sentinel 限流,无此流水:BlockException  " + e.getMessage(), new Payment(id, "null"));
    }

在sentinel 控制台给 fallback 配置流控规则 

测试:

  • 访问一次 http://localhost:84/consumer/fallback/4 出现fallback兜底函数信息

消费者84 - @SentinelResource 增加配置 exceptionsToIgnore  

修改controller,在 @SentinelResource 注解增加 exceptionsToIgnore 配置。它说明当出现 IllegalArgumentException 这个异常的时候,fallback 兜底方法不生效。 

@RestController
@Slf4j
public class CircleBreakerController {
    @Value("${service-url.nacos-user-service}")
    private String SERVICE_URL;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback", fallback = "handlerFallback",
            blockHandler = "blockHandler",
            exceptionsToIgnore = {IllegalArgumentException.class})
    public R<Payment> fallback(@PathVariable("id") Long id){
        if (id == 4) {
            throw new IllegalArgumentException("非法参数异常......");
        }
        R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
        if (r.getData() == null) {
            throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
        }
        return r;
    }

    //fallback 兜底函数
    public R handlerFallback(@PathVariable("id") Long id, Throwable e) {
        return new R(444,"兜底异常 handlerFallback,exception 内容  " + e.getMessage(), new Payment(id, "null"));
    }

    // sentinel 配置违规兜底
    public R blockHandler(@PathVariable("id") Long id, BlockException e) {
        return new R(444,"blockHandler-sentinel 限流,无此流水:BlockException  " + e.getMessage(), new Payment(id, "null"));
    }


}

测试:

  •  访问一次 http://localhost:84/consumer/fallback/4 (此时会出现 IllegalArgumentException  异常)出现报错页面
  • 而访问 http://localhost:84/consumer/fallback/5 出现我们自定义的报错

3、Feign 系列

修改 cloudalibaba-consumer-order84 模块。

注意,项目启动可能会失败,是因为 SpringCloud 与 SpringCloudAlibaba 的版本不对应导致的,我修改了父Pom的版本如下,就可以正常启动了

pom 文件新增 openFeign 依赖

        <!-- openFeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

yaml 增加 openFeign 的配置

#激活sentinel对Feign的支持
feign:
  sentinel:
    enabled: true

主启动类增加 @EnableFeignClients 注解

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain84.class, args);
    }
}

增加带 @FeignClient 的接口

@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
    @GetMapping("/paymentSQL/{id}")
    public R<Payment> paymentSQL(@PathVariable("id") Long id);
}

增加降级处理类

/**
 * 兜底方法
 */
@Component
public class PaymentFallbackService implements PaymentService {
    @Override
    public R<Payment> paymentSQL(Long id) {
        return new R(444,"服务降级返回");
    }
}

controller 增加如下内容

    @Resource
    private PaymentService paymentService;

    @GetMapping("/consumer/paymentSQL/{id}")
    public R<Payment> paymentSQL(@PathVariable("id") Long id){
        return paymentService.paymentSQL(id);
    }

测试:

  • 访问 http://localhost:84/consumer/paymentSQL/1 正常
  • 关闭 payment9003/9004 服务,再访问 http://localhost:84/consumer/paymentSQL/1 ,会出现降级异常 

4、熔断框架比较

十、规则持久化

1、是什么

一旦我们重启应用,sentinel控制台的规则将消失,生成环境需要将配置规则进行持久化

2、怎么玩

将限流规则持久化仅 Nacos 保存,只需要刷新 8401 某个 rest 地址, sentinel 控制台的流控规则就能看到,只要 Nacos 里面的配置不删除,针对 8401 上 sentinel 上的流控规则持续有效。

3、步骤

修改 cloudalibaba-sentinel-service8401 。

POM 中需要有 sentinel-datasource-nacos 的依赖

        <!-- alibaba sentinel-datasource-nacos,后续持久化用到 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

yaml 配置中 sentinel 要添加 nacos 和 持久化配置

spring:
  cloud:
    sentinel:
      # nacos 持久化 sentinel 控制台配置
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848  #nacos 注册中心
            dataID: cloudalibaba-sentinel-service #本项目的 spring.application.name
            groupId: DEFAULT_GROUP   #默认分组
            data-type: json
            rule-type: flow

添加 Nacos 业务规则配置

[
    {
        "resource":"/byUrl",
        "limit":"default",
        "grade":1,
        "count":1,
        "strage":0,
        "controlBehavior":0,
        "clusterMode":false
    }
]

resource:来源名称

limitApp:来源应用

grade:阈值类型,0 线程数,1 QPS

count:阈值

strategy:流控模式,0 直接,1 关联,2链路

controlBehavior:流控效果, 0 直接失败, 1 Warm Up,2排队等待

clusterMode:是否集群

这种做法比较麻烦,后续看还有没有更加好的办法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud Alibaba Sentinel是一个基于Java的开源框架,提供了熔断、降级、限流、系统负载保护等功能,可以帮助开发者实现微服务架构中的高可用性和稳定性。下面是一个使用Spring Cloud Alibaba Sentinel实现熔断限流的项目介绍。 1. 创建Spring Boot项目 首先,需要创建一个Spring Boot项目,并添加Spring Cloud Alibaba Sentinel的依赖: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2.2.3.RELEASE</version> </dependency> ``` 2. 配置Sentinel Dashboard Sentinel Dashboard是Sentinel的可视化管理平台,可以通过它来查看应用程序的运行状况、配置规则等。需要在项目中添加Sentinel Dashboard的依赖: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel-datasource-nacos</artifactId> <version>2.2.3.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-dashboard</artifactId> <version>2.1.1.RELEASE</version> </dependency> ``` 同时,在application.properties文件中添加以下配置: ```properties # Sentinel Dashboard配置 spring.cloud.sentinel.transport.dashboard=localhost:8080 # Nacos配置 spring.cloud.nacos.discovery.server-addr=localhost:8848 spring.cloud.nacos.discovery.namespace= spring.cloud.nacos.discovery.username= spring.cloud.nacos.discovery.password= ``` 启动项目后,访问http://localhost:8080即可进入Sentinel Dashboard界面。 3. 实现熔断限流 在项目中可以通过注解方式实现熔断限流功能。例如,在Controller类中添加以下代码: ```java @RestController public class HelloController { @GetMapping("/hello") @SentinelResource(value = "hello", fallback = "fallback") public String hello(@RequestParam(required = false) String name) { if(StringUtils.isEmpty(name)) { throw new IllegalArgumentException("name is empty"); } return "Hello, " + name; } public String fallback(String name) { return "fallback " + name; } } ``` @SentinelResource注解指定了资源名称为hello,同时指定了fallback方法用于处理熔断降级。可以通过Sentinel Dashboard配置熔断规则和流量控制规则。 以上就是使用Spring Cloud Alibaba Sentinel实现熔断限流的项目介绍。使用Sentinel可以帮助我们更好地保障应用程序的稳定性和可用性,避免因为异常情况导致系统崩溃。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值