Sentinel的降级和热点规则
共 21057字,需浏览 43分钟
·
2022-11-21 10:54
点击关注公众号,Java干货及时送达
出品 | Java技术迷(ID:JavaFans1024)
降级规则
降级规则就是设置当满足什么条件时,对服务进行降级处理,同样点击左侧菜单的簇点链路,然后点击对应资源的降级按钮:
来看一看降级规则有哪些需要设置的内容:
Sentinel提供了三种熔断策略,我们一一来体会,首先是慢调用比例,它选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
比如这样的配置:
它表示统计时长,也就是1秒内请求数大于1,且慢调用比例大于0.5(这里只要请求的响应时间大于5毫秒则认为是慢调用),若是这两个条件都满足,则熔断降级5秒,5秒过后,探测器会进入探测状态,若接下来的一个请求响应时间小于5毫秒则结束熔断,若大于5毫秒,则再次熔断。
需要注意,Sentinel默认统计的RT上限是4900毫秒,超出此阈值的数据都会被认为是4900毫秒,若确实需要设置比上限还大的RT,则可以通过启动Sentinel时携带参数来实现:
-Dcsp.sentinel.statistic.max.rt=xxx
第二种策略是异常比例,当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%。
比如这样的配置:
它表示的是在1秒内若是请求数大于1,并且异常比例大于25%,也就是说4次请求中有多于1次的请求抛出了异常,则这个异常比例就大于25%了,此时请求会被熔断5秒,5秒后探测器进入探测状态,若是接下来的一个请求成功完成则结束熔断,否则再次熔断。
最后一种策略是异常数,当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
比如:
它表示1秒内请求数大于1,并且异常数也大于1,则熔断5秒,5秒后探测器进入探测状态,若接下来的一个请求成功完成,则结束熔断,否则再次熔断。
热点规则
热点规则是一种更细粒度的流控规则,它允许将规则具体到参数上。
编写代码:
@RequestMapping("/order/message3")
@SentinelResource("message3")
public String message3(String name, Integer age) {
return "message3:" + name + "," + age;
}
现有一个控制方法接收两个参数,我们使用@SentinelResource注解将其声明为一个资源,然后在Sentinel控制台设置一下热点规则:
一定要注意应该设置在哪个资源上,因为注解值为message3,所以应该对message3资源进行设置:
参数索引指的是方法中的参数,0为第一个参数,1为第二个参数,对索引为0的参数进行热点规则设置,此时携带该参数的请求在1秒内若是多于3个,则直接控制。
在底下还能够设置参数例外项,这里的意思是设置携带参数age的请求在1秒内不能多于3个,否则直接控制;但若是age值为20,则限流阈值会被指定为1000,即:1秒内不能多于1000个请求。
授权规则
很多时候,我们需要根据调用来源判断该次请求是否允许放行,这时候可以使用Sentinel的来源访问控制的功能,来源访问控制根据资源的请求来源限制资源是否能够通过。
授权规则是需要通过一个标识来匹配来源是否合法的,所以我们需要编写代码:
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
/**
* 定义区分来源
*
* @param request
* @return
*/
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
if (StringUtils.isEmpty(serviceName)) {
throw new RuntimeException("serviceName is not empty");
}
return serviceName;
}
}
该方法将会从请求域中获取一个serviceName属性值,并将它返回给Sentinel进行匹配,所以我们要在Sentinel中进行授权规则的设置:
授权规则采用两种形式,白名单和黑名单,若是白名单,则除了当前配置的应用外,其它应用都将被控制;若是黑名单,则相反。
这里我们设置流控应用为A,类型为白名单,则携带serviceName=A属性的请求不会被限制,除此之外的请求都将被控制:
自定义异常返回页面
在前面的各个规则中,我们发现当资源被控制后,显示的错误页面都是一致的,这样我们很难区分究竟是什么问题,对于用户这样的页面也不友好,为此,我们需要自定义异常的返回页面,方式如下:
@Component
public class ExceptionHandlerPage 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(-1, "限流异常...");
} else if (e instanceof DegradeException) {
// 如果是降级异常
responseData = new ResponseData(-2, "降级异常...");
} else if (e instanceof ParamFlowException) {
// 如果是参数限流异常
responseData = new ResponseData(-3, "参数限流异常");
} else if (e instanceof AuthorityException) {
responseData = new ResponseData(-4, "授权异常");
} else if (e instanceof SystemBlockException) {
responseData = new ResponseData(-5, "系统负载异常");
}
response.getWriter().write(JSON.toJSONString(responseData));
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class ResponseData {
private int code;
private String message;
}
可以对每个异常进行定制化处理。
@SentinelResource
前面我们已经使用到了@SentinelResource注解,它的作用是声明一个资源,但它的作用远不止如此,它可以定义当资源内部发生异常时的处理逻辑:
@Service
@Slf4j
public class OrderMessageService {
@SentinelResource(value = "message", blockHandler = "blockHandler", fallback = "fallback")
public String message(String name) {
return "message";
}
public String blockHandler(String name, BlockException e) {
log.error("BlockException...{}", e);
return "BlockException";
}
public String fallback(String name, Throwable t) {
log.error("Throwable...{}", t);
return "Throwable";
}
}
@SentineResource注解能够指定两个参数用来处理出异常后的逻辑,blockHandler和fallback。
其中blockHandler捕获的是BlockException,该异常是流控异常、降级异常、热点异常等五个异常的父类,也就是说,Sentinel抛出的异常都会被它捕获,该属性需要指定一个处理方法,该方法的声明也有讲究,返回值和参数必须和原资源方法的声明一致,但可以再多声明一个BlockException参数用于获取异常信息。
而fallback的作用与blockHandler类似,但它的作用域更广,对于资源产生的任何异常,都会被fallback捕获到并进行处理,现在我们对/order/message资源进行流控,则发生流控异常后便会进入我们自定义的处理方法:
若是同时定义这两个属性,则对于Sentinel抛出的异常,blockHandler会优先处理。
若是不想让异常处理方法和业务写在一起,我们也可以使用另外一个属性blockHandlerClass将异常处理抽取到外面去:
@SentinelResource(
value = "message",
blockHandlerClass = OrderMessageBlockHandler.class,
blockHandler = "blockHandler",
fallback = "fallback")
public String message(String name) {
return "message";
}
此时在外部定义OrderMessageBlockHandler类:
@Slf4j
public class OrderMessageBlockHandler {
public static String blockHandler(String name, BlockException e) {
log.error("BlockException...{}", e);
return "BlockException";
}
}
要注意此时的异常处理方法必须声明为静态的,fallback也可以如此方式进行处理。
Sentinel持久化
到这里关于Sentinel的各种规则就介绍完了,但是我们仍然有一个问题没有解决,就是每次设置的规则在应用启动后就失效了,需要重新设置,这是因为Sentinel默认将规则设置保存在了内存中,所以要是想将规则保存下来,我们就需要对其进行持久化。
编写一个类:
package com.wwj.config;
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.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 applicationName;
@Override
public void init() throws Exception {
String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + applicationName;
System.out.println(ruleDir);
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.mkdirIfNotExists(ruleDir);
this.createFileIfNotExists(flowRulePath);
this.createFileIfNotExists(degradeRulePath);
this.createFileIfNotExists(systemRulePath);
this.createFileIfNotExists(authorityRulePath);
this.createFileIfNotExists(paramFlowRulePath);
// 流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<List<FlowRule>>(
flowRulePath,
flowRuleListParser
);
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<List<FlowRule>>(
flowRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<List<DegradeRule>>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<List<DegradeRule>>(
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<List<SystemRule>>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<List<AuthorityRule>>(
authorityRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<List<AuthorityRule>>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 热点规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<List<ParamFlowRule>>(
paramFlowRulePath,
paramFlowRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<List<ParamFlowRule>>(
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 mkdirIfNotExists(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExists(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
然后需要在resource目录下创建一个文件夹,名字必须为 META-INF.services
,并在该文件夹下新建一个文件,名字必须为 com.alibaba.csp.sentinel.init.initFunc
,在文件中写上持久化类的全类名:
com.wwj.config.FilePersistence
最后重新启动服务,然后设置一个流控规则,该规则就会被持久化到本地磁盘上: