高并发带来的问题
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
接下来我们通过一个案例,来模拟一下一个高并发的场景。
- 新建
HighConcurrencyController
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
@RestController @Slf4j @RequestMapping("/highConcurrency") public class HighConcurrencyController {
@Autowired private ProductFeignClient productFeignClient; @Autowired private OrderService orderService;
@GetMapping("/save/{pId}") public Order order(@PathVariable("pId") Integer pId, @RequestParam(name = "queryType") String queryType) { log.info(">>客户下单,这时候要调用商品微服务查询商品信息"); Product product = null; product = queryProductLoadBalancingByFeign(pId); log.info(">>商品信息,查询结果:" + JSON.toJSONString(product)); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } Order order = new Order(); order.setUId(1); order.setUserName("测试用户"); order.setPId(product.getPId()); order.setPName(product.getPName()); order.setPPrice(product.getPPrice()); order.setNumber(1); orderService.saveOrder(order); return order; }
@GetMapping("/getMessage") public String getMessage() { return "测试高并发"; }
private Product queryProductLoadBalancingByFeign(Integer pId) { ProductVO productVO = productFeignClient.queryProductInfoByProductId(pId); if (productVO != null) { Product product = new Product(); BeanUtils.copyProperties(productVO, product); return product; } return null; } }
|
1 2 3 4
| server: port: 8091 tomcat: max-threads: 10
|
本次使用jmeter进行压力测试,创建步骤为:
- 创建线程组,线程数量为20,间隔时间为1s,循环次数为:100次;
- 创建http请求,填写创建订单请求信息
- 创建查看结果树,查看请求结果
- 启动
结论:
此时会发现, 由于order方法囤积了大量请求, 导致message方法的访问出现了问题,这就是服务雪崩的雏形。
服务雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应”。
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
常见容错方案
要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措施, 下面介绍常见的服务容错思路和组件。
常见的容错思路
常见的容错思路有隔离、超时、限流、熔断、降级这几种,下面分别介绍一下。
隔离
它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离.
https://imgtu.com/i/W8Nehj
超时
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
限流
限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
熔断
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断一般有三种状态:
- 熔断关闭状态(Closed)
服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制 - 熔断开启状态(Open)
后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法 - 半熔断状态(Half-Open)
尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
降级
降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案。
常见的容错组件
Hystrix
Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
Resilience4J
Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
Sentinel
Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
组件对比
| Sentinel | Hystrix | resilience4j |
---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式、匀速器模式、预热排队模式 | :x: | 简单的RateLimiter |
系统自适应保护 | :white_check_mark: | :x: | :x: |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
Sentinel入门
什么是Sentinel
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景, 例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒级数据, 甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块, 例如与 Spring Cloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。 - 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo /Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
微服务集成Sentinel
为微服务集成Sentinel非常简单, 只需要加入Sentinel的依赖即可
下载jar包
访问sentinel的官方GitHub
仓库下载jar包
启动jar包
直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
1
| java -jar sentinel-dashboard-1.8.2.jar -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dporject.name=sentinel-dashboard
|
在测试高并发专用接口
HighConcurrencyController
中添加测试方法
1 2 3 4 5 6 7 8 9
| @RequestMapping("/message1") public String message1() { return "message1"; }
@RequestMapping("/message2") public String message2() { return "message2"; }
|
引入依赖
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
|
添加配置
1 2 3 4
| sentinel: transport: port: 9999 dashboard: localhost:8080
|
启动order服务测试
启动成功后访问地址:localhost:8080
,账号和密码默认都是sentinel,访问后你会发现服务信息为空白。
发送/highConcurrency/message1
请求,再次测试
补充:了解控制台的使用原理
Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上,即在微服务中指定控制台的地址, 并且还要开启一个跟控制台传递数据的端口, 控制台也可以通过此端口调用微服务中的监控程序获取微服务的各种信息。
入门案例[实现一个接口的限流]
https://imgtu.com/i/W8YnaQ
https://imgtu.com/i/W8Yl2q
https://imgtu.com/i/W8YNa4
Sentinel的概念和功能
基本概念
资源
资源就是Sentinel要保护的东西
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,可以是一个服务,也可以是一个方法,甚至可以是一段代码。
我们入门案例中的message1方法就可以认为是一个资源
规则
规则就是用来定义如何进行保护资源的
作用在资源之上, 定义以什么样的方式保护资源,主要包括流量控制规则、熔断降级规则以及系统保护规则。
我们入门案例中就是为message1资源设置了一种流控规则, 限制了进入message1的流量
重要功能
Sentinel的主要功能就是容错,主要体现为下面这三个:
流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
熔断降级
当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制
Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。 - 通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
Sentinel 和 Hystrix 的区别
两者的原则是一致的, 都是当一个资源出现问题时, 让其快速失败, 不要波及到其它服务但是在限制的手段上, 确采取了完全不一样的方法:
- Hystrix 采用的是线程池隔离的方式, 优点是做到了资源之间的隔离, 缺点是增加了线程切换的成本。
- Sentinel 采用的是通过并发线程的数量和响应时间来对资源做限制。
总之一句话: 我们需要做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能。
Sentinel规则
流控规则
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
点击簇点链路,我们就可以看到访问过的接口地址,然后点击对应的流控按钮,进入流控规则配置页面。新增流控规则界面如下:
https://imgtu.com/i/W8YBxx
针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制
- 阈值类型/单机阈值:
- QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流
- 线程数:当调用该接口的线程数达到阈值的时候,进行限流
- 是否集群:暂不需要集群
接下来我们以QPS为例来研究限流规则的配置。
简单配置
我们先做一个简单配置,设置阈值类型为QPS,单机阈值为3。即每秒请求量大于3的时候开始限流。
接下来,在流控规则页面就可以看到这个配置。
https://imgtu.com/i/W8YssK
然后快速访问message1
接口,观察效果。此时发现,当QPS > 3的时候,服务就不能正常响应,而是返回Blocked by Sentinel (flow limiting)结果。
配置流控模式
点击上面设置流控规则的编辑
按钮,然后在编辑页面点击高级选项
,会看到有流控模式一栏。
https://imgtu.com/i/W8YhRI
sentinel共有三种流控模式,分别是:
- 直接(默认):接口达到限流条件时,开启限流
- 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]
- 链路:当从某个接口过来的资源达到限流条件时,开启限流
下面呢分别演示三种模式.
直接流控模式
直接流控模式是最简单的模式,当指定的接口达到限流条件时开启限流。上面案例使用的就是直接流控模式。
关联流控模式
关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流。
- 配置限流规则, 将流控模式设置为关联,关联资源设置为的
message2
- 使用jmeter进行压力测试访问,注意QPS一定要大于0.3,即每秒请求大于等于三次。
- 第4步:访问
message1
,会发现已经被限流
[
链路流控模式
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
1 2 3 4
|
public void printMessage();
|
1 2 3 4 5
| @Override @SentinelResource("message") public void printMessage() { System.out.println("OrderService.printMessage"); }
|
- 在
message1
、message2
中分别增加该调用方法
1 2 3 4 5 6 7 8 9 10 11
| @RequestMapping("/message1") public String message1() { orderService.printMessage(); return "message1"; }
@RequestMapping("/message2") public String message2() { orderService.printMessage(); return "message2"; }
|
从1.6.3 版本开始,Sentinel Web filter默认收敛所有URL的入口context,因此链路限流不生效。
1.7.0 版本开始(对应SCA的2.1.1.RELEASE),官方在CommonFilter 引入了WEB_CONTEXT_UNIFY 参数,用于控制是否收敛context。将其配置为 false 即可根据不同的URL 进行链路限流。SCA 2.1.1.RELEASE之后的版本,可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛
1 2
| sentinel: web-context-unify: false
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Configuration public class FilterContextConfig {
@Bean public FilterRegistrationBean sentinelFilterRegistrationBean() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new CommonFilter()); registration.addUrlPatterns("/*"); registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY,"false"); registration.setName("sentinelFilter"); registration.setOrder(1); return registration;
}
}
|
https://imgtu.com/i/W8YzQ0
- 分别通过
message1
和message2
访问, 发现2没问题, 1的被限流了
配置流控效果
- 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
- Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。
- 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
配置降级规则
降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:
- 平均响应时间 :当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态。
如果接下来 1s 内持续进入 5 个请求,它们的 RT都持续超过这个阈值,那么在接下的时间窗口(以 s 为单位)之内,就会对这个方法进行服务降级。
https://imgtu.com/i/W8tio4
上述配置为当响应时间大于1毫秒的时候,接下来的十秒内该服务进行降级,十秒后回复正常,进行下一轮的判断。
注意:
Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
问题:
流控规则和降级规则返回的异常页面是一样的,我们怎么来区分到底是什么原因导致的呢?
配置热点规则
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上。
简单配置
编写代码
1 2 3 4 5
| @RequestMapping("/message3") @SentinelResource("messages3") public String message3(String name,Integer age) { return name + age; }
|
配置热点规则
热点规则增强使用
参数例外项允许对一个参数的具体值进行流控
授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源
访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
- 若配置白名单,则只有请求来源位于白名单内时才可通过;
- 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。
上面的资源名和授权类型不难理解,但是流控应用怎么填写呢?
其实这个位置要填写的是来源标识,Sentinel提供了RequestOriginParser
接口来处理来源。
只要Sentinel保护的接口资源被访问,Sentinel就会调用RequestOriginParser的实现类去解析访问来源。
下方有坑,请注意!!!!
==注意:由于上方配置流控规则是把收敛的接口都开放了,博猪在实现RequestOriginParser
接口处理来源,配置黑白名单时不生效,这个具体原因博猪还没进一步查看源码实现逻辑,可能是sentinel的一个bug,所以需要把注入的FilterRegistrationBean
去掉,配置文件中的WEB_CONTEXT_UNIFY
去掉!!==
自定义处理规则
1 2 3 4 5 6 7 8
| @Component public class ServiceNameOriginParser implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest httpServletRequest) { String serviceName = httpServletRequest.getParameter("serviceName"); return serviceName; } }
|
配置授权规则
解释一下配置含义:当seriviceName=pc时不能访问(黑名单)
访问配置地址,查看结果
访问http://localhost:8091/highConcurrency/getMessage?serviceName=pc,查看结果
系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
- CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。
拓展-自定义异常返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @Component public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { response.setContentType("application/json;charset=utf-8"); ResponseData responseData = null; if (e instanceof FlowException) { responseData = new ResponseData(4001, "接口被限流!"); } else if (e instanceof DegradeException) { responseData = new ResponseData(4002, "接口被降级!"); } else if (e instanceof ParamFlowException) { responseData = new ResponseData(4003, "接口参数被限流!"); } else if (e instanceof AuthorityException) { responseData = new ResponseData(4004, "接口授权异常!"); } else if (e instanceof SystemBlockException) { responseData = new ResponseData(4005, "系统负载异常!"); } response.getWriter().write(JSONObject.toJSONString(responseData)); } }
@Data @AllArgsConstructor @NoArgsConstructor class ResponseData { private int code; private String message; }
|
Sentinel注解
SentinelResource
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能通过@SentinelResource
来指定出现异常时的处理策略。@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback 配置项。其主要参数如下:
属性 | 作用 |
---|
value | 资源名称 |
entryType | entry类型,标记流量的方向,取值IN/OUT ,默认是OUT |
blockHandler | 处理BlockException的函数名称,函数要求: 1.必须是 public 2. 返回类型 参数与原方法一致 3.默认需和原方法在同一个类中。若希望使用其他类的函数,可配置blockHandlerClass ,并指定blockHandlerClass 里面的方法。 |
blockHandlerClass | 存放fallback的类。对应的处理函数必须static修饰。 |
fallback | 用于在抛出异常的时候提供fallback处理逻辑。 fallback函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。 函数要求: 1. 返回类型与原方法一致 2. 参数类型需要和原方法相匹配 3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定fallbackClass里面的方法。 |
fallbackClass | 存放fallback的类。对应的处理函数必须static修饰。 |
defaultFallback | 用于通用的 fallback 逻辑。 默认fallback函数可以针对所有类型的异常进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。 函数要求: 1. 返回类型与原方法一致 2. 方法参数列表为空,或者有一个 Throwable 类型的参数。 3.默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定 fallbackClass 里面的方法。 |
exceptionsToIgnore | 指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。 |
exceptionsToTrace | 需要trace的异常 |
下面通过代码案例演示
限流降级定义在同一个类里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
@SentinelResource(value = "message4", blockHandler = "message4BlockHandler", fallback = "message4Fallback") @GetMapping("/message4") public String message4() { ERROR_COUNT++; if (ERROR_COUNT % 3 == 0) { throw new RuntimeException(); } else { return "message4"; } }
public String message4BlockHandler(BlockException e) { log.error("message4 接口被限流或者被降级了!,exception info is:", e); return "message4 接口被限流或者被降级了!"; }
public String message4Fallback(Throwable e) { log.error("message4 接口异常!,throwable info is:", e); return "message4 接口异常!"; }
|
- 运行
- 增加流程规则,简单设置个QPS限制
- 请求接口,测试。
通过请求接口我们发现,throw出来的RuntimeException打印和返回的是message4Fallback
的信息,sentinel的QPS的异常信息打印和返回是message4BlockHandler
.
- ==注意blockHandler里面的方法名一定要和实现异常处理的方法名一致,且返回类型一致!==
- ==注意sentinel的
SentinelResource
的两个属性的实现方法是可以使用参数的,但是必须包含异常信息的参数,并且,blockHandler的异常只能是BlockException
,其他类型则会自动进入fallback
里面== SentinelResource
的异常高于自定义全局异常捕获机智!
将限流降级放在单独的类里面
抽取限流方法
1 2 3 4 5 6 7 8
| @Slf4j public class HighConcurrencyBlockHandler {
public static String message4BlockHandler(BlockException e) { log.error("message4 接口被限流或者被降级了,HighConcurrencyBlockHandler!,exception info is:", e); return "message4 接口被限流或者被降级了,HighConcurrencyBlockHandler!"; } }
|
抽取fallback方法
1 2 3 4 5 6 7 8
| @Slf4j public class HighConcurrencyFallback {
public static String message4Fallback(Throwable e) { log.error("message4 接口异常,HighConcurrencyFallback!,throwable info is:", e); return "message4 接口异常,HighConcurrencyFallback!"; } }
|
改造资源保护方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@SentinelResource(value = "message4", blockHandler = "message4BlockHandler", fallback = "message4Fallback", blockHandlerClass = HighConcurrencyBlockHandler.class, fallbackClass = HighConcurrencyFallback.class) @GetMapping("/message4") public String message4() { ERROR_COUNT++; if (ERROR_COUNT % 3 == 0) { throw new RuntimeException(); } else { return "message4"; } }
|
- 运行
- 增加流程规则,简单设置个QPS限制
- 请求接口,测试。
Sentinel规则持久化
通过前面的讲解,我们已经知道,可以通过Dashboard来为每个Sentinel客户端设置各种各样的规则,但是这里有一个问题,就是这些规则默认是存放在内存中,极不稳定,所以需要将其持久化。
本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则。以本地文件数据源为例,推送过程如下图所示:
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。
实现实例化功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
|
public class SentinelPersistence implements InitFunc { @Override public void init() throws Exception { String ruleDir = System.getProperty("user.home") + "/sentinel-rules/"; String flowRulePath = ruleDir + "/flow-rule.json"; String degradeRulePath = ruleDir + "/degrade-rule.json"; String systemRulePath = ruleDir + "/system-rule.json"; String authorityRulePath = ruleDir + "/authority-rule.json"; String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdir(ruleDir); this.createFile(flowRulePath); this.createFile(degradeRulePath); this.createFile(systemRulePath); this.createFile(authorityRulePath); this.createFile(paramFlowRulePath);
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>( flowRulePath, flowRuleListParser ); FlowRuleManager.register2Property(flowRuleRDS.getProperty()); WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>( flowRulePath, this::encodeJson ); WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>( degradeRulePath, degradeRuleListParser ); DegradeRuleManager.register2Property(degradeRuleRDS.getProperty()); WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>( degradeRulePath, this::encodeJson ); WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>( systemRulePath, systemRuleListParser ); SystemRuleManager.register2Property(systemRuleRDS.getProperty()); WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>( systemRulePath, this::encodeJson ); WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>( authorityRulePath, authorityRuleListParser ); AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty()); WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>( authorityRulePath, this::encodeJson ); WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>( paramFlowRulePath, paramFlowRuleListParser ); ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty()); WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>( paramFlowRulePath, this::encodeJson ); ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS); }
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<FlowRule>>() { } ); private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<DegradeRule>>() { } ); private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<SystemRule>>() { } );
private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<AuthorityRule>>() { } );
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject( source, new TypeReference<List<ParamFlowRule>>() { } );
private void mkdir(String filePath) throws IOException { File file = new File(filePath); if (!file.exists()) { file.mkdirs(); } }
private void createFile(String filePath) throws IOException { File file = new File(filePath); if (!file.exists()) { file.createNewFile(); } }
private <T> String encodeJson(T t) { return JSONObject.toJSONString(t); } }
|
添加配置
- 在
resource
下面创建配置目录META-INF/services
- 添加文件
com.alibaba.csp.sentinel.init.InitFunc
- 在文件中添加配置类的全路径
==该实例化存在一个严重的问题,我为了这个配置类的易用性,增加注入了一个spring的spring.application.name
,但是不知道为啥,项目启动后改配置读取不到,显示为null,后经过debug发现的sentinel相关默认实现添加了spring的order排序,但是博猪更改排序后还是无效,这个疑问还请有踩过坑的,麻烦解读一下==
Feign整合Sentinel
引入sentinel的依赖
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
|
配置开启feign
1 2 3
| feign: sentinel: enabled: true
|
创建产品feign-api项目
创建shop-product-api
模块,jar包形式
增加相关依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties>
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
|
创建feignClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@FeignClient(name = "shop-product" , fallback = ProductFeignClientFallback.class) public interface ProductFeignClient {
public static final String DEFAULT_FALLBACK_MSG = "服务不可用!";
@GetMapping("/api/queryProductInfoByProductId/{productId}") public ProductVO queryProductInfoByProductId(@PathVariable Integer productId);
}
|
创建fallback
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
@Component @Slf4j public class ProductFeignClientFallback implements ProductFeignClient {
@Override public ProductVO queryProductInfoByProductId(Integer productId) { log.error(DEFAULT_FALLBACK_MSG); return null; } }
|
创建feignClientVO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@Data public class ProductVO implements Serializable {
private static final long serialVersionUID = 1L;
private Integer pId; private String pName; private Double pPrice; private Integer stock; }
|
产品服务增加相关依赖
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>com.letcoding</groupId> <artifactId>shop-product-api</artifactId> <version>1.0.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclusion> </exclusions> </dependency>
|
修改订单服务
订单服务开启feign,修改订单启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@SpringBootApplication @EnableDiscoveryClient @ComponentScan(basePackages = {"com.letcoding.order","com.letcoding.product"}) @EnableFeignClients(basePackages = "com.letcoding.product") public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
|
启动测试
- 启动产品服务、订单服务,创建订单试一下
- 停止订单服务,再次下订单
==简单说明一下,个人对于feign编码的习惯,还是遵循低耦合的原则,尽量不适用公共的vo等,同时尽量精简定义feign的相关对象,提供http的传输效率,因为feign的底层也是http请求,所以过大的请求和各服务之间的相互调用,对于feign这一块还是要注意的!==
通过测试,我们发现,该方式请求不会正确的抛出异常来,所以如果想知道具体的fallback异常,请使用一下形式进行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@Component @Slf4j public class ProductFeignClientFallbackFactory implements FallbackFactory<ProductFeignClient> { @Override public ProductFeignClient create(Throwable cause) { log.error("FallbackFactory,服务不可用!",cause); return null; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@FeignClient(name = "shop-product" // , fallback = ProductFeignClientFallback.class , fallbackFactory = ProductFeignClientFallbackFactory.class) public interface ProductFeignClient {
public static final String DEFAULT_FALLBACK_MSG = "服务不可用!";
@GetMapping("/api/queryProductInfoByProductId/{productId}") public ProductVO queryProductInfoByProductId(@PathVariable Integer productId);
}
|
==注意: fallback和fallbackFactory只能使用其中一种方式==