一、服务雪崩
问题描述
我们的系统由微服务架构组成,A调用B,B调用C,C调用D;在正常情况下,A、B、C、D都是正常的;
当某个时间点服务D突然挂掉了,此时的服务C还在疯狂的调用服务D,由于D已经挂掉了,所以服务C调用服务D必须等待服务超时。而每次的C去调用服务D的时候都会创建线程,高并发的场景C就会阻塞大量的线程,那么服务C就会创建大量的线程,当到达一定的程度,服务C也就宕机了。(由于服务D挂掉,导致服务C也跟着宕机了)
就这样,服务B和服务A也跟着会挂掉,造成服务雪崩。
解决方法
方法一:请求超时设置
配置一下请求超时时间,例如:每次请求在1秒内必须返回,否则到点就把线程结束,释放资源。释放资源的速度过快,也不会导致服务被拖死。
(1)设置RestTemplate的超时时间
@Configuration public class WebConfig { @Bean public RestTemplate restTemplate() { //设置restTemplate的超时时间 SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout(1000); requestFactory.setConnectTimeout(1000); RestTemplate restTemplate = new RestTemplate(requestFactory); return restTemplate; } }
(2)进行超时异常处理
try{ ResponseEntity<ProductInfo> responseEntity= restTemplate.getForEntity(uri+orderInfo.getProductNo(), ProductInfo.class); productInfo = responseEntity.getBody(); } catch (Exception e) { throw new RuntimeException("调用超时"); }
(3)设置全局异常处理
@ControllerAdvice public class TulingExceptionHandler { @ExceptionHandler(value = {RuntimeException.class}) @ResponseBody public Object dealBizException() { OrderVo orderVo = new OrderVo(); orderVo.setOrderNo("‐1"); orderVo.setUserName("容错用户"); return orderVo; } }
方法二:线程池隔离模式
M类使用线程池1,N类使用线程池2,彼此的线程池不同,并且为每个类分配的线程池大小。
M类调用B服务,N类调用C服务,如果M类和N类使用相同的线程池,那么如果B服务挂了,M类调用B服务的接口并发又很高,你又没有任何保护措施,你的服务就很可能被M类拖死。而如果M类有自己的线程池,N类也有自己的线程池,如果B服务挂了,M类顶多是将自己的线程池占满,不会影响N类的线程池,于是N类依然能正常工作。
方法三:断路器模式
如果发现在一定时间内失败次数或失败率达到一定阈值,就“跳闸”,断路器打开——此时,请求直接返回,而不去调用原本调用的逻辑。
在跳闸一段时间后(例如15秒),断路器会进入半开状态,这是一个瞬间态,此时允许一次请求再去调用,如果成功,则断路器关闭,应用正常调用;如果调用依然不成功,断路器继续回到打开状态,过段时间再进入半开状态尝试——通过”跳闸“,应用可以保护自己,而且避免浪费资源;而通过半开的设计,可实现应用的“自我修复“。
二、Sentinel 介绍及使用
官网文档:https://github.com/alibaba/Sentinel/wiki/介绍
1、Sentinel 是什么?
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
2、Sentinel的使用
1、引入maven坐标
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-core</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-annotation-aspectj</artifactId> <version>1.7.1</version> </dependency>
2、配置 AspectJ切面 SentinelResourceAspect
@Configuration public class SentinelConfig { @Bean public SentinelResourceAspect sentinelResourceAspect(){ return new SentinelResourceAspect(); } }
3、Controller增加限流规则,需限流的方法上添加注解 @SentinelResource ,并配置上限流处理的 blockHandler,blockHandlerClass;
@RestController @Slf4j public class HelloController { @PostConstruct public void init() { List<FlowRule> flowRules = new ArrayList<>(); // 创建流控规则 FlowRule flowRule = new FlowRule(); //设置流控规则 QPS flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); //设置受保护的资源 flowRule.setResource("helloSentinelResource"); //设置受保护的资源的阈值 flowRule.setCount(1); flowRules.add(flowRule); //加载配置好的规则 FlowRuleManager.loadRules(flowRules); } @GetMapping("/hello/sentinel") @SentinelResource(value = "helloSentinelResource", blockHandler = "testHelloSentinelBlock", blockHandlerClass = BlockUtil.class) public String sentinelDemo() { log.info("-------- success-hello-sentinel"); return "success-hello-sentinel"; } }
注意:
(1)blockHandler 对应处理 BlockException 的函数名称( testHelloSentinelBlock );
(2)blockHandler 函数访问范围需要是 public,类型必须为 static,返回值类型需要与原方法相匹配 ;
(3)blockHandler 函数得参数与原方法的参数一样,并且最后可以加一个类型为 BlockException 的参数;
4、流控处理类
@Slf4j public class BlockUtil { public static String testHelloSentinelBlock(BlockException e) { log.info("block-sentinel---流控了"); return "block-sentinel---流控了"; } }
5、调用 http://localhost:8080/hello/sentinel 接口的结果(每秒只能通过一个接口):
3、Springboot 整合 Sentinel
(1)maven的坐标
<!--加入sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!--加入actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
(2)项目的配置文件增加如下:
spring: cloud: sentinel: transport: dashboard: localhost:9999 application: name: order-center server: port: 8000 # 暴露/actuator/sentinel端点 management: endpoints: web: exposure: include: '*'
注意:必须暴露 /actuator/sentinel端点;(http://localhost:8000/actuator/sentinel)
(3) 下载并启动 sentinel
<1> 下载路径:https://github.com/alibaba/Sentinel/releases
<2> 启动 Sentinel : java -jar sentinel-dashboard-1.6.3.jar --server.port=9999
访问: http://localhost:9999/ ; 默认账号密码 sentinel/sentinel
(4)进入到 sentinel 的控制台之后没有看到里面的内容,需要启动一下微服务;
服务启动发个完成之后去调用一下服务的接口:http://localhost:8000/v1/getProduct;
(5)点击 ”+流控“,设置 QPS 的单机阈值为1(即1秒只能通过一个请求)
(6)可以在 ”流控规则“ 中看到我们刚才增加的流控规则。
(7)接着去快速访问:http://localhost:8000/v1/getProduct,可以在浏览器中看到接口被流控了;
三、Sentinel的面板介绍
1、实时监控
可以监控我们接口的 通过QPS 和 拒绝QPS。(注意:没有设置流控规则的接口是看不到的)
2、簇点链路
用来显示微服务的所监控的API
2.1 流控的设置
选择 ”簇点链路“,选择具体的访问的API,然后点击 ”流控“ 按钮。
说明:
资源名称:接口的URI;
针对来源:这里是默认的default(标示不针对来源),还有一种情况就是假设微服务A需要调用这个资源,微服务B也需要调用这个资源,那么我们就可以单独的为微服务A和微服务B进行设置阈值。这个功能需求写代码来实现。
阈值类型:分为QPS 和 线程数。
若设置阈值为2,
- 选择QPS,则只要是每秒钟访问接口的次数>2就进行限流;
- 选择线程数,为接受请求该资源 分配的线程数>2就进行限流.;
流控模式:
- 直接:达到设置的阈值后直接被流控抛出异常。;
- 关联:当访问的接口1,超过设置的阈值,就去限流接口2;
- 链路:API级别的限制流量;
流控效果:
- 快速失败:直接抛出异常;
- Warm Up(预热):
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。
冷加载因子: coldFactor 默认是3,,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。
上面设置: 就是QPS从100/3=33开始算 经过10秒钟,到达 100 的QPS 才进行限制流量 。
- 排队等待:
这种方式适合用于请求以突刺状来到,这个时候我们不希望一下子把所有的请求都通过,这样可能会把系统压垮;同时我们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,而不是拒绝所有请求。
选择排队等待的阈值类型必须是QPS。
单机阈值:10表示 每秒通过的请求个数是10,则每隔100ms通过一次请求;每次请求的最大等待时间为2000ms=2s,超过2S就丢弃请求。
2.2 降级规则
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。
降级策略:
- RT:平均响应时间(DEGRADE_GRADE_RT);每秒内的请求的响应时间大于100ms,则统计为慢调用;当单位统计时长(
statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的时间窗口(5s)内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
注意:Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置 。
接口请求的响应超过100ms,则在接下来的时间窗口5s中,是被熔断了。
- 异常比例:当单位统计时长(
statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。
请求的异常比例大于30%,则在接下来的时间窗口5秒钟被熔断。
- 异常数:当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态,所以这里的时间窗口设置时间要大于60s。
2.3 热点参数
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
(1)使用 @SentinelResource 注解,代码如下:
@SentinelResource("order-1-res")
@GetMapping("/v1/orderSave")
public String saveOrder(@RequestParam(value = "userId",required = false)String userId,
@RequestParam(value = "productId" ,required = false) String productId,
@RequestParam(value = "name", required = false) String name) {
log.info("----------- start res ---------");
return userId;
}
(2)配置热点参数:
资源 order-1-res 的接口的有携带有第2个参数,每秒通过的请求数超过3个,则触发降级,10s 后恢复正常。
http://localhost:8000/v1/orderSave?userId=1 --------------- 不会降级,因为只携带了1个参数。
http://localhost:8000/v1/orderSave?userId=1&name=zhangsan&productId=2 --------------- 每秒的请求超过3个就会降级
资源 order-1-res 的接口的 第2个参数的值为 wang,则每秒通过的请求超过1个,则触发降级,10s后恢复正常。第2个参数的值为 wang 之外的请求,则每秒超过的请求超过3个,则触发降级。