服务雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 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);
}