SpringCloud【Sentinel】
1,工作原理
2,常见规则
流量控制(FlowRule)
1,阈值类型设置如下
单机均摊:每个机器均摊,比如阈值填5,三个机器,就个机器都可以有5个
总体阈值:所有机器总阈值,比如阈值填5,三个机器,总共进5个请求
2,流控模式设置如下
只有流控效果是快速失败才支持调整流控模式
直接:默认,限制请求直接对资源进行限制
关联:数据库读写,当写比较大时,限制读的限流,达到优先写
关联测试案例
先添加俩方法,一个读一个写,在orderControoler类中
@GetMapping("/readDb")public String readDb(){return "readDb.....";}@GetMapping("/writeDb")public String writeDb(){return "writeDb.....";}
然后调用俩接口,在dashborard中查看俩资源,进行如下设置,即在读的资源下设置限流,流控模式为关联,关联资源为writeDb,则可实现写量过大时,访问读会被限流(设置好之后,测试先访问readDb,正常访问也没被限流,但再访问writeDb的时候,疯狂刷新,再回去访问readDb则readDb就会被限流)
链路: 根据不同的调用链,来控制不同调用链
链路测试案例
添加一个seckill方法,同createOrder,在orderController类中
@GetMapping("/create")public Order createOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId){Order order = orderService.createOrder(userId,productId);return order;}@GetMapping("/seckill")public Order seckill(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId){Order order = orderService.createOrder(userId,productId);order.setId(999l);return order;}
yml配置文件添加配置,取消统一上下文
spring:cloud:sentinel:web-context-unify: false #是否统一web上下文(默认true)
配置链路规则:需要对createOrder限流,则如下设置,设置完之后测试,分别测试create接口和seckill接口,则会发现create接口随便刷新无误,seckill接口就有了一秒一个的限制
3,流控效果设置如下
快速失败:(默认)如果没有超阈值,就交给业务处理,如果超过了,多余的请求就会抛出blockHandler异常(比如每秒一个,哪么这一秒其他多的请求会被直接丢弃)
Warm Up:leng'q如果预见超高峰流量即将到达,设置参数参数QPS(每秒通过几个请求),period(冷启动周期),流程比如QPS=10,period=3,请求是一个稳步上升的趋势,而不是突然增加(每秒多余的请求会被丢弃)
排队等待:匀速排队,比如QPS=2,则每秒会通过2个请求,但其他请求不会被丢弃,而是在后面排队,等前面的请求,排队也有最大等待时间,timeout=100,超过最大等待时间也会被丢弃,如下每秒2个请求
熔断降级(DegradeRule)
思想:如下图的复杂调用关系,A->G->D,B->F->D,如果此时D不稳定,一直不返回结果,那么如果G一直去等D的结果,D也会卡住,同样A也会卡住,此时我们就需要及时切断这种不稳定的调用,当G感知到D调用很慢之后,后面就采取措施不调用或者快速返回,切断跟D的联系,快速返回错误,整个链路才会快速结束,请求不积压,则不会产生服务雪崩问题
此中间就有一个断路器的概念,当调用的B是稳定通畅的,则断路器是关闭的,一旦B出现了问题,则A就会打开,怎么知道什么时候该打开呢,此时就会有一个半开状态,就是稍微开一下,测一下当前状态如果调用不慢了,则闭合,如果调用仍然慢,则打开
断路器工作原理如下:
首先默认断路器是关闭的,调用是通的,此时我们设置慢调用比例(熔断策略:满调用比例/异常比例/异常数),比如设置0.7,就是请求70%都是慢请求(慢调用设置时长阈值)的话,就打开断路器,调用就会失败,但也不能一直打开,此时就会有一个熔断时长,比如30s,这30s以内的所有请求不调用,直接返回失败,但30s之后熔断窗口就结束,结束后断路器就会变成半开状态,A需要调用B就会放一个请求过去探测一下,调用成功,则B又可靠了,断路器闭合,如果还是调用失败或者慢,则断路器还是打开
熔断策略测试案例
慢调用比例
首先运行Order和Product服务,然后给CreateOrder设置熔断规则如下
修改一下商品服务,加个睡眠时长2秒,达到慢请求要求
在productController中的方法里加入睡眠时长
@GetMapping("/getProductById/{productId}")public Product getProductById(@PathVariable("productId") Long productId, HttpServletRequest request) {String header = request.getHeader("X-Token");System.out.println("product==="+header);try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}Product product = productService.getProductById(productId);return product;}
}
然后测试,调用createOrder接口, 先5秒内刷新超过5个请求吧,开始的请求会比较慢,5秒后就会很快,应为断路器打开了,就不再调用了,后台也不再打印,order服务就会返回兜底回调,30秒之后,在测试刷新接口,会有一个慢请求之后,又会变快,因为半开测试之后,断路器仍旧打开了,选择不调用。去掉getProductById里面的睡眠,在重启商品服务,熔断规则还在,然后再测试接口调用,则正常调用,因为不存在慢调用问题
异常比例
同上构造异常调用
修改一下商品服务,加一个异常
在productController中的方法里加入异常代码
@GetMapping("/getProductById/{productId}")public Product getProductById(@PathVariable("productId") Long productId, HttpServletRequest request) {String header = request.getHeader("X-Token");System.out.println("product==="+header);
// try {
// Thread.sleep(2000);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }int a=10/0;Product product = productService.getProductById(productId);return product;}
然后同上测试方法进行测试看效果
当没有加熔断的时候通过后天打印可以看出,每次A调用B 之后,都会返回异常,但是每次还是会调用,加了熔断规则之后,达到规则之后,30s内就不会再调用,就直接执行兜底回调,不用再走一遍失败的路
异常数
测试也同上面一样
系统保护(SystemRule)
来源访问控制(AuthorityRule)
热点参数(ParamFlowRule)
类似流控规则,只是更细话到参数上面的限制
测试案例如下
场景1:每个用户秒杀QPS不得超过1(秒杀下单userId级别)
我们可以先写一个资源,在orderController中
//添加上sentinel资源注解@GetMapping("/seckill")@SentinelResource(value = "seckill-order",blockHandler = "seckillFallback")public Order seckill(@RequestParam(value = "userId",required = false) Long userId, @RequestParam(value = "productId",required = false) Long productId){Order order = orderService.createOrder(userId,productId);order.setId(999l);return order;}public Order seckillFallback(Long userId, Long productId, BlockException e){Order order = new Order();order.setId(-1l);order.setTotalAmount(BigDecimal.ZERO);order.setAddress("异常信息-----");return order;}
启动订单服务,设置热点参数规则
有参数userId,快速刷新效果,会限制1秒一个
去掉userId,快速刷新效果,不会限制
场景2:6号是vip用户,需要放行,不限制QPS
场景3:666号商品已经下架,不可访问
此时需要对另外一个参数进行限流,则需要再加一个限流规则
3,基础场景
1,下载sentinel-dashboard控制台
https://github.com/alibaba/Sentinel/releases/tag/1.8.8
Sentinel-Dashboard 是 阿里巴巴 开源的 流量控制组件 Sentinel的 可视化控制台 ,主要用于 微服务架构 的流量治理,支持实时监控、动态配置规则、熔断降级等功能
下载好之后,在文件夹中cmd,开启命令模式
然后输入java -jar sentinel-dashboard-1.8.8.jar启动
启动之后,浏览器输入http://localhost:8080/ 即可访问,账密sentinel sentinel
2,添加配置
spring:cloud:sentinel:transport:dashboard: localhost:8080 #dashboard控制台eager: true #提前加载(本来是访问了请求才会加载,此时项目启动就会连接加载)
3,启动服务之后,查看控制台
4,在OrderServiceImpl得createOrder方法上添加注解@SentinelResource(value = "createOrder")
@SentinelResource(value = "createOrder")@Overridepublic Order createOrder(Long userId, Long productId) {
// Product product = getProductByRemoteWithLoadBalancerByAnnotatin(productId);Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1l);//todo 远程调用order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("爱学习");order.setAddress("北京");//todo 远程调用order.setProductList(Arrays.asList(product));return order;}
5,重新启动订单服务,然后调用create接口,查看控制台
6,测试规则---流控
7,然后调用createorder接口,一秒一刷则无妨,但一秒超过一次请求就会报错
4,异常处理
如上会默认返回一个流控错误字符串,我们需要返回json数据,返回错误消息和数据则就需要异常处理机制,常见得集中异常和处理方式如下:
1,Web接口
-----实现:AbstractSentinelInterceptor
异常处理,我们自己写个异常处理类,实现BlockExceptionHandler,添加到容器
新增类MyBlockExceptionHandler
package org.example.order.exception;import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.common.R;
import org.springframework.stereotype.Component;import java.io.PrintWriter;@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response,String resourceName, BlockException e) throws Exception {response.setContentType("application/json;charset=utf-8");PrintWriter writer = response.getWriter();R error = R.error(500, resourceName + "被Sentinel限制了" + e.getClass());//写出jsonString s = objectMapper.writeValueAsString(error);writer.write(s);writer.flush();writer.close();}
}
新增一个异常已处理对象R
在公共model模块添加对象
package org.example.common;import lombok.Data;@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(Object data, String msg){R r = new R();r.setCode(200);r.setData(data);r.setMsg(msg);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;}}
然后重新启动,在dashboard中加入流控规则,调用接口,快速刷新即可看到以下报错
2,@SentinelResource
------实现:SentinelResourceAspect
@SentinelResource一般标注在非controller层,一旦违反规则,如果业务规定有回调数据,那就用blockHandle去指定兜底回调,如果没有指定回调,就让异常抛给全局(springboot全局异常处理器),此注解得处理(SentinelResourceAspect)
回调处理
1,在方法上加上注解
@SentinelResource(value = "createOrder",blockHandler = "createOrderFallback")
2,添加回调方法
加上回调得方法,方法名通上面得注解里面blockHandler,其他内容需通上方注解方法,public、返回、参数
OrderServiceImpl
//增加回调createOrderFallback@SentinelResource(value = "createOrder",blockHandler = "createOrderFallback")@Overridepublic Order createOrder(Long userId, Long productId) {
// Product product = getProductByRemoteWithLoadBalancerByAnnotatin(productId);Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1l);//todo 远程调用order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("爱学习");order.setAddress("北京");//todo 远程调用order.setProductList(Arrays.asList(product));return order;}//同一个类中,加上回调得方法,方法名通上面得注解里面blockHandler,其他内容需通上方注解方法,public、返回、参数public Order createOrderFallback(Long userId, Long productId, BlockException e) {Order order = new Order();order.setId(0l);order.setTotalAmount(BigDecimal.ZERO);order.setUserId(userId);order.setNickName("未知用户");order.setAddress("异常信息:"+e.getClass());return order;}
添加流控规则
测试快速刷新的异常处理结果
3,OpenFeign调用
-----实现:SentinelFeignAutoConfiguration
在openFeign笔记中提到过兜底回调,此处即用
首先编写一个兜底返回org.example.order.feign.fallback.ProductFeignClientFallback
实现ProductFeignClient 接口
package org.example.order.feign.fallback;
import java.math.BigDecimal;import org.example.order.feign.ProductFeignClient;
import org.example.producet.domain.Product;
import org.springframework.stereotype.Component;@Component
public class ProductFeignClientFallback implements ProductFeignClient {@Overridepublic Product getProductById(Long id) {Product product = new Product();product.setId(id);product.setPrice(new BigDecimal("0"));product.setProducetName("兜底回调的数据");product.setNum(0);return product;}
}
在ProductFeignClient 接口中添加fallback = ProductFeignClientFallback.class,这时,在远程调用成功则不管,如果调用失败,则会调用fallback,返回ProductFeignClientFallback里面编写的默认数据
package org.example.order.feign;import org.example.order.feign.fallback.ProductFeignClientFallback;
import org.example.producet.domain.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;//需要调用的服务的名字
@FeignClient(value = "services-product",fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {@GetMapping("/getProductById/{id}")Product getProductById(@PathVariable("id") Long id);}
添加流控规则
测试异常回调处理结果
同样,如果此处没有些回调函数,就会抛给springboot全局异常处理
4,Sphu硬编码
以上三种异常处理的源码都会有的编码
SphU.entry(resourceName);
我们也可自定义比如
try {Entry hahah = SphU.entry("hahah");order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("爱学习");order.setAddress("北京");} catch (BlockException e) {throw new RuntimeException(e);}