SpringCloudGateway集成Sentinel

SpringCloudGateway集成Sentinel

介绍

Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关做为流量的,在微服务系统中有着很是做用,网关常见的功能有路由转发、权限校验、限流控制等做用。
Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。(https://github.com/alibaba/Sentinel)
  • 总体结构图以下,将原有的 Spring Cloud Gateway中集成Hystrix替换成Sentinel来进行限流、降级等功能, Hystrix和Sentinel的区别能够参考:Hystrix和Sentinel对比,总结来讲:Hystrix经常使用的线程池隔离会形成线程上下切换的overhead比较大;Hystrix使用的信号量隔离对某个资源调用的并发数进行控制,效果不错,可是没法对慢调用进行自动降级;Sentinel经过并发线程数的流量控制提供信号量隔离的功能
    总体结构图

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将不会生效java

  • 经过代码进行配置,将路由规则设置为一个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时请求直接被转发到百度的首页了。
    生效的路径

基础配置:

  • 如今的请求经过通过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整合,加入以下配置git

<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中提取生成的热点参数规则,根据传入的参数依次进行规则检查。若某条规则不针对请求属性,则会在参数最后一个位置置入预设的常量,达到普通流控的效果。

原理

  • 过滤顺序:当请求到来时,会依次调用下面的规则进行校验,而chain继承自AbstractLinkedProcessorSlot,将过滤的规则依次进行直到不经过或者到最后一个规则。
    过滤顺序
##对应代码中定义的顺序
 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 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;github

    • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用做为多维度限流,降级的依据;web

    • LogSlot:用来记录系统日志,当前经过数量、拒绝数量等正则表达式

    • StatistcSlot 则用于记录,统计不一样纬度的 runtime 信息;redis

    • AuthorizationSlot 则根据黑白名单,来作黑白名单控制;spring

    • SystemSlot 则经过系统的状态,当前的运行状态、CPU占用率等,来控制总的入口流量;json

    • GatewayFlowSlot::网关限流规则api

    • 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: 从请求中提取参数的策略,目前支持提取来源
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=/**
路由转发正常

路由转发

  • 自定义路由拦截规则:在上图中咱们定义并请求了三个路径,其中前面两个都是对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的请求将不会受到影响。

限流

  • 自定义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/****形式的请求都适用上面的规则,这里咱们使用线程池模拟并发请求,请求第四次时将直接失败,限流效果仍是很明显的。
    限流

  • 热点参数规则(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的行为将会被限制,其余用户将不会受到影响
热点参数限流

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,就能够很清楚的看到众多限流、熔断等功能
dshboard

同时对于刚刚创建的热点参数,还有其余特殊的限制功能,如图所示,能够限制某些特殊值不处理:
热点参数特殊规则

  • 规则持久化:在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