SpringCloud学习笔记-4
声明:笔记来源于网络,如有侵权联系删除
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中
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
粘贴到services的pom中,右上角刷新
引入依赖成功后,我们在微服务yml中配置下:
在order的yml中配置如下:
spring:cloud:openfeign:client:config:default:logger-level: fullconnect-timeout: 3000read-timeout: 5000service-product:logger-level: fullconnect-timeout: 3000read-timeout: 5000sentinel:transport:dashboard: localhost:8080eager: true
feign:sentinel:enabled: true
在product的properties中配置如下:
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=falsespring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.eager=true
重启两个微服务,然后在页面上f5刷新下,可以看到控制台已经有了2个微服务的信息
3.基础场景
经过前面的步骤已经把控制台和微服务关联了。
我们定义一个资源:order微服务的创建订单方法:
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {@AutowiredDiscoveryClient discoveryClient;@AutowiredRestTemplate restTemplate;@AutowiredLoadBalancerClient loadBalancerClient;@AutowiredProductFeignClient productFeignClient;@SentinelResource(value="createOrder")@Overridepublic Order createOrder(Long productId, Long userId) {//Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);//使用openfeignProduct 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/4String 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/4String 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 类。代码如下:
@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 回车
代码如下:
@Component
public class MyblockExceptionHandler implements BlockExceptionHandler {private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic 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
得到如下报错。
接下来我们把返回结果优化。
添加注解:
@SentinelResource(value="createOrder",blockHandler = "createOrderFallback")
创建一个跟blockHandler的值一样的方法:
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硬编码
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方法。
@RestController
public class OrderController {
/* @Value("${order.timeout}")private String timeOut;@Value("${order.auto-confirm}")private String aotuConfirm;*/@AutowiredOrderService orderService;@AutowiredOrderProperties 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文件配置下上下文不统一:
spring:cloud:openfeign:client:config:default:logger-level: fullconnect-timeout: 3000read-timeout: 5000service-product:logger-level: fullconnect-timeout: 3000read-timeout: 5000sentinel:transport:dashboard: localhost:8080eager: trueweb-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.流控模式-关联
当读和写两个资源都有请求时,如果要保证优先写,当写的流量过大时,限制下读的请求,这就是关联策略。
先删除所有规则
然后编写读和写的请求代码如下:
@RestController
public class OrderController {
/* @Value("${order.timeout}")private String timeOut;@Value("${order.auto-confirm}")private String aotuConfirm;*/@AutowiredOrderService orderService;@AutowiredOrderProperties 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.流控效果-直接
先删除所有流控规则
编写代码打印日志:
@Slf4j
@RestController
public class OrderController {
/* @Value("${order.timeout}")private String timeOut;@Value("${order.auto-confirm}")private String aotuConfirm;*/@AutowiredOrderService orderService;@AutowiredOrderProperties 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:
@Component
public class MyblockExceptionHandler implements BlockExceptionHandler {private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic 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,达到每次请求都会是慢调用的条件
@RestController
public class ProductController {@AutowiredProductService 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给关闭下:
@RestController
public class ProductController {@AutowiredProductService 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;
@RestController
public class ProductController {@AutowiredProductService 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; }
代码如下:
@Slf4j
@RestController
public class OrderController {
/* @Value("${order.timeout}")private String timeOut;@Value("${order.auto-confirm}")private String aotuConfirm;*/@AutowiredOrderService orderService;@AutowiredOrderProperties 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;
@RestController
public class ProductController {@AutowiredProductService 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
@Slf4j
@RestController
public class OrderController {
/* @Value("${order.timeout}")private String timeOut;@Value("${order.auto-confirm}")private String aotuConfirm;*/@AutowiredOrderService orderService;@AutowiredOrderProperties 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配置规则后,如果重启服务,配置的规则会消失。如何持久化,这个需要再找资料完善下。