雪崩效应
也叫级联故障,基础服务故障导致上层服务故障,并不断放大的过程。
例如C-->B-->A,A挂了,B调用A超时,B的线程有限,满了以后导致B也挂了,逐渐导致C及后面的也挂了,就像滚雪球一样,最终可能导致整个服务都挂了。
常见容错方案
超时
设置比较短的超时时间,比如给B服务设置1秒的超时时间,1秒以后不管调用成不成功,线程都会被释放。
限流
比如服务B最大限流的值是1000,那给B设置800,达到阈值后,后面的请求服务直接拒绝。
仓壁模式
每个controller都有自己的线程池,这个controller线程满了,拒绝服务了,并不会影响其他controller的服务。
断路器模式
监控每个api的状态,例如5秒钟错误次数或错误率到达一个值,那就认定这个服务出了问题,断路器打开。断路器有一个半开状态,隔一段时间断路器会切位半开状态,调用一下挂掉的服务,如果服务可以被调用,则说明服务恢复,断路器关闭。如果不能被调用,说明服务没有恢复,断路器仍处于打开状态。
Sentinel
轻量级的流量控制、熔断级java库。说白了就是个容错的库
加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
没有注解没有配置
加配置可访问 localhost:端口/actuator/sentinel 端点,查看sentinel
management:
endpoints:
web:
exposure:
include: '*'
搭建sentinel控制台
sentinel控制台是官方提供的可视化界面
下载地址:https://github.com/alibaba/Sentinel/releases
版本注意:应该跟应用的sentinel的版本相同
java -jar安装即可
给应用整合控制台
写配置
spring:
cloud:
sentinel:
transport:
#指定sentinel控制台的地址
dashboard: localhost:8080
注意:sentinel是懒加载的,所以需访问请求才可以在控制台上看到
Sentinel流控规则
在控制台簇点链路即可添加设置
sentient的流控规则包含:
直接、关联、链路
直接:直接流控,根据设置的qps或线程数直接限制访问
关联:当关联的资源达到阈值的时候,限流自己。使用场景:有两个接口,一个读,一个改,哪个过快都会影响另一个,所以设置合适的阈值,才可以使两个接口达到平衡状态。
链路:指定资源从入口资源进来的流量达到阈值就限流。比如有ab两个接口,都调用common服务,在簇点链路a下的common设置流控规则,那只会限制a的流量,而对b服务调用common没有影响。
流控效果
快速失败:就是直接抛异常
Warm up:根据codeFactor(默认3)的值,从阈值除以codeFactor,所以刚开始的阈值是设置阈值的三分之一,经过预热时长,才打到设置的qps阈值。说白了就是阈值从一个较小的数慢慢增加。
排队等待:匀速排队,让请求以均匀的速度通过,阈值类型必须设成qps,否则无效。qps的阈值即为每秒钟允许的访问数,超过的就排队,直到超时时间到了,就过期。适用于应对突发大量请求的场景。
Sentinel降级规则
所谓降级,即断路器模式
RT:平均响应时间。例如设置成1,时间窗口为5,即每秒钟请求超过1毫秒,且5秒内请求大于等于5次,断路器打开,5秒结束,断路器关闭。注意:RT最大是4900毫秒
中间浏览器卡了保存失败,后面再补吧,难受
RestTemplate整合Sentinel
在启动类下的初始化RestTemplate类上加注解@SentinelRestTemplate即可
@Bean
@LoadBalance
@SentinelRestTemplate
public RestTemplate restTemplate(){
return new RestTemplate();
}
可以用配置关闭注解
resttemplate:
sentinel:
enabled: false
注意:有空看一下SentinelRestTemplate类
feign整合sentinel
添加配置即可
feign:
sentinel:
enabled: true
如果feign限流、降级发生时,想自己定制自己的逻辑:
新增一个类实现feignclient接口,并写想实现的代码
@Component
public class UserCenterFeignClientFallback implements UserCenterFeignClient{
@Override
public UserDTO findById(Integer id){
UserDTO userDTO = new UserDTO();
userDTO.setWxNickname("一个默认用户");
return userDTO;
}
}
在接口注解上加fallback
@FeignClient(name = "user-center", fallback = UserCenterFeignClientFallback.class)
public interface UserCenterFeignClient{
//当findById被调用的时候,feign就会构建出http://user-center/users/{id}并请求
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable Integer id);
}
这时,当被限流或者降级时,就会自动跳入UserCenterFeignClientFallback这个类。
但如果想打印异常,不能用Fallback了,要用FallbackFactory
@Component
public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> {
@Override
public UserCenterFeignClient create(Throwable cause){
return new UserCenterFeignClient(){
@Override
public UserDTO findById(Integer id){
log.warn("远程调用被限流/降级了", cause);
UserDTO userDTO = new UserDTO();
userDTO.setWxNickname("一个默认用户");
return userDTO;
}
}
}
}
UserCenterFeignClient接口的注解要改为fallbackFactory
@FeignClient(name = "user-center",
fallbackFactory = UserCenterFeignClientFallbackFactory.class)
public interface UserCenterFeignClient{
//当findById被调用的时候,feign就会构建出http://user-center/users/{id}并请求
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable Integer id);
}
sentinel的使用方式
规则持久化
目前为止,每次服务重启,规则都要重新配置,如何让规则不丢失?
拉模式
控制台生成规则后,推给微服务,微服务将规则更新至文件,sentinel会有一个定时任务,定时读取规则文件并更新到规则缓存中。
加依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
</dependency>
写代码
/**
* 拉模式规则持久化
*
* @author itmuch.com
*/
public class FileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
//每个规则写到一个json文件中
// TIPS: 如果你对这个路径不喜欢,可修改为你喜欢的路径
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.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控规则 ReadableDataSource为可读数据源,定时读取json文件并更新到缓存中
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
// 将可读数据源注册至FlowRuleManager
// 这样当规则文件发生变化时,就会更新规则到内存
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
// 将可写数据源注册至transport模块的WritableDataSourceRegistry中
// 这样收到控制台推送的规则时,Sentinel会先更新到内存,然后将规则写入到文件中
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的类,内容为
# 改成上面FileDataSourceInit的包名类名全路径即可。
com.haozi.contentcenter.FileDataSourceInit
优点:简单易懂、没有多余依赖(配置、缓存等)
缺点:因为是定时加载,所以更新会有延迟,且因为存在本地文件,所以服务器迁移,相关文件也要迁移走
推模式
控制台发送规则到nacos中,微服务监听nacos上的服务
加依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
加配置
spring:
cloud:
sentinel:
datasource:
# 名称随意
flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
# 规则类型,取值见:
# org.springframework.cloud.alibaba.sentinel.datasource.RuleType
rule-type: flow
degrade:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade
system:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-system-rules
groupId: SENTINEL_GROUP
rule-type: system
authority:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-authority-rules
groupId: SENTINEL_GROUP
rule-type: authority
param-flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-param-flow-rules
groupId: SENTINEL_GROUP
rule-type: param-flow
控制台改造
1.修改pom.xml,找到
<!-- for Nacos rule publisher sample -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<scope>test</scope>
</dependency>
将<scope>test</scope>注释掉
2.找到 sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos
目录,将整个目录拷贝到 sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos
3.修改 com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2
,找到
@Autowired
@Qualifier("flowRuleDefaultProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleDefaultPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
修改为
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
4.修改 sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html
,找到:
<!--<li ui-sref-active="active">-->
<!--<a ui-sref="dashboard.flow({app: entry.app})">-->
<!--<i class="glyphicon glyphicon-filter"></i> 流控规则 V1</a>-->
<!--</li>-->
把注释都解开
编译、启动
执行 mvn clean package -DskipTests
在项目的 target
目录找到sentinel-dashboard.jar
,执行 java -jar sentinel-dashboard.jar
启动控制台。
- 测试1:用Sentinel控制台【菜单栏的
流控规则 V1
】推送流控规则,规则会存储到Nacos; - 测试2:直接在Nacos上修改流控规则,然后刷新Sentinel控制台,控制台上的显示也会被修改;
- 测试3:重启Sentinel控制台,并重启微服务;刷新控制台,可以发现规则依然存在。
以上,其实只实现了流控规则的持久化。Sentinel有若干种规则,例如降级规则、系统规则、授权规则、热点规则等,都需要使用类似的方式,修改 com.alibaba.csp.sentinel.dashboard.controller
包中对应的Controller,才能实现持久化。
优点:一致性好、性能优秀
缺点:改动多且麻烦、引入额外的依赖
以上完全不要脸的在copy http://www.imooc.com/article/289464
集群流控
每个微服务需要集成Token Client,并与Token Server通信,Token Server得到当前qps,根据规则判断是否限流。
然而当前无法用到生产环境,所以待定。
sentinel的错误页优化
如果关闭sentinel对spring mvc端点的保护,限流或降级会爆同样的错误页面,无法区分具体是哪种,关闭方法:
spring:
cloud:
sentinel:
filter:
enabled: true
sentinel提供了一个处理接口URLBlockHandler,在这个接口中可以对异常做处理
写代码
@Component
public class MyUrlBlockHandler implements UrlBlockHandler{
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException{
ErrorMsg msg = null;
//不同的异常可以做不同的事情
//如果是限流异常
if(ex instanceof FlowException){
msg = ErrorMsg.builder()
.status(100)
.msg("限流了")
.build();
}
//降级异常
else if(ex instanceof DegrandeException){
msg = ErrorMsg.builder()
.status(100)
.msg("降级了")
.build();
}
//参数热点异常
else if(ex instanceof ParamFlowException){
msg = ErrorMsg.builder()
.status(100)
.msg("热点参数限流")
.build();
}
//系统异常
else if(ex instanceof SystemBlockException){
msg = ErrorMsg.builder()
.status(100)
.msg("系统规则(负载、...)不满足规则")
.build();
}
//授权异常
else if(ex instanceof AuthorityException){
msg = ErrorMsg.builder()
.status(100)
.msg("授权规则不通过")
.build();
}
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.setContentType("application/json;charset=utf-8");
new ObjectMapper()
.writeValue(response.getWriter(),msg);
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ErrorMsg{
private Ingeger status;
private String msg;
}
sentinel区分来源
在控制台的流控中有针对来源,授权规则的流控应用也是需要区分来源才支持。sentinel提供了处理来源的RequestOriginParser接口
写代码
@Component
public class MyRequestOriginParser implements RequestOriginParser{
@Override
public String parseOrigin(HttpServletRequest request){
//在生产环境中,可以把origin放在header中,url更加好看
//比如想实现origin参数必传,不传就报错
//从请求参数中获取名为origin的参数并返回
//如果获取不到,就抛异常
String origin = request.getParameter("origin");
if(StringUtils.isBlank(origin)){
throw new IllegalArgumentException("origin参数必须被指定");
}
return origin;
}
}
sentinel支持RESTful的url
假如有/shares/1,/shares/2...多个url,想让他们实现同一种流控规则,一个一个配太麻烦。name需要让他们的资源名称相同,sentinel提供了UrlCleaner接口
写代码
@Component
public class MyUrlCleaner implements UrlCleaner{
@Override
public String clean(String originUrl){
//让/shares/1 /shares/2的返回值相同,返回/shares/{number}
String[] split = originUrl.split("/");
return Arrays.stream(split)
.map(string -> {
if(NumberUtils.isNumber(string)){
return "{number}";
}
return string;
})
.reduce((a , b) -> a + "/" + b)
.orElse("");
}
}
来源:CSDN
作者:耗子肉
链接:https://blog.csdn.net/haozi_rou/article/details/99288536