声明:笔记来源于网络,如有侵权联系删除
Sentinel 服务保护(限流、熔断降级)
Gateway网关
Seata
1.Sentinel

Spring Cloud Alibaba Sentinel,有多种保护规则,这些规则可以动态配置在Nacos、ZooKeeper、Apollo等系统上,同时这个功能支持SpringCloud,Dubbo,gRPC,ServiceMesh等框架

如上图,每一个应用中都有很多资源(比如接口就是资源),当App1引入Sentinel Client客户端时,就可以在Sentinel Dashboard中配置这个资源对应的规则,后面这个规则就会对这个资源起作用。规则可以在Nacos等地方配置。

资源:
1.默认所有Web接口都是资源。
2.使用SphU API
3.使用@SentinelResource
规则:
1.流量控制(FlowRule) 控制访问流量的大小等(限流)
2.熔断降级(DegradeRule) 防止服务雪崩,比如前面的配置兜底数据返回
3.系统保护(SystemRule)根据系统的cpu负载,内存使用率进行保护
4.来源访问控制(AuthorityRule)控制访问权限
5.热点参数(ParamFlowRule)定义常用数据
工作原理:

当用户访问资源时,如果配置了规则,就会引发Sentinel检查,如果没有违反,就放行。如果违反了,就抛出异常,如果有兜底,就走兜底的fallback逻辑,没有的话就是默认错误。
1)sentinel整合基础场景
1.下载并启动sentinel控制台
说明:没找到jar包直接下载的地址,以下方法供参考
下载地址:GitCode - 全球开发者的开源社区,开源代码托管平台

下载后,解压缩到本地

然后用idea打开项目

在控制台(Terminal),输入命令打包: mvn clean package '-Dmaven.test.skip=true'
打包成功后,进入项目的target文件夹,如下图可以看到已生成jar包
在当前路径,输入cmd,回车,打开


输入: java -jar sentinel-dashboard.jar
启动成功,在8080端口
浏览器输入:http://localhost:8080/
打开登陆页面,用户名和密码都输入sentinel

登录成功如下:
2.配置依赖
上一次我们把sentinel依赖引入了order服务,由于每个服务都需要这个依赖,因此我们把这个依赖删除,添加到order和product的父项目services中
java
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

粘贴到services的pom中,右上角刷新

引入依赖成功后,我们在微服务yml中配置下:
在order的yml中配置如下:
java
spring:
cloud:
openfeign:
client:
config:
default:
logger-level: full
connect-timeout: 3000
read-timeout: 5000
service-product:
logger-level: full
connect-timeout: 3000
read-timeout: 5000
sentinel:
transport:
dashboard: localhost:8080
eager: true
feign:
sentinel:
enabled: true

在product的properties中配置如下:
java
spring.application.name=service-product
server.port=9000
spring.cloud.nacos.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.import-check.enabled=false
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.eager=true

重启两个微服务,然后在页面上f5刷新下,可以看到控制台已经有了2个微服务的信息

3.基础场景
经过前面的步骤已经把控制台和微服务关联了。
我们定义一个资源:order微服务的创建订单方法:
java
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
DiscoveryClient discoveryClient;
@Autowired
RestTemplate restTemplate;
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
ProductFeignClient productFeignClient;
@SentinelResource(value="createOrder")
@Override
public Order createOrder(Long productId, Long userId) {
//Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);
//使用openfeign
Product product = productFeignClient.getProductById(productId);
Order order = new Order();
order.setId(1L);
order.setAddress("京海");
//金额
order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(product.getNum())));
order.setUserId(userId);
order.setNickName("张三");
order.setProductList(Arrays.asList(product));
return order;
}
//普通的调用
private Product getProductFromRemote(Long productId){
//获取所有商品微服务实例所在的ip和端口号
List<ServiceInstance> instanceList = discoveryClient.getInstances("service-product");
//获取第一个实例
ServiceInstance instance = instanceList.get(0);
//拼接远程调用地址 http://localhost:9000/product/4
String url = "http://"+instance.getHost()+":"+instance.getPort()+"/product/"+productId;
//打印日志
log.info("请求地址{}",url);
//给远程发送请求
Product pro = restTemplate.getForObject(url, Product.class);
return pro;
}
//LoadBalancerClient实现负载均衡
private Product getProductFromRemoteWithLoadBalancer(Long productId){
ServiceInstance instance = loadBalancerClient.choose("service-product");
//拼接远程调用地址 http://localhost:9000/product/4
String url = "http://"+instance.getHost()+":"+instance.getPort()+"/product/"+productId;
//打印日志
log.info("请求地址{}",url);
//给远程发送请求
Product pro = restTemplate.getForObject(url, Product.class);
return pro;
}
//注解实现负载均衡
private Product getProductFromRemoteWithLoadBalanceAnnotation(Long productId){
String url = "http://service-product/product/"+productId;
//给远程发送请求 此时 restTemplate会动态替换url的值
Product pro = restTemplate.getForObject(url, Product.class);
return pro;
}
}

添加注解@SentinelResource(value="createOrder")后,重启服务,然后浏览器输入:
http://localhost:8000/create?userId=5&productId=88 回车。
可以看到浏览器正常返回数据,同时sentinel控制台刷新后,可以看到刚才的调用方法已经被记录到簇点链路里面了。

上面可以看到,sentinel已经分析出这次调用,经过了/create接口,createOrder方法,还探测到了方法里面调用了product微服务http://service-product/product/{id}
下面我们配置下/create 这个接口的流控:

点击流控后打开页面如下:

把QPS的值设置为1。代表每秒的请求数量最多为1,多了就报错。点击新增按钮。

看到已经配置成功。
接下来验证:
打开页面输入http://localhost:8000/create?userId=5&productId=88
回车后,然后按f5刷新
如果是间隔1s或者更长,那么每次都返回正确的值。
如果1S内连续刷新页面,就报错了 被sentinel阻断,如下图

2)sentinel异常处理
上图中,我们看到,返回了错误信息:Blocked by Sentinel(flow limiting)。正常的话,这种信息是不标准的,我们一般是希望返回如下这种:
{
"code": 500,
"msg": "流量超限",
"data": ""
}

sentinel抛出的异常是BlockException,如上图。
打开项目,双击shift,打开搜索,输入BlockException,打开第一个
然后ctrl+h,打开blockexception的实现,如下图右侧
可以看到有5种方式。其实对应的就是下面的 5个功能

抛出异常后,会根据资源的类型,来处理异常。如上图,如果是web接口, 是用的sentinelwebinterceptor
1.web接口
在项目中双击shift,输入sentinelwebinterceptor,打开第一个



上面这个拦截器中的preHandle表示在调用接口之前执行这个方法,如果返回true,表示放行,如果返回false,就是被拦截了。打开handleBlockEception后,


如上看到这个错误信息是在这里打印的。 BlockExceptionHandler被DefaultBlockExceptionHandler继承后处理了这个业务逻辑。
下面我们写下新的处理逻辑
先定义一个返回标准json格式的类
在model模块创建一个com.atguigu.common.R 类。代码如下:
java
@Data
public class R {
private Integer code;
private String msg;
private Object data;
public static R ok(){
R r = new R();
r.setCode(200);
return r;
}
public static R ok(String msg,Object data){
R r = new R();
r.setCode(200);
r.setMsg(msg);
r.setData(data);
return r;
}
public static R error(){
R r = new R();
r.setCode(500);
return r;
}
public static R error(Integer code,String msg){
R r = new R();
r.setCode(code);
r.setMsg(msg);
return r;
}
}

然后我们建一个MyblockExceptionHandler类处理逻辑

输入 exception.MyblockExceptionHandler 回车

