SpringCloud Alibaba-6-服务容错
1. 微服务架构中高并发带来的问题
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,
但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用。
如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
1.1 熔断与降级的区别
熔断:表示断开的意思,当系统出现异常或超过设定的阈值时,熔断器会中断对该服务的调用,避免继续请求对服务造成更大的影响。熔断器会在一段时间后尝试恢复服务,以检测服务是否已经恢复正常。
降级:表示程序再出现问题时,仍然能保证有限功能仍能使用。降级通常是在服务无法正常提供时,提供一个备用的、简化的服务,以保证系统的可用性和稳定性。
2. Sentinel介绍
官网,介绍的很详细:https://github.com/alibaba/Sentinel/wiki/如何使用
Sentinel:是阿里开源的一套用于服务容错的综合性解决组件。
主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
2.1 sentinel基本概念
资源:资源是 Sentinel 的关键概念(就是sentinel要保护的东西就是资源
)。它可以是 Java 应用程序中的任何内容甚至可以是一段代码。。
规则:规则是围绕资源的实时状态设定的规则(就是以什么规则保护资源
),可以包括流量控制规则、熔断降级规则以及系统负载保护规则。所有规则都可以动态实时调整。
2.2 sentinel的功能
-
流量控制:请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行限流。
-
熔断降级:当调用链路中某个资源出现不稳定,例如,表现为请求超时,请求异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源。
-
系统负载保护:当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
3. Sentinel快速开始
Sentinel 分为两个部分:
-
核心库(Java 客户端):不依赖控制台,即:
可以通过代码调用API的方式控制sentinel
。 -
控制台(Dashboard):是一个图形化的控制Sentinel的控制台(
可以完全不依赖代码
),直接在上面进行操作,控制sentinel。
3.1 启动Sentinel控制台
控制台下载网址:https://github.com/alibaba/Sentinel/releases 注意,根据你的alibba版本,不同的版本需要下载不同版本的sentinel。
启动控制台命令
java -Dserver.port=9000 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar // -jar sentinel-dashboard-1.7.jar jar后面就是你的jar包名,
// 其中 -Dserver.port=9000 用于指定 Sentinel 控制台端口为 9000 这个不做固定,你随意写
// 从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel
// 如果你需要更改用户名和密码:https://sentinelguard.io/zh-cn/docs/dashboard.html#%E9%89%B4%E6%9D%83
// 注:若您的应用为 Spring Boot 或 Spring Cloud 应用,您可以通过 Spring 配置文件来指定配置,详情请参考 https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
3.2 添加依赖
sentinel是用来在消费方处理的,所以只需要在消费方添加依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
3.3 添加配置文件
spring:
cloud:
sentinel:
transport: # 这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。
port: 8719 # 比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Server 接收,这个 Server 再将规则注册到 Sentinel 中。
dashboard: localhost:9000 # 用于指定你设置的 Sentinel 控制台访问地址
eager:
true # 开启对sentinel看板的饥饿式加载。sentinel默认是懒加载机制,只有访问过一次的资源才会被监控,通过关闭懒加载,在项目启动时就连接sentinel控制台。(未验证,网上说的)
3.3 查看效果
因为sentinel具有懒加载的特点,你需要先访问你的接口几次,控制台才会有显示。
如果你不可以指定资源名,默认资源名就是访问的路径名。你可以通过 @SentinelResource(value = "资源名") 来指定你的资源名
@SentinelResource 介绍:
@SentinelResource 注解用于定义资源埋点,但不支持 private 方法。
默认情况下,Sentinel 对控制资源的保护处理是直接抛出异常,这样对用户不友好,所以我们需要通过可选的异常处理 blockHandler 和 fallback 配置项处理一下异常信息。
@SentinelResource 注解包含以下属性:
value:资源名称,必需项
entryType: entry 类型,可选项(默认为 EntryType.OUT)
blockHandler/blockHandlerClass: blockHandler 指定函数负责处理 BlockException 异常,可选项。
// blockHandler 函数默认需要和原方法在同一个类中。通过指定 blockHandlerClass 为对应类的 Class 对象,则可以指定其他类中的函数,但注意对应的函数必需为 static 函数,否则无法解析。
// blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型必须和原方法一致并且最后加一个类型为 BlockException 的异常参数用于接收对应的异常。
fallback /fallbackClass: fallback 指定的函数负责处理业务运行的异常,可选项。
// fallback 函数默认需要和原方法在同一个类中。通过指定fallbackClass 为对应类的 Class 对象,则可以指定指定为其他类的函数,但注意对应的函数必需为 static 函数,否则无法解析。
// fallback 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型必须和原方法一致并且最后加一个类型为 Throwable 的异常参数用于接收对应的异常。
defaultFallback:默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑。
// 默认需要和原方法在同一个类中,通过指定 fallbackClass 为对应类的 Class 对象,则可以指定指定为其他类的函数,但注意对应的函数必需为 static 函数,否则无法解析。
// defaultFallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了fallback和 defaultFallback,则只有 fallback会生效。
exceptionsToIgnore:用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
4. Sentinel控制台功能简介
4.1 实时监控
用来显示同一个服务下的所有机器的簇点信息,并且是以秒级展示的。
实时监控仅存储 5 分钟以内的数据,如果需要持久化,需要通过调用实时监控接口来定制。
注意:请确保 Sentinel 控制台所在的机器时间与自己应用的机器时间保持一致,否则会导致拉不到实时的监控数据
4.2 簇点链路
实时的去拉取指定客户端资源的运行情况。它一共提供两种展示模式:一种用树状
结构展示资源的调用链路,另外一种以列表
展示资源的运行情况。
簇点监控是内存态的信息,它仅展示启动后调用过的资源。
4.3 规则管理
1. 流控规则:监控流量的QPS(每秒访问频率)或并发线程数。当达到指定的阈值时,就对流量进行控制,避免被瞬时的流量高峰击垮。
- 点击簇点链路,可以看到访问过的接口地址。
- 点击对应的流控按钮,就进入流控规则配置页面。
// 资源名:资源唯一名称,默认就是请求路径。
// 针对来源:对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制
// 阈值类型/单机阈值;
// QPS: 当调用该接口的QPS达到阈值的时候,进行限流
// 线程数:当调用该接口的线程数达到阈值的时候,进行限流
// 流控模式:
// 直接:接口达到限流条件时,该接口开启限流
// 关联:当关联的资源接口达到限流条件(此时你页面填的什么QPS,这时是用于设置关联接口的,那个接口满足这个条件时)时,开启对指定接口的限流 关联:选择关联的话,会多出一行,让你填 关联 资源(写上你的访问路径)
// 链路:当从某个接口过来的资源达到限流条件时,该接口开启限流 链路:选择关联的话,会多出一行,让你填 入口 资源(写上你的访问路径)
// 解释一下,比如控制器有2个方法 A方法与B方法,需要调用某个资源(C方法),对该资源进行链路流控,设置入口资源为A方法,阈值类型选QPS,单机阈值填3,则当通过A方法访问C方法时,每秒访问量大于3的时候就进行流控,而通过B方法调用C方法不会出现流控。
// 流控效果:
// 快速失败: 直接失败,抛出异常(FlowException),不做任何额外的处理,是最简单的效果。
// Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。 点该选项,会多出一行,让你填 预热时长 单位S
// 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设置一个超时时间,当请求时间超过超时间时间还未处理,则会被丢弃。 点该选项,会多出一行,让你填 超时时间 单位ms
关于Warm Up
关于Warm Up:该方式只针对 QPS 流控,对并发线程数流控不支持
该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
比如秒杀系统在开启瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
预热底层是根据令牌桶算法实现的,源码对应得类在 com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController 中,算法中有一个冷却因子coldFactor,默认值是3,即请求 QPS 从 threshold(阈值) / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。
比如通过 sentinel-dashboard 设定 testWarmUP 资源的 QPS 阈值为20,流控效果为 warm up,预热时长为10秒,如下图所示,testWarmUP 资源刚开始限流的阈值为 20/3=7,但经过10秒的预热后,慢慢将阈值升至20。
关于排队等待
关于排队等待:该方式只针对QPS流控,并发线程数流控不支持。
该方式主要用于处理间隔性突发的流量。假设某时刻来了大流量的请求,如果此时要处理所有请求,很可能会导致系统负载过高,影响稳定性。
但其实接下来几秒可能系统处于空闲状态,若直接把多余的请求丢弃则没有充分利用系统的处理能力,所以我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
排队等待的方式会以匀速排队方式严格控制请求通过的间隔时间,也就是让请求以均匀的速度通过,其余的排队等待,它还会让设置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
Sentinel会以固定的间隔时间让请求通过, 访问资源,当请求到来时,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;
否则,计算当前请求的预期通过时间,如果该请求的预期通过时间大于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过;反之,则马上抛出阻塞异常。
sentinel-dashboard 对 service 资源设置限流阈值为10,流控效果为排队等候,每秒10次请求时,再有请求就排队等候,等待超时时间为 10000ms,超时过后,请求将被踢出排队队列,返回限流异常。
2. 降级规则:设置当满足什么条件的时候,对服务进行降级。
降级:当调用链路中某个资源出现不稳定,例如,表现为请求超时,请求异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败
,避免影响到其它的资源。
RT:平均响应时间,当资源的平均响应时间超过阈值(单位ms)后,资源进入准降级状态。
异常比例数:当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态。
异常数:当资源在 1 分钟的异常数目超过阈值之后会进行服务降级。
3. 热点规则:一种更细粒度的流控规则, 它允许将规则具体到参数级别上
。
热点就是经常访问的数据,很多时候我们肯定希望统计某个访问频次数据并对其进行限流,比如:
商品 ID 为参数,针对一段时间内最常购买的商品 ID 并进行限制。
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
如果你想对参数的值设置例外项,比如值为多少,就不对其限流
4. 授权规则:根据调用来源来判断该次请求是否允许放行。
资源名:指被访问的资源,只有符合要求的请求来源才能访问该资源。
流控应用:指请求来源标识。多个用 “,” 分隔
流控应用怎么声明?:Sentinel提供了 RequestOriginParser 接口, 我们只需要实现该接口即可。
实现的本质:利用request判断请求来源。
// 举例:若来源请求参数上有 serviceName 这个参数,且参数值是pc时,则不能进行访问。
@Component
public class RequestOriginParserDefinition implements RequestOriginParser{
@Override
public String parseOrigin(HttpServletRequest request) {
String serviceName = request.getParameter("serviceName");
return serviceName;
}
}
访问 http://localhost:8091/order/message1?serviceName=pc观察结果。
5. 系统规则:是运维级别的人员去处理的,不介绍。
5. Sentinel自定义异常返回
当出现异常时,sentinel都会返回这个页面,让我们不知道,它是犯了什么规则出现的异常。所以我们需要自定义异常返回。
//sentinel提供了UrlBlockHandler接口,用于我们自定义Sentinel异常。
@Component
public class ExceptionHandlerPage implements UrlBlockHandler {
// BlockException 异常接口,包含Sentinel的五个异常
// FlowException 限流异常
// DegradeException 降级异常
// ParamFlowException 参数限流异常
// AuthorityException 授权异常
// SystemBlockException 系统负载异常
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException e) {
if (e instanceof FlowException) {
// 限流异常
} else if (e instanceof DegradeException) {
// 降级异常
} else {
// 自定义处理
}
}
}
6. @SentinelResource注解
@SentinelResource 用于定义资源埋点
,设置资源名称。且还能提供异常处理配置。
// 需求:要对 message 这个资源做处理。
@Service
@Slf4j
public class OrderServiceImpl3 {
int i = 0;
@SentinelResource(
value = "message", // 0. 定义资源名为message
blockHandlerClass = OrderServiceImpl3BlockHandlerClass.class, // 1. 用于指定异常处理所在的类。
blockHandler = "blockHandler", // 2. 用于指定是异常处理类中的哪个方法。
fallbackClass = OrderServiceImpl3FallbackClass.class,
fallback = "fallback"
)
public String message() {
i++;
if (i % 3 == 0) {
throw new RuntimeException();
}
return "message4";
}
}
@Slf4j
public class OrderServiceImpl3BlockHandlerClass {
//注意这里必须使用static修饰方法
public static String blockHandler(BlockException ex) {
log.error("{}", ex);
return "接口被限流或者降级了...";
}
}
@Slf4j
public class OrderServiceImpl3FallbackClass {
//注意这里必须使用static修饰方法
public static String fallback(Throwable throwable) {
log.error("{}", throwable);
return "接口发生异常了...";
}
}
7. Feign整合Sentinel
以Springcloud Alibaba-5-并发访问为例
7.1 添加依赖
<!--sentinel客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
7.2 配置文件开启Feign对sentinel的支持
server:
port: 8091
tomcat:
threads:
max: 10 #tomcat的最大并发值修改为10
spring:
application:
name: service-order
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
port: 8719
dashboard: localhost:9000
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
feign: # 开启Feign对sentinel的支持
sentinel:
enabled: true
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
7.3 为订单服务增加降级效果
需求:为订单服务增加降级效果
@FeignClient(name = "service-product", fallback = ProductServiceApiFallBack.class)
public interface ProductServiceApi {
// 根据商品名称获取商品信息
@GetMapping("/product/getProductInfo/{pname}")
public Product getProductInfo(@PathVariable String pname);
// 保存或更新商品信息
@PutMapping("/product/saveOrUpdate}")
public void saveOrUpdate(@RequestBody Product product);
}
//容错类要求必须实现被容错的接口,并为每个方法实现容错方案
@Component
public class ProductServiceApiFallBack implements ProductServiceApi {
// 根据商品名称获取商品信息
@Override
public Product getProductInfo(String pname) {
Product product = new Product();
product.setPname("兜底商品数据");
return product;
}
// 保存或更新商品信息
@Override
public void saveOrUpdate(Product product) {
System.out.println("兜底数据");
}
}
现象:把产品服务关了,订单服务在调用远程服务不成功时,就会返回一个 商品名为 兜底商品数据的 商品给订单服务