SpringCloud系列总集

106 阅读31分钟

SpringCloud基本使用

主要依赖

  • SpringCloud:

    • gateway :Api网关,它可以提供一种简单而有效的方法路由到api。可以用来做身份验证,监考,负载均衡等。
    • openfeign: 通过接口的形式提供服务间的调用。
  • SpringcloudAlibaba

    • nacos: 服务注册与发现,配置中心
    • sentinel: Sentinel 是面向分布式服务架构的流量控制组件。
    • seata: 分布式事务组件

版本

Spring Cloud VersionSpring Cloud Alibaba VersionSpring Boot Version
Spring Cloud Hoxton.SR92.2.6.RELEASE2.3.2.RELEASE
Spring Cloud Alibaba VersionSentinel VersionNacos Version
2.2.6.RELEASE1.8.11.4.2

整体架构

image-20220825174354855

解释:

  • cloud-consumer-payment服务到了 10001 和 10002接口,cloud-gateway(api网关)路由到cloud-consumer-payment服务会有负载均衡的效果(轮询)
  • cloud-consumer-payment 通过openfeign调用cloud-provider-payment服务(同样有负载均衡的效果)。
  • 所有服务都会注册进nacos
  • cloud-consumer-payment服务开启了sentinel,seata。

基本使用

如何使用nacos服务注册与发现

Nacos(官方网站:Nacos .io)是一个易于使用的平台,用于动态服务发现、配置和服务管理。它可以帮助您轻松构建云原生应用程序和微服务平台。

特性:

  • 服务发现和服务健康监测
  • 动态配置服务
  • 动态 DNS 服务
  • 服务及其元数据管理

通俗点说,微服务是用多个服务组合起来的应用。那么多个服务必要要被管理起来,并且他们之间还涉及到服用调用,nacos就是管理这些服务的。这样如果一个服务想找另外一个服务他就可以根据服务名等直接到nacos中寻找。并且一个服务可能有多个实例,这也能在nacos中反应出来。因此也可以更容易的服务间的负载均衡。

前置条件

依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>xxxxx</version>  <!-- 通常在父pom已指定,这里就不需要指定。以下这里不写就说明父pom已经指定-->
</dependency>

配置

spring:
  application:
  //[1]
    name: cloud-provider-payment
  cloud:
    // [0]
    nacos:
      discovery:
        server-addr: localhost:8848
  • [0]处配置nacos地址
  • [1]处应用名字必须写

开启注解

...
// 开启服务发现
@EnableDiscoveryClient
...
public class ProviderMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(ProviderMain9001.class, args);
    }
}

实践

完成如上步骤,登录 ip(具体看你部署的机器ip):8848/nacos,输入用户名:naocs 密码:nacos,点击登录

效果

image-20220825174423230

  • 点击0处来到服务列表页面
  • 点击1处 切换到public 命名空间(命名空间后续会说)。因为配置中我们没有配置命名空间和分组所以默认命名空间为public 分组为DEFAULR_GROUP
  • 2处为刚刚注册进来得服务

总结

  • 服务注册时微服务应用的核心,后续等组件都需要和它联动
  • 利用命名空间和分组可以有效区分服务达到开发,测试分离的目的(后续配置中心会详细说明)。
  • 默认命名空间为public 默认分组为DEFAULR_GROUP
  • nacos服务为单独部署,可以集群以及持久化

如何使用openfeign进行服务间的调用

服务间如何进行接口调用呢?一般来说使用restTemplate,格式为{被调用服务的服务名}/{接口},但是这样做有以下缺点

  • 你只能根据服务名来判断调用了那个服务,而服务名就是字符串而已,时间久了应用复杂他可能就变成魔数了。
  • 所有代码都耦合在了controller里面
  • {被调用服务的服务名}/{接口}本身就是一个字符串,可读性低

以上的解决方法就是使用openFeign,虽然底层仍然是restTemplate。

前置条件

  • 依赖
 <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 开启注解
@SpringBootApplication
// nacos服务发现
@EnableDiscoveryClient
// 开始feign
@EnableFeignClients
public class ConsumerMain10001 {
​
    public static void main(String[] args) {
        SpringApplication.run(ConsumerMain10001.class, args);
    }
}
  • 配置yaml
spring:
  cloud:
    nacos:
      discovery:
        namespace: public
        # 配置nacos地址
        server-addr: localhost:8848
  • 待调用的服务