代码如下:
java
@Component
public class MyblockExceptionHandler implements BlockExceptionHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s, BlockException e) throws Exception {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
R error = R.error(500,s+"被sentinel限制了,原因是:"+e.getClass());
String result = objectMapper.writeValueAsString(error);
writer.write(result);
writer.flush();
writer.close();
}
}
重启微服务,这时候规则会失效,因为是保存在内存中的,需要重新配置下熔断规则

这时候,打开浏览器,输入 http://localhost:8000/create?userId=5&productId=88 回车
当频繁刷新机会触发流控规则,返回如下图:

2.@SentinelResource
上面我们配置的接口的规则,为了方便这个@SentinelResource 我们先把规则删除


上面的代码可知这是对应的方法,那么我们在控制台配置下这个方法的流控规则如下:

配置好后,我们频繁刷新http://localhost:8000/create?userId=5&productId=88
得到如下报错。

接下来我们把返回结果优化。
添加注解:
java
@SentinelResource(value="createOrder",blockHandler = "createOrderFallback")

创建一个跟blockHandler的值一样的方法:
java
public Order createOrderFallback(Long productId, Long userId, BlockException e){
Order order = new Order();
order.setId(0L);
order.setAddress("异常信息:"+e.getClass());
order.setTotalAmount(new BigDecimal("0"));
order.setUserId(userId);
order.setNickName("未知用户");
return order;
}

重启服务,然后重新配置规则:

配置后频繁刷新页面,得到如下返回:

3.OpenFeign
openfeign调用被限流后,如果有fallback的配置,就会走这个兜底回调,如下图:

如果没有,就会抛出异常。因为这个兜底我们之前写过了,下面我们直接验证:
先删掉原来的流控规则

接下来配置feign的流控规则:


浏览器输入http://localhost:8000/create?userId=5&productId=88
频繁刷新后,得到:

可以看到这个返回跟fallback的逻辑代码是一致的:

4.SphU硬编码
java
try {
SphU.entry("newRule");
//代码逻辑A
} catch (BlockException e) {
//处理违背规则后的业务逻辑
}
这个代码中国,newRule是资源名字,当代码逻辑A 的调用触发了资源对应的规则时,会抛出异常,捕获后再处理后续的业务逻辑,这是第四种方式处理异常。
3)sentinel流控规则
1.阈值类型

资源名:sentinel探测到的这个资源名
阈值类型: QPS: 每秒处理的请求数,一般都是设置QPS
并发线程数:线程的总数,涉及到需要限制线程池的线程的数量时使用
是否集群:

如上图,如果是单机均摊,设置为5,那么就是所有的实例都可以承受5,比如有3个实例,那就是一共3*5=15。

如上图,如果是 总体阈值,设置为5,那就是所有实例一共可以承受5,比如有3个实例,那就是5。
2.流控模式-直接
直接控制就是字面意思,最普通的控制
3.流控模式-链路

看上图,链路策略里面,资源B是最下端的,如果想对它配置一个规则,但是它的上游有2个地方都用到了它这个方法,通过资源A调用的资源B的话,这个规则不生效,通过资源C秒杀调用资源B的话,这个规则生效。如何配置规则呢?
如下配置:
先在sentinel控制台删除所有规则。
在order服务中配置一个秒杀的controller入口,跟创建订单入口,使用同一个后续的createOrder方法。
java
@RestController
public class OrderController {
/* @Value("${order.timeout}")
private String timeOut;
@Value("${order.auto-confirm}")
private String aotuConfirm;*/
@Autowired
OrderService orderService;
@Autowired
OrderProperties orderProperties;
//创建订单
@GetMapping("/create")
public Order createOrder(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
return order;
}
//获取配置
@GetMapping("/getConfig")
public String getConfig(){
//return "timeout: "+timeOut + "; auto-confirm" +aotuConfirm;
return "timeout: "+orderProperties.getTimeOut() + "; auto-confirm" +orderProperties.getAutoConfirm()
+"; dburl=" +orderProperties.getDbUrl();
}
//创建订单(秒杀)
@GetMapping("/seckill")
public Order seckill(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
order.setId(Long.MAX_VALUE);
return order;
}
}
yml文件配置下上下文不统一:
java
spring:
cloud:
openfeign:
client:
config:
default:
logger-level: full
connect-timeout: 3000
read-timeout: 5000
service-product:
logger-level: full
connect-timeout: 3000
read-timeout: 5000
sentinel:
transport:
dashboard: localhost:8080
eager: true
web-context-unify: false
feign:
sentinel:
enabled: true
重启服务,页面系统调用http://localhost:8000/seckill?userId=5&productId=88
刷新后,在调用http://localhost:8000/create?userId=5&productId=88
这时候在sentinel打开看到如下:

我们把第一个的流控打开,配置->高级选项->链路->入口资源填入/seckill,如下图

新增配置后,我们在页面输入 http://localhost:8000/create?userId=5&productId=88
频繁快速连续刷新后,发现每次都能返回值

但是当我们在页面输入 http://localhost:8000/seckill?userId=5&productId=88
然后频繁快速连续刷新后,发现会返回兜底值。

这就是配置了链路规则,触发时不对/create 进入的拦截,而是只对/seckill 进入的拦截
4.流控模式-关联

当读和写两个资源都有请求时,如果要保证优先写,当写的流量过大时,限制下读的请求,这就是关联策略。
先删除所有规则

然后编写读和写的请求代码如下:
java
@RestController
public class OrderController {
/* @Value("${order.timeout}")
private String timeOut;
@Value("${order.auto-confirm}")
private String aotuConfirm;*/
@Autowired
OrderService orderService;
@Autowired
OrderProperties orderProperties;
//创建订单
@GetMapping("/create")
public Order createOrder(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
return order;
}
//获取配置
@GetMapping("/getConfig")
public String getConfig(){
//return "timeout: "+timeOut + "; auto-confirm" +aotuConfirm;
return "timeout: "+orderProperties.getTimeOut() + "; auto-confirm" +orderProperties.getAutoConfirm()
+"; dburl=" +orderProperties.getDbUrl();
}
//创建订单(秒杀)
@GetMapping("/seckill")
public Order seckill(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
order.setId(Long.MAX_VALUE);
return order;
}
//读取请求
@GetMapping("/readDb")
public String readDb(){
return "readDb";
}
//写入请求
@GetMapping("/writeDb")
public String writeDb(){
return "writeDb";
}
}
重启服务,浏览器输入http://localhost:8000/readDb
得到如下结果:

浏览器开启无痕模式再打开一个窗口,输入 http://localhost:8000/writeDb
得到如下:

两个接口都返回数据了。
下面配置关联规则


如上图,配置阈值为1,关联资源是/writeDb,这个意思就是当关联资源/writeDb的阈值超过1时,对 /readDb进行限流。
接下来我们验证下:

如上图,频繁刷新writeDb页面,让其达到QPS为1,然后迅速刷新readDb

可以看到read被限流了。
普通的规则是直接对当前资源如果超过阈值,限流。
关联规则,也是对当前资源限流,不过条件变成了关联资源超过阈值。
5.流控效果-直接
先删除所有流控规则
编写代码打印日志:
java
@Slf4j
@RestController
public class OrderController {
/* @Value("${order.timeout}")
private String timeOut;
@Value("${order.auto-confirm}")
private String aotuConfirm;*/
@Autowired
OrderService orderService;
@Autowired
OrderProperties orderProperties;
//创建订单
@GetMapping("/create")
public Order createOrder(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
return order;
}
//获取配置
@GetMapping("/getConfig")
public String getConfig(){
//return "timeout: "+timeOut + "; auto-confirm" +aotuConfirm;
return "timeout: "+orderProperties.getTimeOut() + "; auto-confirm" +orderProperties.getAutoConfirm()
+"; dburl=" +orderProperties.getDbUrl();
}
//创建订单(秒杀)
@GetMapping("/seckill")
public Order seckill(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
order.setId(Long.MAX_VALUE);
return order;
}
//读取请求
@GetMapping("/readDb")
public String readDb(){
log.info("readDb...");
return "readDb";
}
//写入请求
@GetMapping("/writeDb")
public String writeDb(){
return "writeDb";
}
}
修改下处理拦截代码返回code429:
java
@Component
public class MyblockExceptionHandler implements BlockExceptionHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s, BlockException e) throws Exception {
httpServletResponse.setStatus(429);
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
R error = R.error(500,s+"被sentinel限制了,原因是:"+e.getClass());
String result = objectMapper.writeValueAsString(error);
writer.write(result);
writer.flush();
writer.close();
}
}
页面输入http://localhost:8000/readDb
这时候在后台配置下

这时选的流控效果是快速失败。当频繁刷新时就会返回错误数据。先打开f5控制台,然后刷新如下

看到错误码为429.
下面我们下载安装一个apipost工具,可以网络搜索下载,然后注册登录
点击新建接口:

选择http请求

输入localhost:8000/readDb
点击发送


看到返回了数据。

点击一键压测 并发数10,按压测时长5S。表示10个用户一直请求,持续5S

选择调试->后执行操作->断言

断言内容:响应码 等于 200 表示如果响应码是200表示成功。其他是失败
点击压测按钮:

看到访问请求17415,成功数7 。统计有少许误差,每秒请求3000+的情况,基本上绝大部分失败,每秒1次的成功。
6.流控效果-Warm Up
我们修改下设置如下

warm up指的是当配置好阈值后,比如说是QPS为10,预热时长3秒,但是一开始不会允许10,先允许2,然后过1秒允许4,3秒后增加到10。这就是起到一个缓冲的作用
点击压测:

看到5s成功了39次。看控制台:

可以看到第一秒4次请求,第二秒4次,第三秒5次,第四秒7次,第五秒10次。这就做到了冷启动。
7.流控效果-匀速排队

匀速排队指的是,当超过QPS的值2时,多出来的请求,不会立即失败,而是进入一个队列进行排队,然后按照顺序进行请求。但是当队列中的请求过多,如果轮到这个请求的时候超过1000毫秒秒这个设置时,那么这个请求就会被丢弃。
压测:


可以看到每秒2次的稳定调用
说明:只有快速失败的时候 支持流控模式中的关联和链路,Warm Up和排队不支持。
4)sentinel熔断规则
熔断降级(DegradeRule)

微服务之间调用的时候,如果服务G调用服务D的时候,发现服务D不稳定,那么如果不采取措施的话,会导致请求积压,从而造成雪崩效应。为了保证客户端G自身的系统功能,它需要主动切断不稳定调用,快速返回不积压。这就需要用到断路器。当断路器是闭合时,A可以调用B服务。当A调用B发现B服务不行时,那么会打开断路器一段时间。这段时间内,A是不会调用B的,而是返回默认数据。当这个时间过去后,A会把断路器半开,像B发送一次请求,如果B通了,那么就闭合断路器,后面继续访问,如果B还是没通,那就继续打开断路器一段时间。如此往复。
1.断路器工作原理

如上图,sentinel会统计一段时间内的请求情况,拿慢调用比例来说,就是返回的结果等待时间很长的请求,占到所有请求的比例到阈值,那么就会触发断路器打开,断路器打开的状态会持续一个固定时间(熔断时长),当在熔断时长之内时,所有的请求都会拒绝访问,返回默认值,只有过了熔断时间之后,断路器才会变成半开状态,这时候会主动请求一次,如果这次访问不是慢调用,那么就会把断路器关闭,后续的请求都可以访问,但是如果这次请求还是慢调用,那么就会再次触发断路器打开,并持续固定的熔断时长,如此循环。
2.熔断策略-慢调用比例
打开页面,输入http://localhost:8000/create?userId=5&productId=88
回车后,会返回内容。

在sentinel后台配置规则:


上图中,熔断策略选择慢调用比例。
最大RT:最大的响应时间,1000ms,超过这个时间就算作慢调用
比例阈值:0.5,即慢调用占所有调用的比例,50%
熔断时长:即断路器保持打开的时间,这个时间内不可调用
最小请求数:指这个规则触发的前提是必须大于5次请求,样本数量要多才会触发
统计时长:指的是慢调用比例统计的时间跨度。即统计5S内的请求
下面我们修改下product里面的获取商品的方法,将其睡眠2S,达到每次请求都会是慢调用的条件
java
@RestController
public class ProductController {
@Autowired
ProductService productService;
//查询商品
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable("id")Long productId, HttpServletRequest httpServletRequest){
String header = httpServletRequest.getHeader("X-Token");
System.out.println("hello token=【"+header+"】");
Product product = productService.getById(productId);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return product;
}
}
重启product微服务。注意这里不用重启order微服务。
打开页面输入http://localhost:8000/create?userId=5&productId=88
5秒内多次频繁刷新,可以看到一开始是正常返回的,但是后续触发了熔断,返回兜底数据


这就实现了慢调用比例熔断控制。
下面我们把product服务的睡眠2S给关闭下:
java
@RestController
public class ProductController {
@Autowired
ProductService productService;
//查询商品
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable("id")Long productId, HttpServletRequest httpServletRequest){
String header = httpServletRequest.getHeader("X-Token");
System.out.println("hello token=【"+header+"】");
Product product = productService.getById(productId);
/* try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}*/
return product;
}
}
重启product服务。然后页面输入http://localhost:8000/create?userId=5&productId=88
一直不停刷新,可以看到每次都返回正常数据,且不卡顿。

3.熔断策略-异常比例
前面我们配置慢调用比例的时候,每次调用,其实都是会返回正常数据的,只是每次返回的速度慢,触发了规则,导致进行兜底回调。也就是说即使不配置规则,流程也会通。但是为了快速响应,我们配置了规则。
如果调用feign的时候,product服务处理逻辑发生了异常,流程也会通,走feign配置的兜底回调。(稍后我们验证下这点)。但是熔断的作用就是为了快速响应,如果每次都去调product服务,它发生异常后,走兜底,这还是慢了,因此我们配置下熔断规则,符合规则时,触发熔断,在熔断时间内,不去调用product服务,而是直接走兜底,减少资源的浪费,加快响应速度。
验证product异常时的返回:
我们故意写一个运行错误 int i = 10/0;
java
@RestController
public class ProductController {
@Autowired
ProductService productService;
//查询商品
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable("id")Long productId, HttpServletRequest httpServletRequest){
String header = httpServletRequest.getHeader("X-Token");
System.out.println("hello token=【"+header+"】");
int i = 10/0;
Product product = productService.getById(productId);
/* try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}*/
return product;
}
}
然后重启product服务
浏览器输入http://localhost:8000/create?userId=5&productId=88
回车后,发现页面返回了兜底数据,order控制台走了兜底打印逻辑,product抛出了异常。


上面验证的其实就是上图中无熔断规则的情况,product服务报错了,触发了order服务的兜底回调逻辑。但是如果每次都调用product,抛异常,兜底回调,这样耗费时间和资源,所以我们配置个熔断规则,触发后,在熔断时间内,就不用调用product服务而是直接返回兜底回调,这样系统反应更快,雪崩几率大大降低。
下面我们配置下熔断规则:

熔断策略:异常比例
比例阈值:0.8 即80%的请求抛出了异常
熔断时长:30s。即触发规则后,断路器打开的时间,此时间内访问都不通,直接走兜底
最小请求数:5 指触发规则前提至少是5次以上的调用请求
统计时长:5000ms 即5s。即5S内至少调用了5次请求且异常比例大于80%,才会触发规则。
打开页面输入http://localhost:8000/create?userId=5&productId=88
回车后,看到页面返回了兜底数据,product抛出异常,order打印了兜底逻辑

当我们5s内不停刷新大于5次时,

可以看到order服务每次都触发了兜底回调,但是由于熔断规则触发,导致order并没有大量调用product服务,而是直接自行返回兜底逻辑的。(product控制台只有最初的少量异常抛出,触发熔断后,不调用product了,所以后续product就不打印异常了)
4.熔断策略-异常数

异常数配置规则跟异常比例是差不多的,唯一区别就是统计的是次数。配置如上。
浏览器输入:http://localhost:8000/create?userId=5&productId=88
回车后,频繁刷新10次以上,触发熔断。然后清空product和order的控制台的打印日志,便于后续观察。


清空日志后,由于触发了熔断,再次刷新页面,可以看到order服务有兜底日志,而product服务没有,说明熔断生效。

5)sentinel热点规则

热点就是资源当中的参数。比如配置axb为热点,QPS为5,那么如果axb的QPS超过了5,就触发限流。其他情况,非热点,或者热点但QPS值没有超过设置,则不触发限流。
问题1:每个用户秒杀QPS不得大于1(对userId限流)
问题2:6号用户是vvip,不限制QPS
问题3:666号商品已下架,不允许访问(对productId限流)
修改order服务controller中秒杀代码,添加注解和兜底逻辑:
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
public Order seckillFallback(Long userId, Long productId, BlockException e){
log.info("seckillFallback.....");
Order order = new Order();
order.setId(productId);
order.setUserId(userId);
order.setAddress("异常信息:"+e.getClass());
return order;
}
代码如下:
java
@Slf4j
@RestController
public class OrderController {
/* @Value("${order.timeout}")
private String timeOut;
@Value("${order.auto-confirm}")
private String aotuConfirm;*/
@Autowired
OrderService orderService;
@Autowired
OrderProperties orderProperties;
//创建订单
@GetMapping("/create")
public Order createOrder(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
return order;
}
//获取配置
@GetMapping("/getConfig")
public String getConfig(){
//return "timeout: "+timeOut + "; auto-confirm" +aotuConfirm;
return "timeout: "+orderProperties.getTimeOut() + "; auto-confirm" +orderProperties.getAutoConfirm()
+"; dburl=" +orderProperties.getDbUrl();
}
//创建订单(秒杀)
@GetMapping("/seckill")
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
public Order seckill(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
order.setId(Long.MAX_VALUE);
return order;
}
public Order seckillFallback(Long userId, Long productId, BlockException e){
log.info("seckillFallback.....");
Order order = new Order();
order.setId(productId);
order.setUserId(userId);
order.setAddress("异常信息:"+e.getClass());
return order;
}
//读取请求
@GetMapping("/readDb")
public String readDb(){
log.info("readDb...");
return "readDb";
}
//写入请求
@GetMapping("/writeDb")
public String writeDb(){
return "writeDb";
}
}
product微服务注释掉报错的代码:
// int i = 10/0;
java
@RestController
public class ProductController {
@Autowired
ProductService productService;
//查询商品
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable("id")Long productId, HttpServletRequest httpServletRequest){
String header = httpServletRequest.getHeader("X-Token");
System.out.println("hello token=【"+header+"】");
// int i = 10/0;
Product product = productService.getById(productId);
/* try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}*/
return product;
}
}
重启服务,页面输入http://localhost:8000/seckill?userId=5&productId=88
正常返回数据:

下面我们添加热点规则配置:


如上图,参数索引,0代表第一个参数,1代表第二个参数,以此类推。

问题1:每个用户秒杀QPS不得大于1(对userId限流),因此,如果要对userId限流,那么看下上面的代码,userId是在接口的第一个参数,因此参数索引填0。单击阈值1,统计窗口时长1秒,这样就符合QPS不得大于1这个条件。
现在验证:
打开页面,输入http://localhost:8000/seckill?userId=5&productId=88
频繁刷新,可以看到

说明配置生效了
注意:如果接口改为userId不是必传的,那么如果传的话,会拦截,如果不传,不拦截。可以尝试下。
验证如下:传userId:拦截:

不传userId,不拦截:

问题2:6号用户是vvip,不限制QPS
编辑规则:


如上图,userId是long类型,因此参数类型选long,参数值,因为是要对6号用户放开,那么就填6,限流阈值,因为是不限制,因此把阈值填大,1000000,就表示不限制了。点击添加,确定。

把浏览器页面上userId值改为6,频繁刷新,但是不会报错,说明6用户是vvip设置成功了。
如果把userId改为不是6,比如88,那么频繁刷新,会报错。如下图:

问题3:666号商品已下架,不允许访问(对productId限流)
这个是对productId限流,是方法中第二个参数

如上图,我们配置的是userId的规则,索引是0。productId因为是第二个参数,索引应该是1。但是一个规则只能配置一个参数,因此我们单独配置productId。


如上图,因为商品是第二个参数,所以索引填1,因为商品是需要被并发很大的量购买,因此我们把阈值设置很高,1000000,统计窗口时长设置为1秒,表示支持1000000的QPS。点击新增。

新增后,点击编辑,

如上图,高级选项,因为productId是long类型,所以选择long,因为是666号商品要下架,因此填666,因为666要下架,相当于这个商品不让购买了,因为阈值填0,表示如果有666的请求就拦截。配置好后点击添加,保存
把浏览器中商品id改为666,回车,发现被拦截,热点限制生效了。

6)fallback与blockhandler兜底回调

上图中,没有走兜底回调,返回了错误页面,不友好。

上图代码中,我们用的fallback,但是兜底函数用的是BlockException,这两个不对应。我们把fallback改为blockHandler
java
@Slf4j
@RestController
public class OrderController {
/* @Value("${order.timeout}")
private String timeOut;
@Value("${order.auto-confirm}")
private String aotuConfirm;*/
@Autowired
OrderService orderService;
@Autowired
OrderProperties orderProperties;
//创建订单
@GetMapping("/create")
public Order createOrder(@RequestParam("userId")Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
return order;
}
//获取配置
@GetMapping("/getConfig")
public String getConfig(){
//return "timeout: "+timeOut + "; auto-confirm" +aotuConfirm;
return "timeout: "+orderProperties.getTimeOut() + "; auto-confirm" +orderProperties.getAutoConfirm()
+"; dburl=" +orderProperties.getDbUrl();
}
//创建订单(秒杀)
@GetMapping("/seckill")
@SentinelResource(value = "seckill-order",blockHandler = "seckillFallback")
public Order seckill(@RequestParam(value = "userId",required = false)Long userId,@RequestParam("productId")Long productId){
Order order = orderService.createOrder(productId,userId);
order.setId(Long.MAX_VALUE);
return order;
}
public Order seckillFallback(Long userId, Long productId, BlockException e){
log.info("seckillFallback.....");
Order order = new Order();
order.setId(productId);
order.setUserId(userId);
order.setAddress("异常信息:"+e.getClass());
return order;
}
//读取请求
@GetMapping("/readDb")
public String readDb(){
log.info("readDb...");
return "readDb";
}
//写入请求
@GetMapping("/writeDb")
public String writeDb(){
return "writeDb";
}
}
重启服务,然后配置规则:

浏览器输入http://localhost:8000/seckill?userId=62&productId=6668

如上图,走兜底回调了。
还可以如下修改:

把注解改为fallback,把兜底函数改为Throwable。
重启服务,重新配置规则。然后页面刷新:

可以看到也走了兜底回调。
个人总结就是注解里面是fallback,那么对应的兜底函数用Throwable接收。
如果注解里面是blockHandler ,那么对应的兜底函数用BlockException接收
7)sentinel总结
系统规则和授权规则,由于不常用,而且有相关的替代,这里不多做详细了解了。
授权规则替代方案:网关、权限框架、业务流程。
系统规则替代方案:K8S
sentinel配置规则后,如果重启服务,配置的规则会消失。如何持久化,这个需要再找资料完善下。