介绍
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。
Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。(https://github.com/alibaba/Sentinel)
- 整体结构图如下,将原有的 Spring Cloud Gateway中集成Hystrix替换成Sentinel来进行限流、降级等功能, Hystrix和Sentinel的区别可以参考:[Hystrix和Sentinel对比][1],总结来说:Hystrix常用的线程池隔离会造成线程上下切换的overhead比较大;Hystrix使用的信号量隔离对某个资源调用的并发数进行控制,效果不错,但是无法对慢调用进行自动降级;Sentinel通过并发线程数的流量控制提供信号量隔离的功能
1.png
Gateway网关接入
- 创建项目mas-openapi-geteway,遵从SpringBoot家族开箱即用的惯例,在maven中加入如下配置:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
- gateWay内部实际使用的是Reactor模式,所有的请求都是异步非阻塞处理的,加入webFlex的配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
- gateWay的主要功能之一是转发请求,转发规则的定义主要包含三个部分,其中Route和Predicate必须同时申明:
Route(路由) | 路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,根据Predicate进行匹配转发。 | |
Predicate(谓语、断言) | 路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:Path、Query、Method、Header等,写法必须遵循 key=vlue的形式 | |
Filter(过滤器) | 过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容 |
- 规则可通过yml文件方式、代码方式和动态推送(通过配置中心Nacos推送)配置,这里网关的地址为localhost:9022
//通过配置文件配置
spring:
cloud:
gateway:
routes:
- id: gate_route
uri: http://localhost:9023
predicates:
## 当请求的路径为gate、rule开头的时,转发到http://localhost:9023服务器上
- Path=/gate/**,/rule/**
### 请求路径前加上/app
filters:
- PrefixPath=/app
- 转发规则(predicates),转发uri都设定为http://localhost:9023
规则 | 实例 | 说明 |
---|---|---|
Path | - Path=/gate/,/rule/ | ## 当请求的路径为gate、rule开头的时,转发到http://localhost:9023服务器上 |
Before | - Before=2017-01-20T17:42:47.789-07:00[America/Denver] | 在某个时间之前的请求才会被转发到 http://localhost:9023服务器上 |
After | - After=2017-01-20T17:42:47.789-07:00[America/Denver] | 在某个时间之后的请求才会被转发 |
Between | - Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver] | 在某个时间段之间的才会被转发 |
Cookie | - Cookie=chocolate, ch.p | 名为chocolate的表单或者满足正则ch.p的表单才会被匹配到进行请求转发 |
Header | - Header=X-Request-Id, \d+ | 携带参数X-Request-Id或者满足\d+的请求头才会匹配 |
Host | - Host=www.hd123.com | 当主机名为www.hd123.com的时候直接转发到http://localhost:9023服务器上 |
Method | - Method=GET | 只有GET方法才会匹配转发请求,还可以限定POST、PUT等请求方式 |
- 过滤器规则(Filter)
过滤规则 | 实例 | 说明 |
---|---|---|
PrefixPath | - PrefixPath=/app | 在请求路径前加上app |
RewritePath | - RewritePath=/test, /app/test | 访问localhost:9022/test,请求会转发到localhost:8001/app/test |
SetPath | SetPath=/app/{path} | 通过模板设置路径,转发的规则时会在路径前增加app,{path}表示原请求路径 |
注:当配置多个filter时,优先定义的会被调用,剩余的filter将不会生效
- 通过代码进行配置,将路由规则设置为一个Bean即可:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://httpbin.org"))
.route("host_route", r -> r.host("*.myhost.org")
.uri("http://httpbin.org"))
.route("rewrite_route", r -> r.host("*.rewrite.org")
.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
.uri("http://httpbin.org"))
.route("hystrix_route", r -> r.host("*.hystrix.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
.uri("http://httpbin.org"))
.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
.uri("http://httpbin.org"))
.route("limit_route", r -> r
.host("*.limited.org").and().path("/anything/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
.uri("http://httpbin.org"))
.build();
}
- 使用nacos实现动态路由,以上两种方式都是实现的静态配置路径,只能应对部分场景,接下来配置nacos实现动态配置以及配置的存储,由于gateWay并没有适配nacos,需要自定义监听器:
@Component
@Slf4j
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {
private String dataId = "gateway-router";
private String group = "DEFAULT_GROUP";
@Value("${spring.cloud.nacos.config.server-addr}")
private String serverAddr;
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher applicationEventPublisher;
private static final List<String> ROUTE_LIST = new ArrayList<>();
@PostConstruct
public void dynamicRouteByNacosListener() {
try {
ConfigService configService = NacosFactory.createConfigService(serverAddr);
configService.getConfig(dataId, group, 5000);
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
clearRoute();
try {
if (StringUtil.isNullOrEmpty(configInfo)) {//配置被删除
return;
}
List<RouteDefinition> gatewayRouteDefinitions = JSONObject.parseArray(configInfo, RouteDefinition.class);
for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
addRoute(routeDefinition);
}
publish();
} catch (Exception e) {
log.error("receiveConfigInfo error" + e);
}
}
@Override
public Executor getExecutor() {
return null;
}
});
} catch (NacosException e) {
log.error("dynamicRouteByNacosListener error" + e);
}
}
private void clearRoute() {
for (String id : ROUTE_LIST) {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
}
ROUTE_LIST.clear();
}
private void addRoute(RouteDefinition definition) {
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
ROUTE_LIST.add(definition.getId());
} catch (Exception e) {
log.error("addRoute error" + e);
}
}
private void publish() {
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
- 在nacos中增加一个规则:
[{
"filters": [],
"id": "baidu_route",
"order": 0,
"predicates": [{
"args": {
"pattern": "/baidu"
},
"name": "Path"
}],
"uri": "https://www.baidu.com"
}]
- 访问网关的路由规则,能看到刚刚加入的规则,访问http://localhost:9022/baidu时请求直接被转发到百度的首页了。
2.png
基础配置:
- 现在的请求通过经过gateWay网关时,需要在网关统一配置跨域请求,需求所有请求通过
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-headers: "*"
allow-credentials: true
allowed-methods:
- GET
- POST
- DELETE
- PUT
- OPTION
- eureka、admin-client、actuator健康检查配置,为之后的功能提供支持,此部分比较简单,不再赘述,加入以下maven依赖和配置
## maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
##配置项
spring:
application:
name: mas-cloud-gateway
boot:
admin:
client:
### 本地搭建的admin-server
url: http://localhost:8011
eureka:
client:
registerWithEureka: true
fetchRegistry: true
healthcheck:
enabled: true
serviceUrl:
defaultZone: http://localhost:6887/eureka/
enabled: true
feign:
sentinel:
enabled: true
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
- 若转发的目标地址为微服务中组件,不为具体ip:port形式的,应写成lb://mas-openapi-service形式,目标地址会从注册中心直接拉取
Sentinel
使用Sentinel作为gateWay的限流、降级、系统保护工具
基本概念
- 资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中都会用资源来描述代码块。只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
- 规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
依赖
<!--alibaba 流量卫士-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>${sentinel.version}</version>
</dependency>
同时也将SpringCloud、GateWay整合,加入如下配置
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.7.1</version>
</dependency>
- 配置
- 由于sentinel的工作原理其实借助于全局的filter进行请求拦截并计算出是否进行限流、熔断等操作的,增加SentinelGateWayFilter配置
@Bean//拦截请求
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
- sentinel 不仅支持通过硬代码方式进行资源的申明,还能通过注解方式进行声明,为了让注解生效,还需要配置切面类SentinelResourceAspect
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
- sentinel拦截包括了视图、静态资源等,需要配置viewResolvers以及拦截之后的异常,我们也可以自定义抛出异常的提示
public SentinelConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean//自定义异常
@Order(Ordered.HIGHEST_PRECEDENCE)
public ExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new ExceptionHandler(viewResolvers, serverCodecConfigurer);
}
- 自定义异常提示:当发生限流、熔断异常时,会返回定义的提示信息。
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
MasResponse<String> stringMasResponse = MasResponse.fail(
"限流了"
);
byte[] datas = JSON.toJSONString(stringMasResponse).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
return serverHttpResponse.writeWith(Mono.just(buffer));
}
不需要额外的配置,sentinel就已经可以正常工作了
网关流控实现原理
当通过 GatewayRuleManager 加载网关流控规则(GatewayFlowRule)时,无论是否针对请求属性进行限流,Sentinel 底层都会将网关流控规则转化为热点参数规则(ParamFlowRule),存储在GatewayRuleManager 中,与正常的热点参数规则相隔离。转换时 Sentinel 会根据请求属性配置,为网关流控规则设置参数索引(idx),并同步到生成的热点参数规则中。
外部请求进入 API Gateway 时会经过 Sentinel 实现的 filter,其中会依次进行 路由/API 分组匹配、请求属性解析和参数组装。Sentinel 会根据配置的网关流控规则来解析请求属性,并依照参数索引顺序组装参数数组,最终传入SphU.entry(res, args) 中。Sentinel API Gateway Adapter Common 模块向 Slot Chain 中添加了一个 GatewayFlowSlot,专门用来做网关规则的检查。GatewayFlowSlot 会从GatewayRuleManager中提取生成的热点参数规则,根据传入的参数依次进行规则检查。若某条规则不针对请求属性,则会在参数最后一个位置置入预设的常量,达到普通流控的效果。
3.png
- 过滤顺序:当请求到来时,会依次调用下面的规则进行校验,而chain继承自AbstractLinkedProcessorSlot<Object>,将过滤的规则依次进行直到不通过或者到最后一个规则。
##对应代码中定义的顺序
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// Prepare slot
chain.addLast(new NodeSelectorSlot());
chain.addLast(new ClusterBuilderSlot());
// Stat slot
chain.addLast(new LogSlot());
chain.addLast(new StatisticSlot());
// Rule checking slot
chain.addLast(new AuthoritySlot());
chain.addLast(new SystemSlot());
chain.addLast(new GatewayFlowSlot());
chain.addLast(new ParamFlowSlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
- 通过一层层的插点slot,以达到统计、限流、降级等功能,各个slot功能如下(按照执行顺序):
-
NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
-
ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
-
LogSlot:用来记录系统日志,当前通过数量、拒绝数量等
-
StatistcSlot 则用于记录,统计不同纬度的 runtime 信息;
-
AuthorizationSlot 则根据黑白名单,来做黑白名单控制;
-
SystemSlot 则通过系统的状态,当前的运行状态、CPU占用率等,来控制总的入口流量;
-
GatewayFlowSlot::网关限流规则
-
ParamFlowSlot:参数值限流规则定义
-
FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流
-
DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级;
注:Sentinel 1.6.0之后的版本引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API的实体和管理逻辑
-
- AuthorizationSlot:若未配置任何规则,所有的请求都将成功通过,规则字段解释如下:
resource | 指定访问的资源名称 |
limitApp | 限制来源,可通过英文逗号(,)指定多个来源限定,通常为请求者IP或者消费者名称 |
strategy | 限制模式,白名单还是黑名单,默认为白名单,启用后只有在白名单的来源才能通过 |
### 只允许来源为127.0.0.1或者localhost的请求才能通过
AuthorityRule rule = new AuthorityRule();
rule.setResource("info");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("127.0.0.1,localhost");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
###黑名单类似,来源为指定的值时将之前拒绝请求
rule.setStrategy(RuleConstant.AUTHORITY_BLACK);
rule.setLimitApp("127.0.0.1,localhost");
注:当其他的来源的应用访问时,该请求将无法通过,如果未查找到该请求来源将直接通过。
- SystemSlot:系统限制规则,作为整个系统的衡量标准,不需要指定资源名称,字段解释如下:
|||
|-|-|-|
|highestSystemLoad|最大的Load,目前只针对Unix/Linux生效,默认为-1不生效|
|highestCpuUsage |最高CPU使用率,范围[0-1],高于该值时将拒绝所有请求,做降级处理|
|qps |所有入口资源的QPS,默认为-1不生效|
|avgRt |所有入口流量的平均响应时间,ms为单位|
|maxThread|入口流量的最大并发数,默认为-1不生效|
##限制QPS为3,平均返回时间为200ms
List<SystemRule> rules = new ArrayList<>();
SystemRule rule = new SystemRule();
// rule.setHighestSystemLoad(3.0);//系统负载 只针对Linux/unix
rule.setHighestCpuUsage(-1);//当前系统的 CPU 使用率
rule.setAvgRt(200);//所有入口流量的平均响应时间 ms
rule.setQps(3);
rule.setMaxThread(500);
rules.add(rule);
SystemRuleManager.loadRules(rules);
访问本地API服务器,请求第一次时可以得到正确的返回信息,但是第二次访问时,由于上一次的请求耗时544/2ms,第二次请求将不会被通过
请求次数:2
请求路径:/api/query
访问成功了!! 我是API服务器
{"echoCode":500,"echoMessage":"限流了","success":false}
544ms
更改系统规则中的QPS参数,不限制平均响应时间
List<SystemRule> rules = new ArrayList<>();
SystemRule rule = new SystemRule();
// rule.setHighestSystemLoad(3.0);//系统负载 只针对Linux/unix
rule.setHighestCpuUsage(-1);//当前系统的 CPU 使用率
//rule.setAvgRt(200);//所有入口流量的平均响应时间 ms
rule.setQps(3);
rule.setMaxThread(500);
rules.add(rule);
SystemRuleManager.loadRules(rules);
由于我们设置的QPS为3,那么当我们请求10次时,前三次可以成功请求,但第四次开始,请求将不会被处理。
请求次数:10
请求路径:/api/query
访问成功了!! 我是API服务器
访问成功了!! 我是API服务器
访问成功了!! 我是API服务器
访问成功了!! 我是API服务器
{"echoCode":500,"echoMessage":"限流了","success":false}
{"echoCode":500,"echoMessage":"限流了","success":false}
{"echoCode":500,"echoMessage":"限流了","success":false}
{"echoCode":500,"echoMessage":"限流了","success":false}
{"echoCode":500,"echoMessage":"限流了","success":false}
{"echoCode":500,"echoMessage":"限流了","success":false}
530ms
- GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。其中网关限流规则 GatewayFlowRule 的字段解释如下:
resource | 资源名称,可以是网关中的 route 名称或者用户自定义的API 分组名称。 | |
resourceMode | 规则是针对 API Gateway 的route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是route。 | |
grade: | 限流指标维度,同限流规则的grade 字段。 | |
count: | 限流阈值 | |
intervalSec: | 统计时间窗口,单位是秒,默认是1 秒(目前仅对参数限流生效)。 | |
controlBehavior | 流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。 | |
burst: | 应对突发请求时额外允许的请求数目(目前仅对参数限流生效)。 | |
maxQueueingTimeoutMs: | 匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。 | |
paramItem: | 参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段: | |
parseStrategy: | 从请求中提取参数的策略,目前支持提取来源 | IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。 |
fieldName: | 若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。 | |
pattern 和 matchStrategy: | 为后续参数匹配特性预留,目前未实现。 |
- 根据GateWay提供的特性,可设置两个维度的限流和熔断,指定API或者转发路由,为了验证通过性,通过GateWay路由分别路由服务器CMS和PRODUCT服务器,并都将它们都注册到Eureka中,配置规则如下:
## 将包含content请求路径都转发到CMS服务器上,其他求都转发到product服务器上
- id: cms_route
uri: lb://mas-cms-service
predicates:
- Path=/{tenant}/service/content/**
- id: product_route
uri: lb://mas-product-service
predicates:
- Path=/**
路由转发正常
5.png
- 自定义路由拦截规则:在上图中我们定义并请求了三个路径,其中前面两个都是对CMS服务器的请求,所以对于前面两个请求对应的路由资源为cms_route,第三个请求的路由资源为product_route,限定第一个请求的路径每秒QPS为3时:
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("cms_route")
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID)//路由规则
.setCount(3)
.setGrade(RuleConstant.FLOW_GRADE_QPS)//限制规则为QPS
.setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);
针对同一个ROUTE_ID的请求,后面的将会被拦截,而不同ROUTE_ID的请求将不会受到影响。
7.png
- 自定义API拦截规则:若只拦截以{tenant}/service/content/place形式的接口时,需要根据正则表达式匹配该模式的请求地址并将其声明为可拦截的API资源,再设定限流、熔断规则
Set<ApiDefinition> definitions = new HashSet<>();
//显式申明API
ApiDefinition placeApi = new ApiDefinition("place_flow")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/lh9999/service/content/place/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(placeApi);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
//设置规则
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("place_flow")
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
.setCount(3)
.setGrade(RuleConstant.FLOW_GRADE_QPS)
.setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);
-
当同时请求服务器时,只要路径符合**/lh9999/service/content/place/****形式的请求都适用上面的规则,这里我们使用线程池模拟并发请求,请求第四次时将直接失败,限流效果还是很明显的。
6.png -
热点参数规则(ParamFlowRule)类似于流量控制规则(FlowRule)
属性 | 说明 | 默认值 |
---|---|---|
resource | 资源名,必填 | |
count 限流阈值,必填 | ||
grade | 限流模式 | QPS 模式 |
paramIdx | 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置 | |
paramFlowItemList | 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型 | |
clusterMode | 是否是集群参数流控规则 | false |
clusterConfig | 集群流控相关配置 |
- 热点:经常被访问的数据,作为公共资源,很多时候,我们希望统计访问某个热点数据中访问频次最高的几项数据,并将其设置访问限制,例如:
- 商品ID:针对某个热点商品ID设置访问限制
- 用户ID:针对某个热点用户ID设置访问限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
- 可以通过 SphU 类里面几个 entry 重载方法来传入对应的参数以便 Sentinel 统计,为对应的资源配置热点参数限流规则,并在 entry 的时候传入相应的参数,即可使热点参数限流生效。
public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException
public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException
- 实例:若一个用户短时间内登录次数过多,将限制其登录行为,防止恶意登录
@PostMapping("{account}/checkLogin")
@SentinelResource("checkLogin")
public BaasResponse<String> checkLogin(@PathVariable(value = "account") String account, @RequestBody String password) {
try {
SphU.entry("checkLogin", EntryType.IN, 1, account);
...chekcLogin
} catch (BlockException e) {
BaasResponse<String> response = new BaasResponse<>();
response.setSuccess(false);
response.setEchoMessage(account + "被限制登录");
return response;
}
return BaasResponse.success(account + "登录成功");
}
制定限制规则,时间窗口限制为1s,在这1s中所有用户最多允许登录5次,通过ParamFlowRuleManager.loadRuls我们可以很容易制定限流规则
List<ParamFlowRule> rules = new ArrayList<>();
ParamFlowRule rule = new ParamFlowRule();
//阈值类型:只支持QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//阈值
rule.setCount(5);
//资源名
rule.setResource("checkLogin");
rule.setParamIdx(0);//指配热点参数的下标
//统计窗口时间长度
rule.setDurationInSec(1);
List<ParamFlowItem> items = new ArrayList<>();
ParamFlowItem item = new ParamFlowItem();
item.setClassType(String.class.getTypeName());
item.setCount(5);
items.add(item);
rule.setParamFlowItemList(items);
rules.add(rule);
ParamFlowRuleManager.loadRules(rules);
同一时刻tom、jack同时登录,tom登录两次,jack登录了10次,那么根据我们制定的规则,jack的行为将会被限制,其他用户将不会受到影响
8.png
Sentinel图形界面
如果上面的代码配置不够灵活可以通过Sentinel提供的用户界面sentinel-dashboard实时进行规则的制定,可同时管理多个客户端
- 客户端配置:在配置文件中增加下列配置,dashboard就可以轻松管理客户端了,还有一种方式是在启动时加入
spring:
cloud:
sentinel:
transport:
## VM
##-Djava.net.preferIPv4Stack=true -Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.api.port=8666 -Dproject.name=gateway -Dcsp.sentinel.app.type=1
dashboard: localhost:8880
port: 8880
下载完成可直接启动,java -jar sentinel-dashboard.jar,登录dashboard,就可以很清楚的看到众多限流、熔断等功能
9.png
同时对于刚刚建立的热点参数,还有其他特殊的限制功能,如图所示,可以限制某些特殊值不处理:
热点参数特殊规则
-
规则持久化:在dashboard中配置的规则都是存储在内存中的,dashboard 是通过 transport 模块来获取每个 Sentinel 客户端中的规则的,获取到的规则通过 RuleRepository 接口保存在 Dashboard 的内存中,如果在 Dashboard 页面中更改了某个规则,也会调用 transport 模块提供的接口将规则更新到客户端中去。
试想这样一种情况,客户端连接上 Dashboard 之后,我们在 Dashboard 上为客户端配置好了规则,并推送给了客户端。这时由于一些因素客户端出现异常,服务不可用了,当客户端恢复正常再次连接上 Dashboard 后,这时所有的规则都丢失了,我们还需要重新配置一遍规则,这肯定不是我们想要的。 -
把原本保存在 内存中的规则,持久化一份副本出去。这样下次客户端重启后,可以从持久化的副本中把数据 load 进内存中,这样就不会丢失规则了,如下图所示:
持久化原理 -
Sentinel 为我们提供了两个接口来实现规则的持久化,他们分别是:ReadableDataSource 和 WritableDataSource。因为通常各种持久化的数据源已经提供了具体的将数据持久化的方法了,我们只需要把数据从持久化的数据源中获取出来,转成我们需要的格式就可以了。
下面我们来看一下 ReadableDataSource 接口的具体的定义:
public interface ReadableDataSource<S, T> {
// 从数据源中读取原始的数据
S readSource() throws Exception;
// 将原始数据转换成我们所需的格式
T loadConfig() throws Exception;
// 获取该种数据源的SentinelProperty对象
SentinelProperty<T> getProperty();
}
接口很简单,最重要的就是这三个方法,另外 Sentinel 还为我们提供了一个抽象类:AbstractDataSource,该抽象类中实现了两个方法,具体的数据源实现类只需要实现一个 readSource 方法即可,具体的代码如下:
public abstract class AbstractDataSource<S, T>
implements ReadableDataSource<S, T> {
// Converter接口负责转换数据
protected final Converter<S, T> parser;
// SentinelProperty接口负责触发PropertyListener
// 的configUpdate方法的回调
protected final SentinelProperty<T> property;
public AbstractDataSource(Converter<S, T> parser) {
if (parser == null) {
throw new IllegalArgumentException("parser can't be null");
}
this.parser = parser;
this.property = new DynamicSentinelProperty<T>();
}
@Override
public T loadConfig() throws Exception {
return loadConfig(readSource());
}
public T loadConfig(S conf) throws Exception {
return parser.convert(conf);
}
@Override
public SentinelProperty<T> getProperty() {
return property;
}
}
-
每个具体的 DataSource 实现类需要做三件事:
- 实现 readSource 方法将数据源中的原始数据转换成我们可以处理的数据S
- 提供一个 Converter 来将数据S转换成最终的数据T
- 将最终的数据T更新到具体的 RuleManager 中去
-
DataSource 扩展常见的实现方式有:
拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。
- Sentinel 目前支持以下数据源扩展:能保证更新规则时,客户端能得到通知即可
规则的更新可以通过 Sentinel Dashboard 也可以通过各个配置中心自己的更新接口来操作
AbstractDataSource 中的 SentinelProperty 持有了一个 PropertyListener 接口,最终更新 RuleManager 中的规则是 PropertyListener 中实现的
Pull-based: 文件、Consul
Push-based: ZooKeeper, Redis, Nacos, Apollo, etcd
- 规则持久化:对这5种方式一一进行了解,以持久化限流的规则为例。
- File
文件持久化有一个问题就是文件不像其他的配置中心,数据发生变更后会发出通知,使用文件来持久化的话就需要我们自己定时去扫描文件,来确定文件是否发现了变更。
文件数据源是通过 FileRefreshableDataSource 类来实现的,他是通过文件的最后更新时间来判断规则是否发生变更的。
首先需要引入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
</dependency>
接入的方法如下:
private void init() throws Exception {
// 保存了限流规则的文件的地址
String flowRuleName = yourFlowRuleFileName();
Converter<String, List<FlowRule>> parser = source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>() {});
// 创建文件规则数据源
FileRefreshableDataSource<List<FlowRule>> flowRuleDataSource = new FileRefreshableDataSource<>(flowRuleName, parser);
// 将Property注册到 RuleManager 中去
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
-
在系统启动的时候调用该数据源注册的方法,否则不会生效的。具体的方式有很多,可以借助 Spring 来初始化该方法,也可以自定义一个类来实现 Sentinel 中的 InitFunc 接口来完成初始化。
Sentinel 会在系统启动的时候通过 spi 来扫描 InitFunc 的实现类,并执行 InitFunc 的 init 方法 -
Redis 数据源的实现类是 RedisDataSource。
首先引入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-redis</artifactId>
</dependency>
接入方法如下:
private void init() throws Exception {
Converter<String, List<FlowRule>> parser = source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>() {});
RedisConnectionConfig config = RedisConnectionConfig.builder()
.withHost(redisHost)
.withPort(redisPort)
.build();
ReadableDataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<>(config, ruleKey, channel, parser);
FlowRuleManager.register2Property(redisDataSource.getProperty());
}
- Nacos
Nacos 数据源的实现类是 NacosDataSource。
首先引入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
接入方法如下:
private void init() throws Exception {
Converter<String, List<FlowRule>> parser = source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>() {});
ReadableDataSource<String, List<FlowRule>> nacosDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser);
FlowRuleManager.register2Property(nacosDataSource.getProperty());
}
- Zk
Zk 数据源的实现类是 ZookeeperDataSource。
首先引入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-zookeeper</artifactId>
</dependency>
接入方法如下:
private void init() throws Exception {
String remoteAddress = yourRemoteAddress();
String path = yourPath();
Converter<String, List<FlowRule>> parser = source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>() {});
ReadableDataSource<String, List<FlowRule>> zookeeperDataSource = new ZookeeperDataSource<>(remoteAddress, path, parser);
FlowRuleManager.register2Property(zookeeperDataSource.getProperty());
}
- Apollo
Apollo 数据源的实现类是 ApolloDataSource。
首先引入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-apollo</artifactId>
</dependency>
接入方法如下:
private void init() throws Exception {
Converter<String, List<FlowRule>> parser = source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>() {});
ReadableDataSource<String, List<FlowRule>> apolloDataSource = new ApolloDataSource<>(namespaceName, ruleKey, path, defaultRules);
FlowRuleManager.register2Property(apolloDataSource.getProperty());
}
可以看到5种持久化的方式基本上大同小异,主要还是对接每种配置中心,实现数据的转换,并且监听配置中心的数据变化,当接收到数据变化后能够及时的将最新的规则更新到 RuleManager 中去就可以了。其他规则类似,可通过解析type进行区分。
注:https://github.com/alibaba/Sentinel/tree/master/sentinel-dashboard
网友评论