系列文章目录
微服务新王SpringCloudAlibaba
文章目录
- 系列文章目录
- 前言
- Sentinel: 分布式系统的流量防卫兵
- 一、@SentinelResource注解
- 二、Sentinel整合ribbon+fallback
- 三、Sentinel整合ribbon+openFeign+fallback
- 四、Sentinel规则持久化
- 五、总结
前言
Sentinel: 分布式系统的流量防卫兵
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
- 官方文档:https://github.com/alibaba/Sentinel
- 中文文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
- sentinel与hystrix功能类似,但是更优。
一、@SentinelResource注解
1. 按资源名称限流
添加RateLimitController接口,用于测试
package com.atguigu.springcloud.alibaba.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @auther zzyy
* @create 2020-02-25 15:04
*/
@RestController
public class RateLimitController
{
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "handleException")
public CommonResult byResource()
{
return new CommonResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001"));
}
public CommonResult handleException(BlockException exception)
{
return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用");
}
}
好,简单测试一下,这块不再讲,上篇讲过,给byResource配置一个QPS流控规则,观察流控状态。
额外的问题:
此时关闭问服务8401看看,Sentinel控制台,流控规则消失了?????
2. 按URL限流
通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息。
改造RateLimitController,添加测试接口,注意这时SentinelResource没有写兜底方法。
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl()
{
return new CommonResult(200,"按url限流测试OK",new Payment(2020L,"serial002"));
}
配置规则,按照资源路径进行配置(GetMapping的值)
这时可以发现,不会走兜底方法,细心的小伙伴可能会说没写兜底方法,可以自己手动添加上试下,使用URL限流是不会走兜底方法的,只会使用Sentinel自带默认的限流处理信息。
3. 客户自定义限流
上面我们的操作存在的问题:
- 系统默认的,没有体现我们自己的业务要求。
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。
- 每个业务方法都添加一个兜底的,那代码膨胀加剧。
- 全局统一的处理方法没有体现。
我们使用客户自定义限流处理逻辑可以解决以上说的这些问题。
编写controller接口
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler()
{
return new CommonResult(200,"按客戶自定义",new Payment(2020L,"serial003"));
}
新建CustomerBlockHandler,注意:方法要是static的,sentinel会通过类名.方法名的方式调用。
package com.atguigu.springcloud.alibaba.myhandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
/**
* @Author: Daisen.Z
* @Date: 2022/1/21 11:47
* @Version: 1.0
* @Description:
*/
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception)
{
return new CommonResult(4444,"按客戶自定义,global handlerException----1");
}
public static CommonResult handlerException2(BlockException exception)
{
return new CommonResult(4444,"按客戶自定义,global handlerException----2");
}
}
我们这样编写完成后,就相当于编写的customerBlockHandler中,指定CustomerBlockHandler类的handlerException2方法作为兜底方法。
测试一下,先访问下接口
http://localhost:8401/rateLimit/customerBlockHandler
然后添加下规则
再次测试下访问接口,多刷新几次,发现已经会走我们的兜底方法。
这里千万注意:使用资源路径方式进行的配置,不会走兜底方法!!!!!!
二、Sentinel整合ribbon+fallback
1. 场景介绍
我们使用以下场景来学习Sentinel和Ribbon的整合,构建消费者84去伏鳟均衡的调用9003和9004。
2. 构建服务提供者9003
模块名
cloudalibaba-provider-payment9003
基础包
com.atguigu.springcloud.alibaba
pom.xml依赖
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<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>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
主启动类PaymentMain9003
package com.atguigu.springcloud.alibaba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @auther zzyy
* @create 2020-02-25 16:10
*/
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
业务类PaymentController
package com.atguigu.springcloud.alibaba.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
/**
* @auther zzyy
* @create 2020-02-25 16:11
*/
@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,"28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
{
// 这里只有三个记录,当id传成4或其他的不是123的时payment会是null,后续其他微服务直接调用会出现空指针
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort: "+serverPort,payment);
return result;
}
}
3. 构建服务提供者9004
除端口号外,其余和9003工程一样。
4. 构建服务消费者84
模块名
cloudalibaba-consumer-nacos-order84
pom.xml依赖
<dependencies>
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<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>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
基础包
com.atguigu.springcloud.alibaba
配置文件application.yml
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
启动类OrderNacosMain84
package com.atguigu.springcloud.alibaba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @auther zzyy
* @create 2020-02-25 16:04
*/
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
配置类ApplicationContextConfig
package com.atguigu.springcloud.alibaba.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @auther zzyy
* @create 2020-02-25 16:06
*/
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
业务类CircleBreakerController
package com.atguigu.springcloud.alibaba.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
/**
* @auther zzyy
* @create 2020-02-25 16:05
*/
@RestController
@Slf4j
public class CircleBreakerController
{
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback") //没有配置
public CommonResult<Payment> fallback(@PathVariable Long id)
{
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
}
5. 测试一下环境
启动Sentinel、Nacos、9003、9004及84服务模块
访问84接口,重复刷新,看是否能够轮询的去调用9003和9004模块
http://localhost:84/consumer/fallback/1
Sentinel也已经监控成功
当我们输入参数4会报非法参数异常
http://localhost:84/consumer/fallback/4
当我们输入非1234的其他参数会报空指针异常
http://localhost:84/consumer/fallback/5
注意,这时@SentinelResource没有配置任何兜底方法,错误会直接返回到前端,这样不太友好。。。
6. @SentinelResource的fallback
fallback属性配置的是出现异常时的兜底方法。
我们给84的接口添加上该注解
@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常,表示出现异常时会找handlerFallback方法
添加上handlerFallback方法
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}
重启84服务,我们再次访问接口
http://localhost:84/consumer/fallback/5
这时页面友好多了,没有把错误直接抛给浏览器,而是走的我们的异常兜底方法。
7. @SentinelResource的blockHandler
接下来,我们再添加上blockHandler
@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id,BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}
重启84服务,访问接口并在Sentinel页面上配置上限流规则
http://localhost:84/consumer/fallback/1
好,加好之后我们再测试一下访问接口,狂刷一把让流控规则生效。
http://localhost:84/consumer/fallback/1
已经走了限流,并且使用的是我们刚才添加的兜底方法。
接下来我们再做一个测试,访问报错的接口,在fallback(异常兜底方法)和blockHandler(Sentinel规则兜底方法)同时存在的情况下,出现异常和命中限流时会怎样呢?
http://localhost:84/consumer/fallback/4
缓慢点击时会走fallback异常兜底方法
快速刷新,命中Sentinel限流规则后会走blockHandler降级兜底方法。
这就说明了,fallback(异常兜底方法)和blockHandler(Sentinel规则兜底方法)同时存在的情况下,各管各的,互不影响,且Sentinel规则的优先级高于异常解析。
8. @SentinelResource的忽略属性exceptionsToIgnore
给注解添加上属性,并重启84服务
@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
exceptionsToIgnore 意思就是当出现指定的错误类型(例外)时,就不走兜底方法,让错误该往下传往下传,到页面就到页面,通常我们在开发、测试时需要用的该注解,用于调试、处理异常情况。
我们测试下分别访问参数4(会报IllegalArgumentException异常和参数5),看下结果。
http://localhost:84/consumer/fallback/4
参数4时没有走兜底方法,直接报错了。。。
http://localhost:84/consumer/fallback/5
参数是5时,仍然走兜底方法。
三、Sentinel整合ribbon+openFeign+fallback
1. 添加OpenFeign调用
添加pom依赖
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
修改84工程application.yml增加对openFeign支持
feign:
sentinel:
enabled: true
给主启动类添加开启Feign的注解
@EnableFeignClients
好了,接下来编写Service,@FeignClient注解的意思是去注册中心找nacos-payment-provider服务,然后fallback 表示的是服务出错时调用哪个类来兜底。
package com.atguigu.springcloud.alibaba.service;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @Author: Daisen.Z
* @Date: 2022/1/21 16:56
* @Version: 1.0
* @Description:
*/
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService {
// 表示找/paymentSQL/{id}接口
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
PaymentFallbackService
package com.atguigu.springcloud.alibaba.service;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.stereotype.Component;
/**
* @auther zzyy
* @create 2020-02-25 18:30
*/
@Component
public class PaymentFallbackService implements PaymentService
{
@Override
public CommonResult<Payment> paymentSQL(Long id)
{
return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService",new Payment(id,"errorSerial"));
}
}
给Controller添加使用OpenFeign的方式调用
//==================OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
{
return paymentService.paymentSQL(id);
}
2. 测试
接下来启动测试,我们只启动9003和84模块,一会方便测试出现调用失败的情况。
调用使用OpenFeign的接口。
http://localhost:84/consumer/paymentSQL/1
哎,完美~
接下来我们把9003手动停掉,测试下调用不通(报错)的情况。
也能够成功进行降级,完美!!
四、Sentinel规则持久化
1. 规则持久化介绍
在前面的测试中我们发现,一旦重启,sentinel配好的规则就会消失了,生产环境需要将配置规则进行持久化。
我们使用以下步骤进行演示:
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效。
2.开始玩耍
修改8401工程,让规则持久化进Nacos。
先给pom添加依赖。
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
给sentinel客户端添加nacos持久化数据源
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
# 默认端口8719,假如被占用会自动+1直到找到可用端口
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
登录Nacos,在配置管理里面,新建配置文件,dataid为配置文件中的dataId,group为配置中的groupId,配置格式配置文件中的data-type,配置内容下面给出详解。
[{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}]
#resource: 资源名称;
#limitApp:来源应用;
#grade:阈值类型, 0表示线程数, 1表示QPS;
#count:单机阈值;
#strategy:流控模式,0表示直接,1表示关联, 2表示链路;
#controlBehavior:流控效果,0表示快速失败, 1表示Warm Up, 2表示排队等待;
#clusterMode:是否集群。
重新启动Sentinel和8401工程,狂刷接口,发现已经呗限制了(因为我们使用的是接口的方式,所以使用的是Sentinel默认的兜底方法),然后访问Sentinel页面
http://localhost:8401/rateLimit/byUrl
访问Sentinel页面,发现规则也自动加载了。
localhost:8080
棒棒的,以后这个功能可能还会继续完善,或者我们可以在网上找一些方法修改源码实现自动化的同步。
五、总结
通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息。
@SentinelResource注解的fallback(异常兜底方法)和blockHandler(Sentinel规则兜底方法)同时存在的情况下,各管各的,互不影响,且Sentinel规则的优先级高于异常解析。
@SentinelResource注解的exceptionsToIgnore可以指定某些类型的异常(多个)不走fallback兜底的异常降级方法。
Sentinel可以进行持久化配置,在nacos中通过json文件配置即可。