服务雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了
问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等
待,进而导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是
服务故障的 “雪崩效应” 。
服务容错核心思想
- 不被上游请求压垮
- 不被下游响应拖垮
- 不被外界环境影响(运维配置系统规则)
常见容错方案Sentinel
在pom.xml中加入下面依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
安装Sentinel控制台
Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。
- 下载jar包,解压到文件夹
https://github.com/alibaba/Sentinel/releases - 启动控制台
# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目) java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar
- 修改服务配置,加入控制台的配置
spring: cloud: sentinel: transport: port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可 dashboard: localhost:8080 # 指定控制台服务的地址
- 通过浏览器访问localhost:8080 进入控制台 ( 默认用户名密码是 sentinel/sentinel )
注意:sentinel控制台界面,是懒加载,必须先访问服务后,在sentinel控制台界面才会显示
流控规则
表示:每秒请求量大于3的时候开始限流
sentinel共有三种流控模式,分别是:
直接(默认):接口达到限流条件时,开启限流
关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]
链路:当从某个接口过来的资源达到限流条件时,开启限流
降级规则
表示:响应时间超过1ms时,接下来5s内服务降级,5s后服务恢复正常,进行下一轮判断。
降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:
- 平均响应时间 :当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态。
如果接下来 1s 内持续进入 5 个请求,它们的 RT都持续超过这个阈值,那么在接下的时间窗口
(以 s 为单位)之内,就会对这个方法进行服务降级。 - 异常比例:当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下的
时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0,
1.0]。
- 异常数 :当资源近 1 分钟的异常数目超过阈值之后会进行服务降级。注意由于统计时间窗口是分
钟级别的,若时间窗口小于 60s,则结束熔断状态后仍可能再进入熔断状态。
热点规则
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上。
热点规则简单使用
第1步: 编写代码
@RequestMapping("/order/message3") @SentinelResource("message3") //注意这里必须使用这个注解标识,否则热点规则不生效 public String message3(String name, Integer age) { return "message3" + name + age; }
第2步: 配置热点规则
表示:1s内超过1个请求,接下来10s内服务限流,10s后服务恢复正常,进行下一轮判断。
第3步: 分别用两个参数访问,会发现只对第一个参数限流了
热点规则增强使用
参数例外项允许对一个参数的具体值进行流控
表示:第一个参数值为zhu的时候,限流阈值为20,即每秒超过20个请求才会限流。
授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源
访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
- 若配置白名单,则只有请求来源位于白名单内时才可通过;
- 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。
比如:A调用服务C都放行,B调用服务C都限流。
上面的资源名和授权类型不难理解,但是流控应用怎么填写呢?
其实这个位置要填写的是来源标识,Sentinel提供了 RequestOriginParser 接口来处理来源。
只要Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析
访问来源。
第1步: 自定义来源处理规则
@Component public class RequestOriginParserDefinition implements RequestOriginParser { //定义区分来源: 本质作用是通过request域获取到来源标识 //app pc //然后 交给流控应用 位置进行匹配 @Override public String parseOrigin(HttpServletRequest request) { String serviceName = request.getParameter("serviceName"); if (StringUtils.isEmpty(serviceName)){ throw new RuntimeException("serviceName is not empty"); } return serviceName; } }
第2步: 授权规则配置
这个配置的意思是只有serviceName=pc不能访问(黑名单)
第3步: 访问 http://localhost:8091/order/message1?serviceName=pc观察结果
自定义规则异常返回
//自定义异常返回页面(区分各种限流和降级等异常) @Component public class ExceptionHandlerPage implements UrlBlockHandler { @Override public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException e) throws IOException { response.setContentType("application/json;charset=utf-8");//处理中文乱码 ResponseData responseData = null; //BlockException 异常接口,包含Sentinel的五个异常 // FlowException 限流异常 // DegradeException 降级异常 // ParamFlowException 参数限流异常 // AuthorityException 授权异常 // SystemBlockException 系统负载异常 if (e instanceof FlowException) { responseData = new ResponseData(-1, "接口被限流了..."); } else if (e instanceof DegradeException) { responseData = new ResponseData(-2, "接口被降级了..."); } response.getWriter().write(JSON.toJSONString(responseData)); } } @Data @AllArgsConstructor //全参构造 @NoArgsConstructor //无参构造 class ResponseData { private int code; private String message; }
@SentinelResource的使用
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能
通过@SentinelResource来指定出现异常时的处理策略。
@Service @Slf4j public class OrderServiceImpl3 { int i = 0; //定义一个资源 //定义当资源内部发生异常的时候的处理逻辑 //blockHandler 定义当资源内部发生了BlockException应该进入的方法[捕获的是Sentinel定义的异常] //fallback 定义当资源内部发生了Throwable应该进入的方法 @SentinelResource( value = "message", blockHandlerClass = OrderServiceImpl3BlockHandler.class, blockHandler = "blockHandler", fallbackClass = OrderServiceImpl3Fallback.class, fallback = "fallback" ) public String message(String name) { i++; if (i % 3 == 0) { throw new RuntimeException(); } return "message"; } }
//OrderServiceImpl3对应的BlockException处理的类 @Slf4j public class OrderServiceImpl3BlockHandler { //blockHandler //要求: //1 当前方法的返回值和参数要跟原方法一致(且是静态方法) //2 但是允许在参数列表的最后加入一个参数BlockException, 用来接收原方法中发生的异常 public static String blockHandler(String name, BlockException e) { //自定义异常处理逻辑 log.error("触发了BlockException,内容为{}", e); return "BlockException"; } }
//OrderServiceImpl3对应的Throwable处理的类 @Slf4j public class OrderServiceImpl3Fallback { //fallback //要求: //1 当前方法的返回值和参数要跟原方法一致 //2 但是允许在参数列表的最后加入一个参数Throwable, 用来接收原方法中发生的异常 public static String fallback(String name, Throwable e) { //自定义异常处理逻辑 log.error("触发了Throwable,内容为{}", e); return "Throwable"; } }
Sentinel规则持久化
通过前面的讲解,我们已经知道,可以通过Dashboard来为每个Sentinel客户端设置各种各样的规
则,但是这里有一个问题,就是这些规则默认是存放在内存中,极不稳定,所以需要将其持久化。
- 编写处理类
import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler; import com.alibaba.csp.sentinel.datasource.*; import com.alibaba.csp.sentinel.init.InitFunc; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager; import com.alibaba.csp.sentinel.slots.system.SystemRule; import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.TypeReference; import org.springframework.beans.factory.annotation.Value; import java.io.File; import java.io.IOException; import java.util.List; /** * 流控规则持久化 */ public class FilePersistence implements InitFunc { @Value("spring.application.name") private String appcationName; @Override public void init() throws Exception { String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + appcationName; 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.mkdirIfNotExits(ruleDir); this.createFileIfNotExits(flowRulePath); this.createFileIfNotExits(degradeRulePath); this.createFileIfNotExits(systemRulePath); this.createFileIfNotExits(authorityRulePath); this.createFileIfNotExits(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 mkdirIfNotExits(String filePath) throws IOException { File file = new File(filePath); if (!file.exists()) { file.mkdirs(); } } private void createFileIfNotExits(String filePath) throws IOException { File file = new File(filePath); if (!file.exists()) { file.createNewFile(); } } private <T> String encodeJson(T t) { return JSON.toJSONString(t); } }
- 添加配置
在resources下创建配置目录 META-INF/services ,然后添加文件
com.alibaba.csp.sentinel.init.InitFunc
在文件中添加配置类的全路径
com.itheima.config.FilePersistence
Feign整合Sentinel
第1步: 引入sentinel的依赖
<!--sentinel客户端--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
第2步: 在配置文件中开启Feign对Sentinel的支持
# 开启feign对sentinel的支持 feign: sentinel: enabled: true
第3步: 创建容错类
//这是容错类,他要求我们要是实现一个FallbackFactory<要为哪个接口产生容错类> @Slf4j @Service public class ProductServiceFallbackFactory implements FallbackFactory<ProductService> { //Throwable 这就是fegin在调用过程中产生异常 @Override public ProductService create(Throwable throwable) { return new ProductService() { @Override public Product findByPid(Integer pid) { log.error("{}",throwable);//打印异常 Product product = new Product(); product.setPid(-100); product.setPname("商品微服务调用出现异常了,已经进入到了容错方法中"); return product; } @Override public void reduceInventory(Integer pid, Integer number) { } }; } }
第4步: 为接口指定容错类
//value用于指定调用nacos下哪个微服务 //fallback 指定当调用出现问题之后,要进入到哪个类中的同名方法之下执行备用逻辑 //fallbackFactory 指定当调用出现问题之后,要进入到哪个类中的同名方法之下执行备用逻辑,并且可以在日志中打印异常信息 @FeignClient( value = "service-product", // fallback = ProductServiceFallback.class, fallbackFactory = ProductServiceFallbackFactory.class ) public interface ProductService { //@FeignClient的value + @RequestMapping的value值 其实就是完成的请求地址 "http://service-product/product/" + pid //指定请求的URI部分 @RequestMapping("/product/{pid}") Product findByPid(@PathVariable Integer pid); //扣减库存 //参数一: 商品标识 //参数二:扣减数量 @RequestMapping("/product/reduceInventory") void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("number") Integer number); }