Spring Cloud Alibaba之服务限流降级Sentinel
Sentinel概述
Sentinel: 分布式系统的流量防卫兵
Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 分为两个部分:
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
Sentinel 的主要特性:
Sentinel 的开源生态:
GitHub地址: https://github.com/alibaba/Sentinel
官网: https://sentinelguard.io/zh-cn/
项目集成Sentinel
相关依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<dependencyManagement>
<dependencies>
<!--整合spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在Sentinel控制台搭建成功后配置Sentinel
spring:
cloud:
sentinel:
transport:
# 指定sentinel 控制台的地址
dashboard: IP:8858
查看Sentinel端点
暴露端点
management:
endpoints:
web:
exposure:
include: '*'
访问: http://localhost:8082/actuator/sentinel
{
"blockPage": null,
"appName": "pay-center",
"consoleServer": [
],
"coldFactor": "3",
"rules": {
"systemRules": [
],
"authorityRule": [
],
"paramFlowRule": [
],
"flowRules": [
],
"degradeRules": [
]
},
"metricsFileCharset": "UTF-8",
"filter": {
"order": -2147483648,
"urlPatterns": [
"/**"
],
"enabled": true
},
"totalMetricsFileCount": 6,
"datasource": {
},
"clientIp": "192.168.42.16",
"clientPort": "8719",
"logUsePid": false,
"metricsFileSize": 52428800,
"logDir": "C:\\Users\\Administrator\\logs\\csp\\",
"heartbeatIntervalMs": 10000
}
Sentinel控制台
首先确定spring-cloud-alibaba对应Sentinel版本,然后搭建对应版本的Sentinel控制台
<!--点击artifactId进入spring-cloud-alibaba依赖管理中查看nacos的版本-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<sentinel.version>1.8.1</sentinel.version>
Docker部署Sentine
https://blog.csdn.net/qq_38628046/article/details/106875278
添加Sentinel的配置并重启项目
spring:
cloud:
sentinel:
transport:
# 指定sentinel 控制台的地址
dashboard: IP:8858
由于Sentinel是懒加载,需要访问如(http://192.168.179.1:8082/test/1
)任一接口一次才会出现相应服务
多次访问接口,发现无任何监控信息
查看Docker容器日志,发现似乎是Ip的原因造成的,且IP陌生。
ERROR 1 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher : Failed to fetch metric from <http://192.168.42.16:8719/metric?startTime=1638617140000&endTime=1638617146000&refetch=false> (ConnectionException: Connection timed out)
ERROR 1 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher : Failed to fetch metric from <http://192.168.42.16:8719/metric?startTime=1638617147000&endTime=1638617153000&refetch=false> (ConnectionException: Connection timed out)
ERROR 1 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher : Failed to fetch metric from <http://192.168.42.16:8719/metric?startTime=1638617154000&endTime=1638617160000&refetch=false> (ConnectionException: Connection timed out)
ERROR 1 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher : Failed to fetch metric from <http://192.168.42.16:8719/metric?startTime=1638617161000&endTime=1638617167000&refetch=false> (ConnectionException: Connection timed out)
关闭服务,然后在sentinel控制台的机器列表移除错误的机器IP
修改Sentinel配置
spring:
cloud:
sentinel:
transport:
# 指定sentinel 控制台的地址
dashboard: IP:8858
#指定和控制台通信的ip,如不配置,会自动选择一个ip注册,可能造成错误
clientIp: localhost
重启服务
多次访问http://192.168.179.1:8082/test/1
接口服务
请求test/{id}接口,实时监控中没有发现该接口,折腾许久未找到原因,无解,于是直接下载Jar,本地运行。
下载地址:https://github.com/alibaba/Sentinel/releases
java -jar sentinel-dashboard-1.8.1.jar
修改Sentinel配置(改Sentinel服务地址为本机),访问http://localhost:8082/test/1
服务地址,最后访问http://localhost:8080/
Sentinel的各种规则
流控规则
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
一条限流规则主要由下面几个因素组成,可以组合这些元素来实现不同的限流效果:
resource:资源名,即限流规则的作用对象
count: 限流阈值
grade: 限流阈值类型(QPS 或并发线程数)
limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
strategy: 调用关系限流策略
controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)
流量控制主要有两种统计类型
统计并发线程数
统计QPS
基于调用关系的流量控制
直接
关联
链路
当QPS超过某个阈值的时候,则采取措施进行流量控制
直接拒绝
Warm Up
匀速排队
直接拒绝
直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
源码类: com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
Warm Up
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
根据codeFactor(默认3)的值,从阈值/codeFactor,经过预热时长才到达设置的QPS阈值
官方原理分析文档
https://github.com/alibaba/Sentinel/wiki/%E9%99%90%E6%B5%81---%E5%86%B7%E5%90%AF%E5%8A%A8
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
匀速排队
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
匀速排队,让请求以均匀的速度通过,或值类型必须设成QPS,否则无效
官方文档
https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6-%E5%8C%80%E9%80%9F%E6%8E%92%E9%98%9F%E6%A8%A1%E5%BC%8F
源码: com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
流控测试
直接流控模式
当Qps超过1则报异常,进行限流。
关联流控模式
当关联的资源达到阈值,就限流自己
先循环执行调用/test2接口,再访问/test/{id}接口,/test/{id}接口将一直处于限流状态
链路流控模式
只记录指定链路上的流量
@Component
public class TestComponent {
@SentinelResource("common")
public String common() {
return "common";
}
}
@GetMapping("test-a")
public String testA() {
this.testService.common();
return "test-a";
}
@GetMapping("test-b")
public String testB() {
this.testService.common();
return "test-b";
}
当访问test-a接口达到Qps时限流,而无论如何访问test-b都不会限流
降级规则
一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
因此需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。
Sentinel熔断策略:
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% – 100%。
异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
熔断降级规则重要属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
参考源码: com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule
热点参数限流
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
添加/testParam
接口
@GetMapping("/testParam")
@SentinelResource("param")
public String testParam(Integer a,String b){
return a + "-------------" +b;
}
请求http://localhost:8082/testParam?a=1&b=Sentinel
,由于对第一个参数限制,当Qps达到1则限流抛出异常
请求http://localhost:8082/testParam?a=&b=Sentinel
,无论如何请求都不会限流
当第一个参数值不是2时,Qps达到1则限流
当第一个参数值是2时,Qps达到100则限流
参考源码: com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowChecker
系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。
CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
参考源码: com.alibaba.csp.sentinel.slots.system.SystemRuleManager
授权规则
根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
Sentinel的规则持久化
拉模式:
客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
推模式:
规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
具体实现参考:https://github.com/alibaba/Sentinel/wiki/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%99%E6%89%A9%E5%B1%95
Sentinel的配置
应用端连接控制台的配置
spring:
cloud:
sentinel:
transport:
# 指定sentinel 控制台的地址
dashboard: IP:8858
#指定和控制台通信的ip,如不配置,会自动选择一个ip注册,可能造成错误
clientIp: localhost
# 指定和控制台通信的端口,默认值8719
# 如不设置,会自动从8719开始扫描,依次+1,直到找到未被占用的端口
port: 8719
# 心跳发送周期,默认值null,但在SimpleHttpHeartbeatSender会用默认值10秒
heartbeat-interval-ms: 10000
控制台配置
配置项 | 默认值 | 描述 |
---|---|---|
server.port | 8080 | 指定端口 |
csp.sentinel.dashboard.server | localhost:8080 | 指定地址 |
project.name | 指定程序的名称 | |
sentinel.dashboard.auth.username | sentinel | Dashboard登录账号 |
sentinel.dashboard.auth.password | sentinel | Dashboard登录密码 |
server.servlet.session.timeout | 30分钟 | 登录Session过期时间 配置为7200表示7200秒 配置为60m表示60分钟 |
Sentinel的使用
API方式
三个核心API:
SphU :定义资源,让资源受到监控,以及保护资源
Tacer:对想要的异常进行统计
ContextUtil: 实现调用来源,标记调用
注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常。
@GetMapping("/test-api")
public String testSentinelAPI(@RequestParam(required = false) String name) {
// 定义资源名称
String resourceName = "test-api";
// 实现调用来源,标记调
ContextUtil.enter(resourceName, "test-service");
Entry entry = null;
try {
entry = SphU.entry(resourceName);
// 被保护的业务逻辑
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("参数不合法");
}
return name;
}
// 如果被保护的资源被限流或者降级,就会抛BlockException
catch (BlockException blockException) {
return "限流或者降级";
} catch (IllegalArgumentException illegalArgumentException) {
// 自行调用 Tracer.trace(ex) 来记录业务异常
Tracer.trace(illegalArgumentException);
return "参数不合法";
} finally {
if (entry != null) {
// 退出entry
entry.exit();
}
ContextUtil.exit();
}
}
注解方式
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback配置项
使用详情参考: https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81
@GetMapping("/test-sentinel-resource")
@SentinelResource(
value = "test-sentinel-api",
blockHandler = "block",
//blockHandlerClass = BlockHandlerTestClass.class,
//fallbackClass = FallbackTestClass.class,
fallback = "fallback"
)
public String testSentinelResource(@RequestParam(required = false) String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("参数不合法");
}
return name;
}
public String block(String name, BlockException blockException) {
return "限流或者降级---------block";
}
public String fallback(String name, Throwable throwable) {
return "限流或者降级---------fallback";
}
将block()与fallback()进行抽离,使用blockHandlerClass与fallbackClass分别指定
@GetMapping("/test-sentinel-resource")
@SentinelResource(
value = "test-sentinel-api",
blockHandler = "block",
blockHandlerClass = BlockHandlerTestClass.class,
fallbackClass = FallbackTestClass.class,
fallback = "fallback"
)
public String testSentinelResource(@RequestParam(required = false) String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("参数不合法");
}
return name;
}
blockHandlerClass = BlockHandlerTestClass.clas:对应的
block
函数需要位于BlockHandlerTestClass
类中,并且必须为 public static 函数.
fallbackClass = FallbackTestClass.class:对应的
fallback
函数需要位于FallbackTestClass
类中,并且必须为 public static 函数.
public class BlockHandlerTestClass{
/**
* 处理限流或者降级
*/
public static String block(String name, BlockException blockException) {
return "BlockHandlerTestClass----限流或者降级-------block";
}
}
与RestTemplate和Feign集成
RestTemplate集成Sentinel
@SentinelRestTemplate注解同样提供fallback与blockHandler的处理
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SentinelRestTemplate {
String blockHandler() default "";
Class<?> blockHandlerClass() default void.class;
String fallback() default "";
Class<?> fallbackClass() default void.class;
String urlCleaner() default "";
Class<?> urlCleanerClass() default void.class;
}
定义限流降级异常处理类
public class BlockHandler {
public static SentinelClientHttpResponse handleException(HttpRequest request, byte[] body, ClientHttpRequestExecution clientHttpRequestExecution, BlockException blockException) {
return new SentinelClientHttpResponse("限流或者降级---------BlockHandler Class");
}
}
public class FallBackHandler {
public static SentinelClientHttpResponse fallBackHandle(HttpRequest request, byte[] body, ClientHttpRequestExecution clientHttpRequestExecution, BlockException blockException){
return new SentinelClientHttpResponse("限流或者降级---------FallBackHandler Class");
}
}
RestTemplate添加@SentinelRestTemplate注解
@Bean
//Ribbon负载均衡
@LoadBalanced
//让RestTemplate支持Sentinel限流
@SentinelRestTemplate(blockHandler = "handleException",blockHandlerClass = BlockHandler.class,
fallback = "fallBackHandle", fallbackClass = FallBackHandler.class)
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
return template;
}
}
@SentinelRestTemplate注解配置可关闭
resttemplate:
sentinel:
# 开发环境可以临时关闭: 降级,限流
# 默认true,设置成false,表示关闭@SentinelRestTemplate注解
enabled: false
Feign集成Sentinel
开启Feign对Sentinel的支持
feign:
sentinel:
# 为feign整合sentinel
enabled: true
创建限流降级容错类
直接实现被容错的接口,并为每个方法实现容错方案
实现FallbackFactory接口
@Component
public class UserCenterFeignClientFallback implements UserCenterFeignClient {
@Override
public UserDTO selectUserById(Integer id) {
UserDTO userDTO = new UserDTO();
userDTO.name("流控或降级-------implements UserCenterFeignClient");
return userDTO;
}
}
@Component
public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> {
@Override
public UserCenterFeignClient create(Throwable cause) {
return new UserCenterFeignClient() {
@Override
public UserDTO selectUserById(Integer id) {
UserDTO userDTO = new UserDTO();
userDTO.name("流控或降级-------implements FallbackFactory");
return userDTO;
}
};
}
}
为被限流降级容错的interface指定容错类
@FeignClient(name = "user-center",
// fallback = UserCenterFeignClientFallback.class,
fallbackFactory = UserCenterFeignClientFallbackFactory.class
)
public interface UserCenterFeignClient {
@GetMapping("/users/{id}")
UserDTO selectUserById(@PathVariable Integer id);
}
Sentinel的异常处理
统一处理返回异常: 实现BlockExceptionHandler接口重写handle()方法。
@Component
public class MyUrlBlockHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, BlockException e) throws Exception {
String msg = null;
if (e instanceof FlowException) {
msg = "限流---------FlowException";
} else if (e instanceof DegradeException) {
msg = ("降级---------DegradeException";
} else if (e instanceof ParamFlowException) {
msg = "热点参数限流---------ParamFlowException";
} else if (e instanceof SystemBlockException) {
msg = "系统规则---------SystemBlockException";
} else if (e instanceof AuthorityException) {
msg = "授权规则不通过---------AuthorityException";
}
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);
}
Sentinel实现来源区分
代码实现
@Component
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 从请求头/请求参数中获取名为origin的参数
// String origin = request.getHeader("request");
String origin = request.getParameter("origin");
// 获取不到origin参数则抛异常
if (StringUtils.isBlank(origin)) {
throw new IllegalArgumentException("请求不合法");
}
return origin;
}
}
针对来源限流测试
请求:http://localhost:8082/test?origin=web
限流---------FlowException
请求:http://localhost:8082/test?origin=app
: 随便访问
针对来源授权测试
请求:http://localhost:8082/test?origin=app
授权规则不通过---------AuthorityException
请求:http://localhost:8082/test?origin=web
: 通过
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/137015.html