什么是服务雪崩效应
服务雪崩效应是一种因“服务提供者服务的不可用”(原因)导致“服务调用者服务不可用”(结果),并将不可用逐渐放大的现象。如下图所示
形成原因
服务雪崩的过程可以分为三个阶段:
- 服务提供者不可用;
- 重试加大请求流量;
-
服务调用者不可用;
服务雪崩的每个阶段都可能由不同的原因造成,总结如下:
应对策略
常见容错方案:
1、超时
2、限流
3、舱壁模式(如每个controller都有自己独立的线程池,之间互不干扰)
4、断路器模式
全面应对策略:
Sentinel 是什么
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助您保护服务的稳定性。
大家可能会问:Sentinel 和之前常用的熔断降级库 Netflix Hystrix 有什么异同呢?Sentinel官网有一个对比的文章,这里摘抄一个总结的表格,具体的对比可以点此 链接 查看。
对比内容 | Sentinel | Hystrix |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间或失败比率 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 不支持 |
流量整形 | 支持慢启动、匀速器模式 | 不支持 |
系统负载保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
从对比的表格可以看到,Sentinel比Hystrix在功能性上还要强大一些,本文让我们一起来了解下Sentinel的源码,揭开Sentinel的神秘面纱。
Sentinel功能特点
1、丰富的应用场景:例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等
2、完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
3、广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
4、完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
开源生态
Sentinel 分为两个部分:
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
为应用整合Sentinel
引入maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!--sentinel整合之后会暴露出/actuator/sentinel-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--整合Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合Spring Cloud Alibaba-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>0.9.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
加入配置暴露/actuator/sentinel端点
#添加sentinel依赖后 暴露/actuator/sentinel端点
management:
endpoints:
web:
exposure:
include: '*'
启动服务访问http://localhost:port/actuator/sentinel会返回json信息,说明已经整合好了Sentinel
搭建Sentinel控制台
下载Sentinel控制台:https://github.com/alibaba/Sentinel/releases
为服务整合Sentinel控制台:
spring:
cloud:
sentinel:
filter:
#打开/关闭掉对Spring MVC端点的保护
enabled: true
transport:
port: 8719
#指定sentinel控制台的地址
dashboard: localhost:8080
定义资源:也就是对哪个资源进行流量控制,现在已经提供了注解形式,所以新的接入直接用注解,@SentinelResource
- 关于SentinelResource注解,这里列出几个好用和必填的参数,具体参考这里
Sentinel控制台配置流控规则
流控模式
- 直接:当QPS超过阈值就进行限流。
- 关联:当关联的资源达到阈值,就限流自己。
- 适用场景:查询和修改同一表的数据,如果是高并发的应用,查询接口的流量过大,就会影响修改接口的性能,反之同理,这就可以根据业务需求,去衡量希望优先读还是优先写。
- 关联其实是一种保护关联资源的设计。
- 链路:只记录指定链路上的流量,即指定资源从入口资源进来的流量如果达到阈值就限流。
- 链路其实是一种细粒度的针对来源,而编辑流控规则中的针对来源输入框是微服务级别的,可以指定指定微服务过来的流量达到阈值就限流。
- 而链路是api级别的,指定的是api的调用流量达到阈值就限流。
流控效果
- 快速失败:直接失败,抛异常
相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController - Warm Up(预热):根据codeFactor(冷加载因子,默认值为3),从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
即如果阈值为100,冷加载因子为3,预热时长为10秒,那么就会用100 / 3作为最初的阈值,经过10秒之后才会将阈值达到100,进而进行限流,意思就是让允许通过的流量缓慢增加,在达到一定的时间之后才达到阈值这样会更好的保护微服务 - 排队等待:匀速排队,让请求以均匀的速度通过,阈值类型必须设置成QPS,否则无效。此种模式可适用于应对突发流量的场景
相关源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
Sentinel控制台配置降级规则,即断路器模式,Sentinel目前只有断路器三态中的打开和关闭,没有半开状态
降级策略
-
RT:平均响应时间
注意:Sentinel默认RT最大时间为4900毫秒,可通过-Dcsp.sentinel.statistic.max.rt=xxx修改
-
异常比例
-
异常数
注意:时间窗口<60秒可能会出问题
相关源码:com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule
Sentinel控制台配置热点规则,是一种特殊的流控规则,支持对特定的参数和参数的值限流
适用于存在热点参数(某些参数QPS很高),并希望提升API可用性的场景
注意:参数必须是基本类型或者String
相关源码:com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowChecker#passCheck()
加入代码
@GetMapping("/test-hot")
@SentinelResource("hot")
public String testHot(@RequestParam(required = false) String a,
@RequestParam(required = false)String b){
return a + ":" + b;
}
Sentinel控制台配置系统规则
阈值类型
- LOAD
当系统Load1(1分钟的load)超过阈值,且并发线程数超过系统容量时触发,建议配置为CPU核心数*2.5。(仅对Linux/Unix-like 机器生效)
系统容量 = maxQps * minRt
maxQps:秒级统计出来的最大QPS
minRt:秒级统计出来的最小响应时间
相关源码:com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkBbr() - RT:所有入口流量的平均RT达到阈值触发
- 线程数:所有入口流量的并发线程数达到阈值触发
-
入口QPS:所有入口流量的QPS达到阈值触发
相关源码:com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkSystem()
Sentinel控制台配置授权规则
- 白名单:资源名里的资源只允许为白名单里面的流控应用访问。
- 黑名单:资源名里的资源不允许为黑名单里面的流控应用访问。
- 授权规则通过调用来源从而实现对服务消费者的授权或者限制。
Sentinel使用Java代码方式配置规则
请点击:
Alibaba Sentinel 规则参数总结
Alibaba Sentinel 配置项总结
Sentinel与控制台通信原理剖析
微服务注册到Sentinel控制台和发送心跳源码:com.alibaba.csp.sentinel.transport.heartbeat.SimpleHttpHeartbeatSender
微服务和Sentinel控制台通信API源码:com.alibaba.csp.sentinel.command.CommandHandler接口的实现类
修改控制台规则是如何通知客户端的?
看sentinel-transport-simple-http包中的HttpEventTask类,它开启了一个线程,专门用来做为socket连接,控制台通过socket请求通知客户端,从而更新客户端规则,更改规则核心代码如下:
long start = System.currentTimeMillis();
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream(), SentinelConfig.charset()));
OutputStream outputStream = this.socket.getOutputStream();
printWriter = new PrintWriter(new OutputStreamWriter(outputStream, Charset.forName(SentinelConfig.charset())));
String line = in.readLine();
CommandCenterLog.info("[SimpleHttpCommandCenter] socket income: " + line + "," + this.socket.getInetAddress(), new Object[0]);
CommandRequest request = this.parseRequest(line);
String commandName = HttpCommandUtils.getTarget(request);
if (!StringUtil.isBlank(commandName)) {
CommandHandler<?> commandHandler = SimpleHttpCommandCenter.getHandler(commandName);
if (commandHandler != null) {
CommandResponse<?> response = commandHandler.handle(request);
this.handleResponse(response, printWriter, outputStream);
} else {
this.badRequest(printWriter, "Unknown command `" + commandName + '`');
}
printWriter.flush();
long cost = System.currentTimeMillis() - start;
CommandCenterLog.info("[SimpleHttpCommandCenter] Deal a socket task: " + line + ", address: " + this.socket.getInetAddress() + ", time cost: " + cost + " ms", new Object[0]);
return;
}
this.badRequest(printWriter, "Invalid command");
Sentinel配置项
微服务应用端连接Sentinel控制台配置项
spring.cloud.sentinel.transport:
# 指定控制台的地址
dashboard:localhost:8080
# 指定和控制台通信的IP
# 如不配置,会自动选择一个IP注册
client-ip:127.0.0.1
# 指定和控制台通信的端口,默认8719
# 如不设置,会自动从8719开始扫描,依次+1,直到找到未被占用的端口
port:8719
# 心跳发送周期,默认值null
# 但在SimpleHttpHeartbeatSender会用默认值10秒
heartbeat-interval-ms:1000
Sentinel控制台配置项
配置项 | 默认值 | 最小值 | 描述 |
---|---|---|---|
sentinel.dashboard.app.hideAppNoMachineMillis | 0 | 60000 | 是否隐藏无健康节点的应用,距离最近 一次主机心跳时间的毫秒数,默认关闭 |
sentinel.dashboard.removeAppNoMachineMillis | 0 | 120000 | 是否自动删除无健康节点的应用,距离 最近一次其下节点心跳时间毫秒数,默 认关闭 |
sentinel.dashboard.unhealthyMachineMillis | 60000 | 30000 | 主机失联判定,不可关闭 |
sentinel.dashboard.autoRemoveMachineMillis | 0 | 300000 | 距离最近心跳时间超过指定时间是否 自动删除失联节点,默认关闭 |
server.port | 8080 | - | 指定端口 |
csp.sentinel.dashboard.server | localhost:8080 | - | 指定地址 |
project.name | - | - | 指定程序名称 |
sentinel.dashboard.auth.username[1.6] | sentinel | - | dashboard登录账号 |
sentinel.dashboard.auth.password[1.6] | sentinel | - | dashboard登录密码 |
server.servlet.session.timeout[1.6] | 30分钟 | - | 登录session过期时间 配置为 7200表示7200秒 配置为60m表示为60分钟 |
Sentinel API
- Sphu:定义资源,让资源受到监控并保护资源。
- Tracer:可以对我们想要的异常进行统计。
- ContextUtil:可以实现调用来源,还可以标记调用。
@GetMapping("/test-sentinel-api")
public String testSentinelAPI(@RequestParam(required = false) String a){
String resourceName = "test-sentinel-api";
ContextUtil.enter(resourceName,"test-wfw");
Entry entry = null;
try {
//定义一个sentinel保护的资源 名称是test-sentinel-api
entry = SphU.entry(resourceName);
//被保护的业务逻辑
if(StringUtils.isEmpty(a)){
throw new IllegalArgumentException("a不能为空");
}
return a;
} catch (BlockException e) {
//如果被保护的资源被限流或者降级了,就会抛BlockException
log.warn("限流,或者降级了...",e);
return "限流,或者降级了...";
}catch (IllegalArgumentException e) {
//统计IllegalArgumentException发生的次数,发生的占比等
Tracer.trace(e);
return "参数非法...";
} finally {
if(entry != null){
//退出entry
entry.exit();
}
ContextUtil.exit();
}
}
Sentinel @SentinelResource详解
@SentinelResource使用方式一
//使用blockHandler属性,blockHandler的方法必须和资源在同一类中,并且有相同的参数和返回值
@PostMapping
@SentinelResource(value = "createOrder",blockHandler = "doOnBlock")
public OrderInfo create(@RequestBody OrderInfo order, @AuthenticationPrincipal String username){
log.info("用户名为:username={}",username);
PriceInfo priceInfo = priceFeignClient.getPrice(order.getProductId());
log.info("商品价格为,priceInfo={}",priceInfo);
return order;
}
public OrderInfo doOnBlock(@RequestBody OrderInfo order, @AuthenticationPrincipal String username, BlockException exception){
log.info("blocked by :blockException={}",exception.getClass().getSimpleName());
return order;
}
@SentinelResource使用方式二
@PostMapping
@SentinelResource(value = "createOrder",blockHandler = "doOnBlock",blockHandlerClass = SentinelBlockHandler.class)
public OrderInfo create(@RequestBody OrderInfo order, @AuthenticationPrincipal String username){
log.info("用户名为:username={}",username);
PriceInfo priceInfo = priceFeignClient.getPrice(order.getProductId());
log.info("商品价格为,priceInfo={}",priceInfo);
return order;
}
@Slf4j
public class SentinelBlockHandler {
public static OrderInfo doOnBlock(@RequestBody OrderInfo order, @AuthenticationPrincipal String username, BlockException exception){
log.info("blocked by :blockException={}",exception.getClass().getSimpleName());
return order;
}
}
@SentinelResource其与属性和新增属性
属性 | 作用 | 是否必须 |
---|---|---|
value | 资源名称 | 是 |
entryType | entry类型,标记流量的方向,取值IN/OUT,默认是OUT | 否 |
blockHandler | 处理BlockException的函数名称。函数要求: 1. 必须是 public 2.返回类型与原方法一致 3. 参数类型需要和原方法相匹配,并在最后加 BlockException 类型 的参数。 4. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 blockHandlerClass ,并指定blockHandlerClass里面的方法。 |
否 |
blockHandlerClass | 存放blockHandler的类。对应的处理函数必须static修饰, 否则无法解析,其他要求:同blockHandler。 |
否 |
fallback | 用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对 所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型) 进行处理。函数要求: 1. 返回类型与原方法一致 2. 参数类型需要和原方法相匹配,Sentinel 1.6开始, 也可在方法最后加 Throwable 类型的参数。 3.默认需和原方法在同一个类中。若希望使用其他类的函数, 可配置 fallbackClass ,并指定fallbackClass里面的方法。 |
否 |
fallbackClass【1.6】 | 存放fallback的类。对应的处理函数必须static修饰, 否则无法解析,其他要求:同fallback。 |
否 |
defaultFallback【1.6】 | 用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的 异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。 若同时配置了 fallback 和defaultFallback,以fallback为准。 函数要求:1. 返回类型与原方法一致 2. 方法参数列表为空,或者有一个 Throwable 类型的参数。 3. 默认需要和原方法在同一个类中。若希望使用其他类的函数, 可配置 fallbackClass ,并指定 fallbackClass 里面的方法。 |
否 |
exceptionsToIgnore【1.6】 | 指定排除掉哪些异常。排除的异常不会计入异常统计, 也不会进入fallback逻辑,而是原样抛出。 |
否 |
exceptionsToTrace | 需要trace的异常 | Throwable |
@SentinelResource相关源码
com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect
com.alibaba.csp.sentinel.annotation.aspectj.AbstractSentinelAspectSupport
RestTemplate整合Sentinel
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate RestTemplate(){
return new RestTemplate(new HttpComponentsClientHttpRequestFactory());
}
resttemplate:
sentinel:
#false 关闭@SentinelRestTemplate注解,在做开发调试的时候可以关闭此注解,专注于功能的实现
enabled: true
相关源码:org.springframework.cloud.alibaba.sentinel.custom.SentinelBeanPostProcessor
Feign整合Sentinel
feign:
sentinel:
#为feign整合Sentinel
enabled: true
发生限流降级时,自定义处理逻辑
使用@FeignClient的fallback属性
@FeignClient(value="priceServer",fallback = PriceFeignClientFallback.class)
public interface PriceFeignClient {
@GetMapping(value = "/prices/{id}")
PriceInfo getPrice(@PathVariable("id") Integer id);
}
/**
* 发生限流降级时,自定义处理逻辑
*
* 一旦PriceFeignClient中远程调用的getPrice()方法被流控了或发生异常了,就会进入此方法
* 这就相当于一个兜底的行为,保证了服务的可用
*/
@Component
public class PriceFeignClientFallback implements PriceFeignClient {
@Override
public PriceInfo getPrice(Integer id) {
PriceInfo priceInfo = new PriceInfo();
priceInfo.setId(id);
priceInfo.setPrice(new BigDecimal(id));
return priceInfo;
}
}
使用@FeignClient的fallbackFactory属性,推荐使用这种方案
@FeignClient(value="priceServer",fallbackFactory = PriceFeignClientFallbackFactory.class)
public interface PriceFeignClient {
@GetMapping(value = "/prices/{id}")
PriceInfo getPrice(@PathVariable("id") Integer id);
}
/**
* 发生限流降级时,自定义处理逻辑
* 一旦PriceFeignClient中远程调用的getPrice()方法被流控了或发生异常了,就会进入此方法
* 这就相当于一个兜底的行为,保证了服务的可用
* 相比于FeignClient中的fallback属性而言,fallbackFactory属性在fallback的基础上可以拿到异常信息
*/
@Component
@Slf4j
public class PriceFeignClientFallbackFactory implements FallbackFactory<PriceFeignClient> {
@Override
public PriceFeignClient create(Throwable throwable) {
return new PriceFeignClient() {
@Override
public PriceInfo getPrice(Integer id) {
log.error("远程调用被限流或降级了,throwable={}",throwable);
PriceInfo priceInfo = new PriceInfo();
priceInfo.setId(id);
priceInfo.setPrice(new BigDecimal(id));
return priceInfo;
}
};
}
}
相关源码:org.springframework.cloud.alibaba.sentinel.feign.SentinelFeign
Sentinel使用方式总结
使用方式 | 使用方式 | 使用方法 |
---|---|---|
编码方式 | API | try...catch...finally |
注解方式 | @SentinelResource | blockHandler / fallback |
RestTemplate | @SentinelRestTemplate | blockHandler / fallback |
Feign | feign.sentinel.enabled=true | fallback / fallbackFactory |
Sentinel规则持久化方案推荐:
Alibaba Sentinel规则持久化-推模式-手把手教程【基于Nacos】
https://github.com/eacdy/Sentinel-Dashboard-Nacos/releases
https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel#pull模式
参考: