Spring Cloud Alibaba学习笔记
Sentinel流量防卫兵
热点参数限流
概念
- 热点参数限流指的是,在流控规则中指定对某参数的 QPS 限流后,当所有对该资源的请求 URL 中携带有该指定参数的请求 QPS 达到了阈值,则发生限流。
- 前面的流控规则是方法级别的限流,而这里的热点参数限流则是方法参数级别的限流,是基于令牌桶算法实现的限流。
动态设置
- 创建工程:复制 06-consumer-sentinel-degrade-method-8080 工程,重命名为 06-consumer-sentinel-paramflow-8080。
- 修改处理器类:添加两个方法
@SentinelResource(value = "paramFlowRule", fallback = "getCompluxFallback")
@GetMapping("/complux")
public String getComplux(Integer id, String name) {
return "complux: " + id + ", " + name;
}
/**
* 降级方法
*/
public String getCompluxFallback(Integer id, String name) {
return "complux fallback: " + id + ", " + name;
}
- dashboard 设置规则:在启动了消费者工程后,再设置 Sentinel 控制台。
- 测试:直接访问 http://localhost:8080/consumer/depart/complux 不会降级,但是访问 http://localhost:8080/consumer/depart/complux?id=5&name=zhangsan QPS超过阈值会发生熔断降级,这里的统计窗口时长严格来讲应该是熔断时长。
参数例外项
- 参数例外项是指,对于热点参数中某个或某些特殊值单独设置规则。参数类型仅支持基本数据类型或其对应的包装类型,及 String 类型。
代码设置
- 直接在 06-consumer-sentinel-paramflow-8080 工程进行修改,在启动类中添加如下代码。
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Consumer068080 {
public static void main(String[] args) {
SpringApplication.run(Consumer068080.class, args);
initRule();
}
private static void initRule() {
List<ParamFlowRule> rules = new ArrayList<>();
rules.add(paramFlowRule());
ParamFlowRuleManager.loadRules(rules);
}
private static ParamFlowRule paramFlowRule() {
ParamFlowRule rule = new ParamFlowRule();
rule.setResource("/complux");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
rule.setCount(2);
rule.setParamIdx(1);
rule.setDurationInSec(10);
List<ParamFlowItem> items = new ArrayList<>();
items.add(nameParamItem("human", 100));
items.add(nameParamItem("test", 10));
rule.setParamFlowItemList(items);
return rule;
}
private static ParamFlowItem nameParamItem(String paramValue, int count) {
ParamFlowItem item = new ParamFlowItem();
item.setClassType(String.class.getName());
item.setObject(String.valueOf(paramValue));
item.setCount(count);
return item;
}
}
系统自适应限流
概念
- Sentinel 系统自适应限流对应用级别入口流量进行整体控制,结合应用的 Load、CPU 使用率、平均 RT、入口 QPS 和入口并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
- 由于该限流方式中阈值的设置需要很多系统软硬件相关的数据,而与代码关系不大,所以这种限流方式一般是由运维来设置的。
规则模式
- 系统规则目前支持五种模式:
系统负载 Load
- 该模式仅对 Linux/Unix-like 系统生效。当系统 CPU 最近一分钟的负载量 load1 超过了设置的阈值时会触发系统保护,即对再来的请求进行限流处理。这个阈值就是系统负载容量,系统容量可以由 maxQps * minRt 估算得出。不过,也可以通过 CPU cores * 2.5 计算出其参考数值。
CPU 使用率
- 当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
平均响应时间 RT
- 当对当前应用上所有入口流量的平均 RT 达到阈值时触发系统保护,单位是毫秒。
并发线程数
- 当对当前应用的所有入口流量进行处理的所有线程数量达到阈值时触发系统保护。
入口 QPS
- 当对当前应用的所有入口流量的总 QPS 达到阈值时触发系统保护。
网关流控
- Sentinel 对于主流网关限流的实现,是通过 Sentinel API Gateway Adapter Common 这个公共适配器模块实现的。
这个适配模块提供了两种维度的限流
- Route 维度:根据网关路由中指定的路由 id 进行路由。
- API 维度:使用 Sentinel 提供的 API 自定义分组进行限流,是比 Route 维度更加细粒度的限流。
搭建测试环境
depart 工程
- 创建工程:复制 02-consumer-nacos-8080 工程,重命名为 06-consumer-depart-sentinel-gateway-8180 在此基础上进行修改。
- 配置文件:
- 处理器DepartController:将原有 Controller 中的所有处理器方法全部删除,写入如下新的方法。
- 启动工程,测试接口:http://localhost:8180/consumer/depart/get/1
staff 工程
- 创建工程:复制 06-consumer-depart-sentinel-gateway-8180 工程,重命名为 06-consumer-staff-sentinel-gateway-8280,在其基础上进行修改。
- 修改配置文件:
- 修改处理器:将原处理器删除,再新建如下处理器。
- 启动工程,测试接口:http://localhost:8280/consumer/staff/get/1
Route 维度限流
- 创建工程:复制 05-gateway-config-9000 工程,重命名为 05-gateway-sentinel-route-9000,在其基础上进行修改。
- 添加依赖:
<!-- sentinel 与 spring cloud gateway 适配依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
<!-- sentinel 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 修改配置文件:
server:
port: 9000
spring:
application:
name: msc-gateway-sentinel
cloud:
nacos:
discovery:
server-addr: 192.168.0.100:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: depart_route
predicates:
- Path=/consumer/depart/get/**
uri: lb://msc-consumer-depart
- id: staff_route
predicates:
- Path=/consumer/staff/get/**
uri: lb://msc-consumer-staff
- 定义配置类:
@Configuration
public class GatewayConfig {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
// 负责视图解析与生成
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
// 负责gateway中http消息的读写操作配置
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 被限流后的异常处理器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 实现 sentinel 限流的全局 Filter
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter globalFilter() {
return new SentinelGatewayFilter();
}
}
- 修改启动类:
@SpringBootApplication
public class Gateway069000 {
public static void main(String[] args) {
SpringApplication.run(Gateway069000.class, args);
initRule();
initBlockHandlers();
}
private static void initRule() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(gatewayFlowRule());
GatewayRuleManager.loadRules(rules);
}
/**
* 对名称为 staff_route 的路由规则进行限流
*/
private static GatewayFlowRule gatewayFlowRule() {
// 定义一个 Gateway 限流规则实例
GatewayFlowRule rule = new GatewayFlowRule();
// 指定规则模式是 route 限流,其为默认值
rule.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID);
// 指定 sentinel 资源名称为 路由规则id
rule.setResource("staff_route");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(2);
return rule;
}
/**
* 路由限流降级
*/
private static void initBlockHandlers() {
GatewayCallbackManager.setBlockHandler((exchange, e) -> {
// 从请求中获取url
URI uri = exchange.getRequest().getURI();
// 将响应数据写入到map
Map<String, Object> map = new HashMap<>(2);
map.put("uri", uri);
map.put("msg", "访问量过大,请稍后再试");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
});
}
}
- 启动工程,访问接口 http://localhost:9000/consumer/depart/get/1 不限流,但是访问 http://localhost:9000/consumer/staff/get/1 会发生限流。
API 维度限流
- 使用 Route 限流,其仅会对指定的 Route 路由的请求进行限流,而对于更加细粒度的访问进行限流控制却做不到。例如,对于具体携带了哪些参数或参数值的请求进行限流。
- 创建工程:复制 06-gateway-sentinel-route-9000 工程,重命名为 06-gateway-sentinel-api-9000。
- 修改启动类:
@SpringBootApplication
public class Gateway069000 {
public static void main(String[] args) {
SpringApplication.run(Gateway069000.class, args);
initCustomizedApis();
initRule();
initBlockHandlers();
}
private static void initCustomizedApis() {
// 定义一个名称为 depart_api 的路由 api
ApiDefinition departApi = new ApiDefinition("depart_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/consumer/depart/get/**")
// 指定该路由api对于请求的匹配策略是 前缀匹配
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
// 定义一个名称为 staff_api 的陆游与 api
ApiDefinition staffApi = new ApiDefinition("staff_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/consumer/staff/get/2"));
add(new ApiPathPredicateItem().setPattern("/consumer/staff/get/3")
// 指定该路由api对于请求的匹配策略是 精准匹配
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_EXACT));
}});
Set<ApiDefinition> definitions = new HashSet<>();
definitions.add(departApi);
definitions.add(staffApi);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
private static void initRule() {
Set<GatewayFlowRule> rules = new HashSet<>();
// 这里仅对/consumer/depart/get/..形式的请求进行限流
rules.add(gatewayFlowRule("depart_api", 1));
// 这里仅对/consumer/staff/get/2与/consumer/staff/get/3这两个请求进行限流
rules.add(gatewayFlowRule("staff_api", 2));
GatewayRuleManager.loadRules(rules);
}
/**
* 对名称为 staff_route 的路由规则进行限流
*/
private static GatewayFlowRule gatewayFlowRule(String apiName, int count) {
// 定义一个 Gateway 限流规则实例
GatewayFlowRule rule = new GatewayFlowRule();
// 指定规则模式为路由api限流
rule.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME);
// 指定 sentinel 资源名称为 路由规则id
rule.setResource(apiName);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(count);
return rule;
}
/**
* 路由限流降级
*/
private static void initBlockHandlers() {
GatewayCallbackManager.setBlockHandler((exchange, e) -> {
// 从请求中获取url
URI uri = exchange.getRequest().getURI();
// 将响应数据写入到map
Map<String, Object> map = new HashMap<>(2);
map.put("uri", uri);
map.put("msg", "访问量过大,请稍后再试");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
});
}
}
- 测试下限流效果:
动态规则扩展
DataSource扩展
- 简介:https://github.com/alibaba/Sentinel/wiki/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%99%E6%89%A9%E5%B1%95
- 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更。
- 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
- 官方推荐:Spring Cloud Alibaba 中的配置中心使用的是 Nacos,所以直接使用推送模式是比较方便的高效的。
示例演示
- 创建工程:复制 06-consumer-sentinel-flowcontrol-8080 工程,重命名为 06-consumer-sentinel-persist-8080,在其基础上进行修改。
- 添加依赖:
<!-- sentinel 的 nacos 数据源依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- 修改配置文件:
spring:
application:
name: msc-consumer-depart
cloud:
nacos:
discovery:
server-addr: 192.168.0.100:8848
sentinel:
transport:
dashboard: 192.168.254.130:8888
port: 8719
eager: true
# 指定nacos数据源
datasource:
# 指定数据源名称,可以随意
my-flow-rule:
nacos:
server-addr: 192.168.0.100:8848
# 指定配置文件名称,可以随意
dataId: my_consumer_rule
# 指定配置文件中的规则类型
rule-type: flow
# 指定配置文件中的数据格式
data-type: json
- 新增流控规则配置:在 Nacos Config 中添加流控规则的配置文件。
[
{
"resource": "qpsFlowRule",
"limitApp": "default",
"grade": 1,
"count": 2
}
]
- 启动工程,然后去 Sentinel Dashboard 查看:可以看到流控规则已经加载到了
- 测试访问 http://localhost:8080/consumer/depart/get/1,验证下流控效果。
Sentinel 规则持久化
- 上面的方式虽然将流控规则持久化到了 Nacos Config,但还存在问题:如果修改 Sentinel Dashboard 中的流控规则后,Nacos Config 并不会改变,下次重启应用时,加载到的流控规则还是之前 Nacos Config 中配置的。
改造 Sentinel Dashboard
- 修改 pom:
- 目录复制:将 test 下的如下 nacos 目录,整体复制到 main 下的如下 rule 目录中。
- 修改控制器类:打开 sentinel-dashboard 模块下的 controller/v2 子包中的 FlowControllerV2 类,对其进行两处修改。
- 修改 NacosConfig:
- 修改页面:将 dashboard.flowV1()修改为 dashboard.flow()。
- 重新打包 dashboard,然后启动新的 dashboard。
java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -Dsentinel.dashboard.auth.username=sentinel -Dsentinel.dashboard.auth.password=123456 -jar sentinel-dashboard.jar
- 本地启动 DashboardApplication:
修改微服务应用
- 修改 06-consumer-sentinel-persist-8080 工程:
- dataId 和 groupId 在源码中写死了:
- 修改 02-provider-nacos-8081 工程:直接修改该工程的配置文件即可。
- 分别启动提供者和消费者工程,测试访问 http://localhost:8080/consumer/depart/get/1 此时是没有流控的。我们可以动态的添加流控规则:
- 验证:在 Sentinel Dashboard 修改流控规则,Nacos Config 中也会对应修改配置文件。Nacos 中修改流控配置,Sentinel Dashboard 也会被修改。访问 http://localhost:8080/consumer/depart/get/1,验证下限流效果。
集群流控
概述
为什么需要集群流控
- 有这样一种场景:某微服务由 10 台主机构成的集群提供,现需要控制该微服务的某 API 总 QPS 为 50,如何实现?为每台主机分配单机 QPS 阈值为 5 可以实现吗?不可以。因为对各个主机的访问可能并不均衡,可能有些尚未到达阈值,而有些由于已经超过了阈值而出现了限流。
- 这种场景下就需要来一个专门的主机,用于统计、分配集群中 QPS,以达到最大限度的降低由于流控而拒绝的请求数量,这就是集群流控。
集群流控原理
集群流控*有两种角色:
- Token Client:集群流控客户端,用于向所属 Token Server 通信,以请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
- Token Server:集群流控服务端,处理来自 Token Client 的令牌请求,根据配置的集群规则判断是否应该发放 Token(是否允许通过)。
Token Server 启动模式
- 根据 Token Server 启动方式的不同,可以分为两种:
独立模式
- Token Server 作为独立的进程启动,独立部署,隔离性好,但是需要额外的部署操作。
嵌入模式
- Token Client 中的某个被指定为 Token Server,该主机同时具备 Server 与 Client 两种身份。该模式下集群中各个主机都是对等的,Token Server 和 Client 可以随时通过提交一个 HTTP 请求进行转变,无需单独部署,灵活性比较好。但隔离性不好,可能会对充当 Server 的 Client 产生影响,且 Token Server 的性能也会由于访问量的增加而受影响。
在nacos新建配置
- 配置内容:
[
{
"resource": "qpsFlowRule",
"grade": 1,
"count": 2,
"clusterMode": true,
"clusterConfig": {
"flowId": 123456,
// 0: 单机均摊;1: 总体阈值
"thresholdType": 1,
// true: 如果Token Server不可用了,则count变为单机阈值
"fallbackToLocalWhenFail": true
}
}
]
- 新建配置:在 Nacos Config 的默认命名空间 public 中新建一个名称为 consumer_rule 的配置,作为集群流控规则的动态数据源。
示例演示
定义 Token Server 工程
- 创建工程:任意复制一个 Spring Cloud Alibaba 工程,例如 06-gateway-sentinel-api-9000 工程,重命名为 06-cluster-sentinel-token-server-9000,在此基础上进行修改。
- 主要依赖如下:
- 修改配置文件:
server:
port: 9000
spring:
application:
name: msc-cluster-token # 该名称必须与client微服务名称相同
cloud:
nacos:
discovery:
server-addr: 192.168.0.100:8848
sentinel:
transport:
dashboard: 192.168.254.130:8888
eager: true
datasource:
my-flow-rule:
nacos:
server-addr: 192.168.0.100:8848
data-id: consumer_rule
rule-type: flow
data-type: json
- 修改启动类:
@SpringBootApplication
public class TokenServer069000 {
public static void main(String[] args) throws Exception {
SpringApplication.run(TokenServer069000.class, args);
// ServerTransportConfig 集群流控中Token Server与Client通信相关的配置
ClusterServerConfigManager.loadGlobalTransportConfig(
// 指定当前Token Server的端口号
new ServerTransportConfig().setPort(9999));
// 指定当前Token Server的管理范围,即server/client的微服务名称
ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton("msc-cluster-token"));
// 创建并启动一个Token Server
ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();
tokenServer.start();
}
}
定义 Token Client 工程
- 创建工程:复制 06-consumer-sentinel-persist-8080 工程,重命名为 06-cluster-sentinel-token-client-8080。
- 修改依赖:和 06-cluster-sentinel-token-server-9000 一样。
- 修改配置文件:
server:
port: 8080 # 修改端口号启动多个Client
spring:
application:
name: msc-cluster-token
cloud:
nacos:
discovery:
server-addr: 192.168.0.100:8848
sentinel:
transport:
dashboard: 192.168.254.130:8888
eager: true
datasource:
my-flow-rule:
nacos:
server-addr: 192.168.0.100:8848
dataId: consumer_rule
rule-type: flow
data-type: json
- 修改启动类:
@SpringBootApplication
public class TokenClient068080 {
public static void main(String[] args) {
SpringApplication.run(TokenClient068080.class, args);
// 指定当前 Token Client
ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
// 指定当前 Token Client 所要连接的 Token Server
ClusterClientAssignConfig assignConfig = new ClusterClientAssignConfig();
assignConfig.setServerHost("localhost");
assignConfig.setServerPort(9999);
ClusterClientConfigManager.applyNewAssignConfig(assignConfig);
}
}
测试
查看Nacos Config
- 启动工程:分别启动 04-provider-nacos-8081、06-cluster-sentinel-token-server-9000、06-cluster-sentinel-token-client-8080(修改端口号启动多个Client:分别是7070、8080、9090)
查看 Sentinel Dashboard
- 流控规则:
- 集群限流 - Token Server 列表:
- 将 Server ID 为
localhost:9999(自主指定)
的这一项移除“已选取的 client 列表”:
- 为 Server ID 为
192.168.0.100@8721
的这一项“选取的 client 列表”
- 这个时候再看下连接详情:
- 集群限流 - Token Client 列表:
- 测试地址:http://localhost:8080/consumer/depart/get/1。调整配置参数,验证集群流控的效果。