// cloud-provider-payment服务
@RestController
public class ProviderPaymentHello {
​
    @Value("${server.port}")
    private String port;
​
    @GetMapping("hello")
    public String hello() {
        return "hello provider" + " port:" + port;
    }
}

实践

  • 创建接口

    • @FeignClient:很重要的注解,value(name)为要调用服务的名字(spring.application.name的值),fallback为整合sentinel后服务降级熔断后的错误回调,可不加。
@FeignClient(value = "cloud-provider-payment", fallback = PaymentFallbackService.class)
public interface CloudProviderPaymentFeignService {
    @GetMapping("hello")
    String hello();
}
  • 使用接口
@RestController
@Slf4j
public class ConsumerController {
​
    // 注入上面的接口
    @Resource
    private CloudProviderPaymentFeignService cloudProviderPaymentFeignService;
​
    @Value("${server.port}")
    private String port;
​
    @GetMapping("/consumerHello")
    public String consumerHello() {
        log.info("feign consumer");
        // 调用接口即可
        return cloudProviderPaymentFeignService.hello();
    }
}

效果

image-20220825174440158

总结

  1. 调用其他服务的接口的时候,先在本地创建相应的接口
  2. 使用@FeigClient注解,传入调用服务的服务名。
  3. 最后在其他地方注入实例后即可调用
  4. openFeign本质是RestTemplate

tips:

  • 当调用服务有多个实例的时候,会自动开启负载均衡功能(轮询)。
  • 当前demo版本不需要配置RestTemplate,其他版本如果报错则需要配置:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}
  • 如果使用Spring Cloud 2020.0.0Spring Cloud Alibaba 2021.1,Spring Boot 2.4.2这需要另外引入

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

如何使用gateway

使用Gateway网关

前置条件

依赖

<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

注意: 这里不需要引入spring-boot-starter-web依赖因为gateway使用了 webflux而不是springmvc

开启注解

@SpringBootApplication
// 开启nacos服务发现
@EnableDiscoveryClient
public class CloudGateWayMain {
    public static void main(String[] args) {
        SpringApplication.run(CloudGateWayMain.class, args);
    }
}
实践

配置yml文件

因为我这里使用了配置中心,所以要先配置boostrap.yml文件。这里我们先不讲配置中心,只关注于配置。

spring:
  cloud:
  // 配置gateway
    gateway:
      discovery:
        locator:
          enabled: true
        // [0]  
      routes:
        // [1]
        - id: after_route
        // [2]
          uri: lb://cloud-consumer-feign
         // [3]
          predicates:          
            - Path=/api/**
            - After=2021-07-13T14:11:00.123+08:00[Asia/Shanghai]
    nacos:
      discovery:
        server-addr: localhost:8848
  application:
    name: cloud-gateway
management:
  endpoints:
    web:
      exposure:
        include: refresh
server:
  port: 2001

解释

  • [0]routes: 标识路由配置开始

  • [1] -id: "-"符号是yml里面数组的标识方法,标识这里可以配置多项路由。id为自定义的名字,因为我这里使用了after route所以定义为after router

  • [2] uri: 以lb://cloud-consumer-feign 为例,lb标识开启负载均衡,实际上还能使用httphttpscloud-consumer-feign为其他服务的服务名。

  • [3] predicates: 翻译过来是谓词逻辑来自java中,通俗点讲就是在这里配置路由规则。

    • path: 匹配请求路径
    • After: 当前请求在指定时间之后才匹配。After=2021-07-13T14:11:00.123+08:00[Asia/Shanghai] 以此为例,表示只有只有在2021-07-13 14:11:00之后才能访问上面path匹配的url。

tips:这里只介绍了after,path 路由谓词。实际上有很多种路由谓词并且支持自定义路由谓词。简单列出一下几项:

谓词逻辑作用
After route predicate当前请求在指定时间**之后**才匹配
Before route predicate当前请求在指定时间**之前**才匹配
Between route predicate当前请求在指定时间**之间**才匹配
Header route predicate当前请求中的header值匹配配置的header参数值时生效
Method Route Predicate只有指定方式(put,post,get....)才能访问
Path Route Predicate匹配请求路径
Query route predicate匹配请求参数
RemoteAddr route predicate匹配请求的ip地址,支持ipv4和ipv6
Weight route predicate根据权重来分发请求,权重是根据group来计算的

路由的接口

@GetMapping("/api/testAfterRoute")
public String testGateWayAfterRoute() {
    return "testAfterRoute time:" + LocalDateTime.now() + " port:" + port;
}

效果

**

image-20220825174825236

改变时间尝试路由

After=2021-07-16T14:11:00.123+08:00[Asia/Shanghai]

结果

image-20220825174844448

总结
  • 在gateway种我们通过简单的配置即可达到api网关的目的。
  • 利用官方自带的谓词逻辑,我们可以实现限时开放接口,负载均衡,身份验证等等。
  • 通过自定义谓词逻辑,可以高度定制api网关逻辑
  • 配和nacos的配置中心可以实现动态更改路由谓词规则,不用重启服务即可生效新的路由谓词规则。
  • 低代码,yaml文件配置可满足大部分需求
引用

Spring Cloud Gateway

Spring Cloud Gateway Route Predicate Factory 的使用

如何使用nacos配置中心

前置条件

依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

开启注解

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
// 服务发现
@EnableDiscoveryClient
// 因为整合了mybatis,不整合可以不写
@MapperScan(basePackages = {"com.celi.springcloud.mapper"})
public class ProviderMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(ProviderMain9001.class, args);
    }
}

实践

配置bootstrap.yml

因为配置中心的本质是从nacos的服务端拉取配置文件,所以这一切必须要在服务运行起来之前进行。

bootstrap.yml就是在程序引导时执行,应用于更加早期配置信息读取。有点类似于计算机启动过程中引导程序,所以取名bootstrap就很见名知意。


spring:
  application:
    name: cloud-provider-payment
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      //[0]
      config:
        group: DEFAULT_GROUP
        namespace: ce08d54a-a515-4ae2-a055-3f5321732cd0
        server-addr: localhost:8848
        file-extension: yaml
  • [0]config: 这里开始配置中心的配置

    • group: 所属组,这里使用得默认组。
    • namespace: 命名空间。用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
    • server-addr: nacos地址
    • file-extension: 配置文件格式

解释:

  • namespace:以包钢为例,新建一个namespace为bg,再新建一个namespace为nj(南京)。用这个来区分现场和公司的环境配置。
  • group: 我们在bgnj的命名空间下可以分别新建DEV_GROUP,PRD_GROUP用于区分开发和生产环境
  • dataId: 这个我先看application.yml的配置

配置application.yml

spring:
  profiles:
    active: dev

application.yml 中active对应得值就是dataId。我们联系nacos来进行解释。

新建namespace

image-20220825175916079

  1. 点击0处出来到命名空间页面
  2. 点击1新建命名空间
  3. 2处填写命名空间信息,其中命名空间id会自动生成。
  4. 填写完成确定后,3处多了一条数据,其中命名空间id则为,上述namespace得值

新建配置

image-20220825175925752

  1. 点击0配置管理,切换到我我们刚刚新建得1DEV命名空间
  2. 点击2处出加号来到如下界面。

image-20220825175935514

  1. 0出得配置规则如下:

    {service-name}-{dataid}.{ file-extension} 其中data id就是上面我们再application.yml里面配置得active值。利用这里也可以区分开分和测试配置。

    当然这里还有许多其他得格式,我这里使用了这一种而已。具体可以参照nacos官方,建议固定一种格式。

  2. 接着就可以再配置内容里面写配置了。

效果

image-20220825175950412

假设我们在配置中心加入一些配置然后点击发布:

image-20220825180003515

然后通过如下接口,确认是否配置已同步到服务中:

@Value("${config.info}")
private String info;
​
@GetMapping("getInfo")
public  String getInfo(){
    return info;
}

测试接口

image-20220825180016316

总结

  • 利用配置中心我们实现不停机更改配置,比如换数据库等
  • 利用namespace,group,dataId我们可以有效区分区域,开发环境等不同

如何使用sentinel进行流量控制

sentinel功能十分强大,受能力和时间限制我无法一次性全部讲清楚,只能挑部分说明。

sentinel单独使用

这里我们为cloud-consumer-payment配置sentinel

前置条件

依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

配置yml

server:
  port: 10001
spring:
  application:
    name: cloud-consumer-feign
  cloud:
    nacos:
      discovery:
        namespace: public
        server-addr: localhost:8848
    //[0]    
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
  • [0]处 为配置sentinel,dashboard时sentinel后台管理页面地址,port为sentinel api 端口
实践—限流配置
  1. 新建一个测试接口
@GetMapping("/testA")
public String testA() {
    return "test A port:" + port;
}
  1. 进入dashboard

浏览器输入上述配置的dashboard地址用户名密码都是 sentinel

image-20220825180029127

先看侧边栏,第一道划线处为当前配置sentinel的服务,看上面 “配置yml” 处配置的spring.apploication.name=cloud-consumer-openfeign就知道为什么这里名字时cloud-consumer-openfeign了。

  1. 点开簇点链路

image-20220825180058233

可以看到这已经有一些请求过来了,还可以看到我们之前测试openfeign时调用的 /consumerHello接口实际时调用了cloud-provider-paymenthello接口

  1. 请求测试接口

  2. 回到后台页面刷新

    可以看到testA接口已经被探测到了,由此可见sentinel他是懒加载的。即只有接口请求之后才会被sentinel发现。

下面我们围绕流控熔断降级热点授权分别举例子来说明

流控
  • 如何添加流控规则

    • 簇点链路界面选择点击接口的流控的按钮

    image-20220825180111203

    • 点击新增或者新增并继续添加,并配置规则(稍后说)

    • 切换到流控规则页面,此时就会多一条流控的规则的信息

      image-20220825180121887

  • 参数说明

    • 回到我们新建流控规则的簇点链路界面。再次点击 “流控” 按钮,我们来看看参数时是什么意思

      image-20220825180131298

      资源名:接口名

      针对来源:Sentinel可以针对调用者进行限流,填写微服务名,指定对哪个微服务进行限流 ,默认default(不区分来源,全部限制)

      阈值类型:

      • QPS: 每秒钟的请求数量,当调用该接口的QPS达到了阈值的时候,进行限流;
      • 并发线程数:当调用该接口的线程数达到阈值时,进行限流

      单机阈值:限流的阈值

      流控模式:

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

      流控效果:

      • 快速失败:直接失败
      • Warm Up:即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。点击后可以选择预热时长
      • 排队等待:排队等待点击之后可以设置等待时间
  • 如何编辑规则

    image-20220825180246767

    如图直接填写参数就好。

  • QPS模式->直接->直接失败演示

    我们配置限流为 每秒只能请求一次 /testA接口,现在测试。如下图可以看到,当我们请求过快之后,第二次就失败了

    image-20220825180325484

    并且失败之后会有如下字样,这是sentinel自带的失败处理,我们后面可以自定义。

    image-20220825180337068

    选择直接失败就会直接返回如上字样,告知你接口被限流了。

  • QPS模式->直接->warm up演示

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

    • 假设我们设置如下

    image-20220825180346853

    阈值:3,预热时长 :6。会限制QPS到3/3次,然后经过6秒之后 加到QPS为3。

    • jmeter配置

      每秒三个请求

      image-20220825180402005

    • 测试结果

    image-20220825180410064

    可以发现很明显的刚开始三个只能成功一个,后面三个就全部成功了。

  • QPS模式->直接->排队等待演示

    • 流控规则配置如下

      image-20220825180424033

    • jmeter配置

      一秒发五个请求

      image-20220825180435423

    • 测试结果

image-20220825180443892

分析一下为什么有一个会失败,因为QPS为3,所以有三个肯定成功,触发排队等待之后。每三分之一秒会处理一个数据。而我们的超时时间500ms

所以最后被后端接受处理的两个请求必定有一个要等待600ms才会被处理。所以就会失败。

注意:图中的顺序并不是,后端处理的顺序。理论上他们是同一时间到达后端的。

  • QPS模式->关联->快速失败演示

    这里我只演示一个剩下的大家举一反三即可

    • 新建关联接口 /testB

      @GetMapping("/testB")
      public String testB() {
          return "test B port:" + port;
      }
      
    • 流控规则配置如下

      • /testA

      image-20220825180455726

      • /testB

      image-20220825180510761

    • jemter配置

      请求 /testB 一秒两次触发限流

      image-20220825180529230

    • 结果

      image-20220825180537347

      可以看见 /testA被限流了,当我们取消调/testA的测试 立马 /testA接口就又好了。

  • QPS模式->链路->快速失败演示

    同样这个模式,我也只演示一个。其他的依然可以举一反三或者通过查文档解决.

    • 流控配置

      这次调用链路为 chain1->chain2->chain3

      image-20220825180549799

      此处入口资源填写来自于簇点链路页面

      image-20220825180557815

      因为chain1被我整合在了gateway服务里面,所以这里看不到。但我们同样可以用这个举例子。

      a->b->d, a->b->e, a->c->f, a->c->g均可视作链路。 假设我以a为入口资源,d为终点资源,对这条链路进行限制的话,则资源a,b,d均会被限制访问。

      同样如果我们对chain2进行了链路流控,则从入口资源chain2的位置的资源就会被限流。

    • 测试

      请求过快会导致限流

      image-20220825180607756

      再来看看chain3则完全不受影响

      image-20220825180614102

      所以链路流控更加像是区间流控,它会限流一个区间。

  • 流控总结

    • 通过页面配置可以完成大部分流控
    • 限流措施又三种直接失败warm up,排队等待
    • 限流阈值类型有两种:QPS并发线程数
    • 限流对象有三种:直接(单点)关联链路(区间)
熔断

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

ps:因为三种熔断机制差不多,所以我只讲一种剩下得大家举一反三即可。

  • 如何添加熔断规则

    和流控一样点击熔断按钮即可。ps:对应的接口必须请求一次才能出现在sentinel后台。

    image-20220825180624053

  • 参数说明

    image-20220826164110303

    • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
    • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
    • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
  • 慢调用比例演示

    • 熔断配置

      image-20220826164121995

    • 改造testA接口,确保每次调用都大于100ms

    @GetMapping("/testA")
    public String testA() {
        try {
            TimeUnit.MICROSECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "test A port:" + port;
    }
    
  • 测试

    jmeter配置,一秒一个发20个请求

    image-20220826164133059

    测试结果

    image-20220826164143241

    可以看到前6个是没有问题得,但是程序里面设置sleep所以。慢调用比例肯定是100%。所以过了统计时间之后就满足了。统计时间内请求数目大于5,并且慢调用比例大于50%。所以进入熔断,但是熔断时间只有一秒便会进入探测恢复状态。又因为接下来得请求还是慢调用,所以就又会熔断,如此反复。

  • 总结

    • 熔断之后会进入探测恢复状态,而不是全恢复
    • 熔断机制有三种:慢调用异常比例异常数
    • 熔断像是断臂求生一样,舍弃一两个保全大局
热点

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

商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制

用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

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

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。

使用热点参数限流功能,必须加入以下依赖:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-parameter-flow-control</artifactId>
</dependency>
  • 热点配置界面

    image-20220826164233039

  • 参数说明

    参数索引:接口的第几个参数

    统计时间:统计多长时间的数据

    参数类型:指定接口索引参数的类型,目前支持基本类型。

    参数值:接口索引参数的值

    限流阈值:当索引参数的值为指定值是,接口QPS阈值是多少

  • 热点限流演示

    • 热点配置

      image-20220826164243703

    • jmeter配置

      image-20220826164252445

    • 结果

      image-20220826164302452

      可以看到当id为1时,基本5个请求只能成功一个。说明是按照单机阈值来限流的。

    • 修改jmeter配置

      image-20220826164330336

    • 结果

      image-20220826164336227

      可以看到请求全部成功了,说明我们的热点限流配置起效果了。

  • 总结

    • 热点限流粒度区分到了具体参数

    • 热点限流仅支持QPS模式

    • 热点参数例外项参数类型支持七种基本类型

      image-20220826164357347

系统规则

Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

image-20220826164406564

LOAD: 当系统load1(1分钟平均负载)超过阈值,且并发线程数超过系统容量时触发,建议设置为系统CPU核心数 * 2.5;仅对Linux/Unix系统有效。其中的load1,可以在Linux系统上通过命令 uptime 查看:

image-20220826164416130

其中的系统容量,含义如下所示:

image-20220826164422726

RT: 所有入口流量的平均RT达到阈值触发

线程数: 所有入口流量的并发线程数达到阈值触发

入口QPS: 所有入口流量的QPS达到阈值触发

CPU使用率: 当cpu使用率达到阈值就很被限流

对于系统规则,使用的比较少也比较简单。并且也存在一定问题,具体参考sentinel系统规则,因此也就不做演示了。

授权规则

很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

  • 配置请求源解析

    @Configuration
    public class SentinelConfig implements RequestOriginParser {
    ​
        @Override
        public String parseOrigin(HttpServletRequest httpServletRequest) {
            return httpServletRequest.getRemoteAddr();
        }
    }
    

    这里我配置的是过滤ip地址

  • 授权页面配置

    image-20220826164433559

    白名单模式 ,只允许127.0.0.1访问(localhost无法访问)

  • 测试

    image-20220826164442109

    改为localhost

    image-20220826164450841

  • 总结

    • 要想授权规则生效必须在代码中增加请求源解析
    • 根据请求源解析配置的不同会有不同的效果,简单的就是过滤ip地址
实践—自定义错误处理

之前接口在限流或者熔断等之后都会返回一个Blocked by sentinel(flow limiting)。下面就阐述如何自定义返回的内容以便给用户一个友好提示。

@SentinelResource 注解

注意:注解方式埋点不支持 private 方法。

@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 函数,否则无法解析。

1.8.0 版本开始,defaultFallback 支持在类级别进行配置。

注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,不能针对业务异常进行处理

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

  • blockHandler演示

    • 代码部分
    @GetMapping("/testB")
    @SentinelResource(value = "testB", blockHandler = "handler")
    public ResponseEntity<String> testB() {
        return ResponseEntity.ok().body("test B port:" + port);
    }
    ​
    public ResponseEntity<String> handler(BlockException exception) {
        return ResponseEntity.status(444).body(exception.getClass().getCanonicalName()+ " \t 我是一个友好的错误提示,真的 (~ ̄▽ ̄)~ ");
    }
    
    • dashboard配置

      配置了QPS限流,注意资源名不是 /testB 而是testB,和我们@SentinelResource注解中的value值一样。如果此时资源名使用接口url则发生限流之后不会进入我们的blockHadnler方法

      image-20220826164503706

    • 测试

      可以看到触发流控规则之后,返回的不再是默认的错误提示了

      image-20220826164519998

  • blockHandlerClass演示

    上面的方式会使得错误处理方法和业务代码耦合的太紧密。等日后业务代码多了之后,会变得更加难以管理。这种方式可以实现错误处理与业务分离。

    • 代码部分

      @GetMapping("/testB")
      @SentinelResource(value = "testB", blockHandlerClass = CustomerBlockHandler.class,blockHandler = "handler")
      public ResponseEntity<String> testB() {
          return ResponseEntity.ok().body("test B port:" + port);
      }
      
      public class CustomerBlockHandler {
          // 必须是public
         public static ResponseEntity<String> handler(BlockException exception) {
              return ResponseEntity.status(444).body(exception.getClass().getCanonicalName()+ " \t 我是一个友好的错误提示,真的 (~ ̄▽ ̄)~ ");
          }
      ​
      }
      
    • 测试

      image-20220826164546345

  • fallback演示

    • 代码

      @GetMapping("/testC")
      @SentinelResource(value = "testC", fallback = "fallbackHandler")
      public ResponseEntity<String> testC(@RequestParam("id") String id) {
          if ("1".equals(id)) {
              throw new IllegalArgumentException();
          }
          return ResponseEntity.ok().body("test C port:" + port);
      }
      ​
      public ResponseEntity<String> fallbackHandler(String id) {
          return ResponseEntity.status(444).body("我是自定义的异常处理  port:" + port + " id:" + id);
      }
      
    • 测试

      image-20220826164554453

  • fallbackClass演示

    • 代码部分

      @GetMapping("/testD")
      @SentinelResource(value = "testD", fallbackClass = FallbackConfig.class,fallback = "fallbackHandlerInClass")
      public ResponseEntity<String> testD(@RequestParam("id") String id) {
          if ("1".equals(id)) {
              throw new IllegalArgumentException();
          }
          return ResponseEntity.ok().body("test C port:" + port);
      }
      
      public class FallbackConfig {
          public static ResponseEntity<String> fallbackHandlerInClass(String id) {
              return ResponseEntity.status(444).body("我是自定义的异常处理 id:" + id);
          }
      }
      
    • 测试

      image-20220826164614077

  • fallback,blockhandler同时配置

    • 代码部分

      @GetMapping("/testE")
      @SentinelResource(value = "testE", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handler1",fallbackClass = FallbackConfig.class,fallback = "fallbackHandlerInClass")
      public ResponseEntity<String> testE(@RequestParam("id") String id) {
          if ("1".equals(id)) {
              throw new IllegalArgumentException();
          }
          return ResponseEntity.ok().body("test E port:" + port);
      }
      
      // 规则一场处理类
      public class CustomerBlockHandler {
          public static ResponseEntity<String> handler1(String id, BlockException exception) {
              return ResponseEntity.status(444).body(exception.getClass().getCanonicalName() + " \t 我是一个友好的错误提示,真的 (~ ̄▽ ̄)~ ");
          }
      }
      
      // 异常处理类
      public class FallbackConfig {
          public static ResponseEntity<String> fallbackHandlerInClass(String id) {
              return ResponseEntity.status(444).body("我是自定义的异常处理 id:" + id);
          }
      }
      
  • 全局配置处理

    有没有感觉这样很烦每个都要处理,那么这就是究极偷懒的方法。

    • 代码部分

      实现BlockExceptionHandler再注入springIOC容器即可

      @GetMapping("/testF")
      // 没有@SentinelResource,所以资源名就是url
      public ResponseEntity<String> testF() {
          throw new IllegalArgumentException();
      }
      
      @Configuration
      public class GlobalBlockHandler implements BlockExceptionHandler {
      ​
          @Override
          public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
              if (e instanceof FlowException) {
                  //流控规则异常
                  response.setStatus(445);
      ​
                  PrintWriter out = response.getWriter();
                  out.print("i am custom BlockException handler");
                  out.flush();
                  out.close();
              }
      ​
              if (e instanceof AuthorityException) {
                  //授权规则异常
                  System.out.println("i am flow AuthorityException");
              }
      ​
              if (e instanceof DegradeException) {
                  // 熔断规则
                  System.out.println("i am flow DegradeException");
              }
      ​
      ​
              if (e instanceof ParamFlowException) {
                  // 热点规则
                  System.out.println("i am flow ParamFlowException");
              }
      ​
              if (e instanceof SystemBlockException) {
                  // 系统规则
                  System.out.println("i am flow SystemBlockException");
              }
          }
      }
      
    • dashboard配置

      image-20220826164627309

    • 测试

      image-20220826164634710

  • 总结

    • blockHandler/blockHandler负责配置违规

    • fallback/fallbackClass负责业务异常

    • 如果两个都配置了,则被限流降级而抛出的BlockException时只会进入blockHandler处理逻辑

    • 本质上违反了配置规则就会抛出BlockException,而此时若配置了blockHandler就会调用blockhandler指定的方法处理这个异常的。若没有配置blockhandler则所有异常则归fallback处理

    • 实在不想写可以配置全局异常处理。

    • exceptionsToIgnore里的异常不会处理

      @GetMapping("/testE")
      @SentinelResource(value = "testE",fallbackClass=FallbackConfig.class,fallback"fallbackHandlerInClass",
                        exceptionsToIgnore = {IllegalArgumentException.class})
      public ResponseEntity<String> testE(@RequestParam("id") String id) {
          if ("1".equals(id)) {
              throw new IllegalArgumentException();
          }
          return ResponseEntity.ok().body("test E port:" + port);
      }
      

sentinel整合openfeign

前置条件
  • 依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  • 配置yml
feign:
  sentinel:
    enabled: true
实践
  • 改造接口

配置fallback参数

@FeignClient(value = "cloud-provider-payment", fallback = PaymentFallbackService.class)
public interface CloudProviderPaymentFeignService {
​
    @GetMapping("hello")
    String hello();
​
    @GetMapping("chain3")
    String chain3();
}
  • 实现接口
@Component
public class PaymentFallbackService implements CloudProviderPaymentFeignService {
    @Override
    public String hello() {
        return "我是整合openFeign后的异常处理";
    }
​
    @Override
    public String chain3() {
        return null;
    }
}
  • 测试

    正常情况

    image-20220826164648654

    cloud provider payment 9001服务宕机。可以发现自动调用了fallback里面的实现类对应的方法。

    image-20220826164655594

总结
  • 简单

sentinel整合gateway

前置条件

若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依赖,同时需要添加 spring-cloud-starter-gateway 依赖来让 spring-cloud-alibaba-sentinel-gateway 模块里的 Spring Cloud Gateway 自动化配置类生效:

  • 依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency><dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
    </dependency><dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    
  • 配置yml

    server:
      port: 2001
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true
          routes:
            - id: after_route
              uri: lb://cloud-consumer-feign
              predicates:
                - Path=/api/**
                - After=2021-07-13T14:11:00.123+08:00[Asia/Shanghai]
        // 核心
        sentinel:
          filter:
            enabled: false
          transport:
            dashboard: localhost:8080
            port: 8719
        // 
        nacos:
          discovery:
            server-addr: localhost:8848
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
  • 请求一次

image-20220826164707364

  • 回到页面

image-20220826164715395

实践
流控
  • 简单演示

    • 从请求链路里面添加流控规则并填写参数

      image-20220826164727780

    • 测试

      image-20220826164734965

  • 参数解释

    image-20220826164748242

  • API分组:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/**/baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。

其中网关限流规则 GatewayFlowRule 的字段解释如下:

  • API名称:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。

  • 间隔:统计时间窗口,单位是秒,默认是 1 秒。

  • 流控方式:流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。

  • burst:应对突发请求时额外允许的请求数目。

  • maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。

    针对请求属性:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:

    • 参数属性:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。
  • 匹配模式:参数值的匹配策略,目前支持精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持)

用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或通过 GatewayRuleManager.register2Property(property) 注册动态规则源动态推送(推荐方式)。

  • 针对请求属性演示

    这里就对header进行演示,其他的大家以此类推即可。

    • 流控配置如下

      image-20220826164759836

    • 测试

      无header

      image-20220826164822822

      有header

      image-20220826164829850

  • API分组限流演示

    • 代码

      这里我们定义了两个分组 some_customized_apianother_customized_api

      @Configuration
      public class ApiGroupConfig {
      
          @PostConstruct
          public void doInit() {
              initCustomizedApis();
          }
      
          private void initCustomizedApis() {
              Set<ApiDefinition> definitions = new HashSet<>();
              //
              ApiDefinition api1 = new ApiDefinition("some_customized_api")
                      .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                          add(new ApiPathPredicateItem().setPattern("/product/baz"));
                          add(new ApiPathPredicateItem().setPattern("/product/foo/**")
                                  .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                      }});
              //
              ApiDefinition api2 = new ApiDefinition("another_customized_api")
                      .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                          add(new ApiPathPredicateItem().setPattern("/ahas"));
                      }});
              definitions.add(api1);
              definitions.add(api2);
              GatewayApiDefinitionManager.loadApiDefinitions(definitions);
          }
      }
      
    • 重启项目打开dashboard

      image-20220826164840072

    • 再流控页面则可以按照分组来进行配置

      image-20220826164847082

  • 整合gateway其实就是网关级别得流控

sentinel持久化到nacos(规则管理与推送)

一般来说,规则的推送有下面三种模式:

推送模式说明优点缺点
原始模式API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource简单,无任何依赖不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境
Pull 模式扩展写数据源(WritableDataSource), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等简单,无任何依赖;规则持久化不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。
Push 模式扩展读数据源(ReadableDataSource),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。规则持久化;一致性;快速引入第三方依赖

Push模式

生产环境下一般更常用的是 push 模式的数据源。对于 push 模式的数据源,如远程配置中心(ZooKeeper, Nacos, Apollo等等),推送的操作不应由 Sentinel 客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。因此推送规则正确做法应该是 配置中心控制台/Sentinel 控制台 → 配置中心 → Sentinel 数据源 → Sentinel,而不是经 Sentinel 数据源推送至配置中心。这样的流程就非常清晰了:

image-20220826164904780

前置条件

依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

配置yml

spring:
  application:
    name: cloud-consumer-feign
  cloud:
    nacos:
      discovery:
        namespace: public
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
       //[0]
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            [1]
            data-id: cloud-consumer-feign
            [2]
            group-id: DEFAULT_GROUP
            [3]
            data-type: json
            [4]
            rule-type: flow
实践
  • nacos中新建配置

    注意yml配置:

    [0]表示sentinel数据源配置开始;

    [1]对应下图的0

    [2]对应下图的1

    [3]对应下图的2

image-20220826164920202

json中的名词解释:

image-20220826164928301

[4]rule-type需要单独讲,它取值如下:

    /**
     * flow.
     */
    FLOW("flow", FlowRule.class),
    /**
     * degrade.
     */
    DEGRADE("degrade", DegradeRule.class),
    /**
     * param flow.
     */
    PARAM_FLOW("param-flow", ParamFlowRule.class),
    /**
     * system.
     */
    SYSTEM("system", SystemRule.class),
    /**
     * authority.
     */
    AUTHORITY("authority", AuthorityRule.class),
    /**
     * gateway flow.
     */
    GW_FLOW("gw-flow",
            "com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule"),
    /**
     * api.
     */
    GW_API_GROUP("gw-api-group",
            "com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition");

全部配置好之后点击发,此时可以看到sentinel页面也跟着变化了:

image-20220826164939824

测试
  • 配置jmeter

    一秒20个请求

    image-20220826164948863

  • 结果

image-20220826164957687

总结
  • 配合nacos可以实现规则持久化
  • 也可以利用redis,zk等实现持久化
  • 生产环境持久化使用推模式
  • 这个持久化很难用