SpringCloudGateway中ratelimiter源碼分析

前言

在SpringCloudGateway中官方默認提供了基於Redis的分佈式限流方案,對於大部分的場景開箱即用。但實際應用場景下,針對不同的業務場景可能需要進行定製化擴展,此時很有必要了解其工作原理,從而更加快速有效的實現自定義擴展。

正文

此部分將通過3個層面逐步展開:

  • Redis分佈式限流的核心組件;
  • 如何配置路由;
  • 如何處理請求;
  • 如何刷新路由配置;

Redis分佈式限流的核心組件

既然是Gateway模塊的源碼分析,根據springboot源碼分析的套路,從GatewayAutoConfiguration類着手逐步展開,在GatewayAutoConfiguration類中能夠找到如下bean實例的註冊

@Bean(name = PrincipalNameKeyResolver.BEAN_NAME)
@ConditionalOnBean(RateLimiter.class)
public PrincipalNameKeyResolver principalNameKeyResolver() {
   return new PrincipalNameKeyResolver();
}

@Bean
@ConditionalOnBean({RateLimiter.class, KeyResolver.class})
public RequestRateLimiterGatewayFilterFactory requestRateLimiterGatewayFilterFactory(RateLimiter rateLimiter, PrincipalNameKeyResolver resolver) {
   return new RequestRateLimiterGatewayFilterFactory(rateLimiter, resolver);
}

其中

  • PrincipalNameKeyResolver 將作爲默認的 KeyResolver 實現,其作用於redis存儲的限流鍵key定義;
    - RequestRateLimiterGatewayFilterFactory 請求限流網關過濾器工廠類,其會默認注入已經定義的 RateLimiter 實例和 PrincipalNameKeyResolver 實例,此處說明 PrincipalNameKeyResolver 作爲了默認的 KeyResolver 實現。

不難發現兩個bean實例的註冊均依賴於 RateLimiter 實例,該接口定義了判斷是否能夠放行的isAllowed方法,如下:

public interface RateLimiter<C> extends StatefulConfigurable<C> {
   Mono<Response> isAllowed(String routeId, String id);
   .....
}

在默認配置中,可以在 GatewayRedisAutoConfiguration類中找到如下其Bean實例的默認裝配,目前SpringCloudGateway分佈式限流官方提供的正是基於redis的實現,如下

@Bean
@ConditionalOnMissingBean
public RedisRateLimiter redisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate,
                               @Qualifier(RedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> redisScript,
                               Validator validator) {
   return new RedisRateLimiter(redisTemplate, redisScript, validator);
}

RedisRateLimiter 實例通過 @ConditionalOnMissingBean實現了條件注入,並不會被強制注入,其提供了自定義擴展的可能性。當前Bean實例依賴注入的 RedisScript實例,其指定了具體執行的lua腳本路徑,

@Bean
@SuppressWarnings("unchecked")
public RedisScript redisRequestRateLimiterScript() {
   DefaultRedisScript redisScript = new DefaultRedisScript<>();
   redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/request_rate_limiter.lua")));
   redisScript.setResultType(List.class);
   return redisScript;
}

該腳本已經在對應的jar包中可以直接查看,其默認採用的是令牌桶算法。需要注意的是該bean實例並不是條件註冊的,而是默認強制註冊。此時如果我們需要對腳本進行簡單的調整,可以添加一個新的 RedisScript 實例,同時重新註冊 RedisRateLimiter 實例,並重新指定其依賴注入的RedisScript實例爲定義的新實例即可。

小節
到這裏基本已經清楚SpringCloudGateway基於Redis實現的分佈式限流的核心組件以及對應的實現:

  • RequestRateLimiterGatewayFilterFactory;
  • KeyResolver:PrincipalNameKeyResolver;
  • RateLimiter:RedisRateLimiter;
  • RedisScript :META-INF/scripts/request_rate_limiter.lua。

如何配置路由

Gateway中的限流目前是針對每個路由單獨定義的,在瞭解如何針對每個路由定製化限流參數之前,需要先了解Gateway中是如何配置路由定位器的,從一個簡單的application.yaml配置角度入手,其定義如下:

spring:
  cloud:
    gateway:
      routes:
        - id: consumer-service
          uri: http://127.0.0.1:8081
          predicates:
            - Path=/consumer-service/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@userKeyResolver}"
                redis-rate-limiter.replenishRate: 5
                redis-rate-limiter.burstCapacity: 10
            - RewritePath=/consumer-service/(?<segment>.*), /$\{segment}

其中明確指定將採用限流過濾器 RequestRateLimiter並配置了3個主要參數。
此時再次把焦點放在 GatewayAutoConfiguration類,根據spring.cloud.gateway前綴設定,上述 application.yaml中的配置項將綁定到 GatewayProperties實例中,

@Bean
public GatewayProperties gatewayProperties() {
   return new GatewayProperties();
}

根據 GatewayProperties中的路由配置信息,將生成基於properties的路由定義定位器 PropertiesRouteDefinitionLocator

@Bean
@ConditionalOnMissingBean
public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(GatewayProperties properties) {
   return new PropertiesRouteDefinitionLocator(properties);
}

默認情況下,系統還會注入一個基於內存的路由定義實例,如下 InMemoryRouteDefinitionRepository

@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
   return new InMemoryRouteDefinitionRepository();
}

在實際開發中可以定義多個路由定義定位器(此部分也是一個常規的擴展點,比如通過DB獲取路由定義等),並通過 CompositeRouteDefinitionLocator將所有的路由定義定位器信息進行組合合併,

@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(List<RouteDefinitionLocator> routeDefinitionLocators) {
   return new CompositeRouteDefinitionLocator(Flux.fromIterable(routeDefinitionLocators));
}

在Debug模式下可以看到 routeDefinitionLocators包含了上述兩個路由定義實例,如下
在這裏插入圖片描述
基於路由配置定義即可實例化路由定位器,如下實例化RouteLocator的實現RouteDefinitionRouteLocatorr

@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,
                                    List<GatewayFilterFactory> GatewayFilters,
                                    List<RoutePredicateFactory> predicates,
                                    RouteDefinitionLocator routeDefinitionLocator) {
   return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, GatewayFilters, properties);
}

其中將注入RouteDefinitionLocatorr實例以及GatewayPropertiesr實例,RouteDefinitionRouteLocatorr的構造函數如下:

public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,
                           List<RoutePredicateFactory> predicates,
                           List<GatewayFilterFactory> gatewayFilterFactories,
                           GatewayProperties gatewayProperties) {
   this.routeDefinitionLocator = routeDefinitionLocator;
   initFactories(predicates);
   gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
   this.gatewayProperties = gatewayProperties;
}

目前來看構造函數中並沒有對routeDefinitionLocator gatewayProperties 進行過多的處理,其作用將會在下一小節分析中體現,
下一步會實例化CachingRouteLocator作爲默認的RouteLocator實例,其會合並所有之前定義的RouteLocator實例,默認情況下僅有RouteDefinitionRouteLocator一個實現:

@Bean
@Primary
//TODO: property to disable composite?
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
   return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}

小節
如上在實例化路由定義相關bean實例時,僅有CachingRouteLocator(cachedCompositeRouteLocator)CompositeRouteDefinitionLocator(routeDefinitionLocator)@Primary註解,故在後續的實際使用中注入的路由定義定位器和路由定位器即爲CachingRouteLocatorCompositeRouteDefinitionLocator實例。

如何處理請求

默認情況下,當Gateway接收到轉發請求時,會被RoutePredicateHandlerMapping類接收處理,其中注入了RouteLocator對應的CachingRouteLocator實例,根據之前的分析,目前CachingRouteLocator實例中僅僅包含了一個RouteDefinitionRouteLocator實例,故其會執行RouteDefinitionRouteLocator下的getRoutes方法:

@Override
public Flux<Route> getRoutes() {
   return this.routeDefinitionLocator.getRouteDefinitions()
         .map(this::convertToRoute)
         //TODO: error handling
         .map(route -> {
            if (logger.isDebugEnabled()) {
               logger.debug("RouteDefinition matched: " + route.getId());
            }
            return route;
         });
}

此處的routeDefinitionLocator即爲上述的CompositeRouteDefinitionLocator實例獲取所有的路由定義,通過convertToRoute方法轉換爲實際路由對象,

private Route convertToRoute(RouteDefinition routeDefinition) {
   AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
   List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);

   return Route.async(routeDefinition)
         .asyncPredicate(predicate)
         .replaceFilters(gatewayFilters)
         .build();
}

此處有兩個核心方法combinePredicatesgetFilters方法,此處我們重點關注getFilters方法的定義,

private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {
   List<GatewayFilter> filters = new ArrayList<>();

   //TODO: support option to apply defaults after route specific filters?
   if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {
      filters.addAll(loadGatewayFilters("defaultFilters",
            this.gatewayProperties.getDefaultFilters()));
   }

   if (!routeDefinition.getFilters().isEmpty()) {
      filters.addAll(loadGatewayFilters(routeDefinition.getId(), routeDefinition.getFilters()));
   }

   AnnotationAwareOrderComparator.sort(filters);
   return filters;
}

如上代碼所示,getFilters方法調用loadGatewayFilters方法從gatewayPropertiesrouteDefinition中採集所有的filter配置(如上application.yaml示例,定義了2個filter),來觀察loadGatewayFilters的定義

private List<GatewayFilter> loadGatewayFilters(String id, List<FilterDefinition> filterDefinitions) {
   List<GatewayFilter> filters = filterDefinitions.stream()
         .map(definition -> {
            // 對應了yaml中的name定義,通過name即可獲取對應的GatewayFilterFactory,gatewayFilterFactories中存儲了所有實例化的GatewayFilterFactory實例
            GatewayFilterFactory factory = this.gatewayFilterFactories.get(definition.getName());
            if (factory == null) {
                       throw new IllegalArgumentException("Unable to find GatewayFilterFactory with name " + definition.getName());
            }
            Map<String, String> args = definition.getArgs();
            if (logger.isDebugEnabled()) {
               logger.debug("RouteDefinition " + id + " applying filter " + args + " to " + definition.getName());
            }
            
            //根據定義的args參數轉換爲鍵值對,如果是#{***}格式的value則會轉換爲對應的Bean實例
           Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);
           // 對應GatewayFilterFactory中定義的Config類的默認值
           Object configuration = factory.newConfig();
            // 綁定屬性到GatewayFilterFactory中定義的Config類
           ConfigurationUtils.bind(configuration, properties,
                   factory.shortcutFieldPrefix(), definition.getName(), validator);
            //配置GatewayFilterFactory
           GatewayFilter gatewayFilter = factory.apply(configuration);
           // 發佈FilterArgsEvent事件,通知監聽者綁定properties參數,id爲當前route的id屬性
           if (this.publisher != null) {
               this.publisher.publishEvent(new FilterArgsEvent(this, id, properties));
           }
           return gatewayFilter;
         })
         .collect(Collectors.toList());

   ArrayList<GatewayFilter> ordered = new ArrayList<>(filters.size());
   for (int i = 0; i < filters.size(); i++) {
      GatewayFilter gatewayFilter = filters.get(i);
      if (gatewayFilter instanceof Ordered) {
         ordered.add(gatewayFilter);
      }
      else {
         ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));
      }
   }

   return ordered;
}
  • 通過name屬性即可找到對應的GatewayFilterFactory,此處我們主要關注RequestRateLimiterGatewayFilterFactory
  • 通過 Map<String, String> args = definition.getArgs();即可獲取對應的參數,

如下圖可以看到在application.yaml中定義的3個參數,
在這裏插入圖片描述
args又是如何被綁定到配置實例的呢?所有的GatewayFilterFactory均實現了ShortcutConfigurable接口,ShortcutConfigurable中定義瞭解析上述參數的方法,

String key = normalizeKey(entry.getKey(), entryIdx, shortcutConf, args);
Object value = getValue(parser, beanFactory, entry.getValue());

此部分爲核心實現,在getValue方法中可以看到對以#{開頭和}結果的value值將通過beanFactory獲取對應的bean實例

if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) {
   // assume it's spel
   StandardEvaluationContext context = new StandardEvaluationContext();
   context.setBeanResolver(new BeanFactoryResolver(beanFactory));
   Expression expression = parser.parseExpression(entryValue, new TemplateParserContext());
   value = expression.getValue(context);
}

此處非常關鍵,此方式提供了在application.yaml通過變量定義即可決定具體採用哪個Bean實例的能力,如上在實際開發應用中將通過userKeyResolver替換默認註冊的principalNameKeyResolver作爲KeyResolver實例。
藉助ConfigurationUtils類中提供的bind方法將對應的屬性綁定到RequestRateLimiterGatewayFilterFactory.Config類,

new Binder(new MapConfigurationPropertySource(properties))
              .bind(configurationPropertyName, Bindable.ofInstance(toBind));

根據application.yaml中的定義,此處會調用setKeyResolver綁定自定義的KeyResolver鍵定義bean實例(此處除了keyResolverrateLimiter同樣提供了類似的自定義配置能力)

public static class Config {
   private KeyResolver keyResolver;
   private RateLimiter rateLimiter;
   private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;
.....
   public Config setKeyResolver(KeyResolver keyResolver) {
      this.keyResolver = keyResolver;
      return this;
   }
.....
}

通過GatewayFilter gatewayFilter = factory.apply(configuration);將調用RequestRateLimiterGatewayFilterFactory中的apply方法:

public GatewayFilter apply(Config config) {
   KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
   RateLimiter<Object> limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;

   return (exchange, chain) -> {....
   };
}

其中可以看到未來實際應用的KeyResolver RateLimiter取值邏輯,其會優先從Config中提取,如果沒有任何自定義則直接採用默認值,默認值的設定已經在本章開頭介紹過。

不難發現,我們自定義的3個參數僅僅有keyResolver被成功賦值,那麼剩下的兩個參數呢,又是如何配置綁定?繼續往下看

this.publisher.publishEvent(new FilterArgsEvent(this, id, properties));

此處發佈了FilterArgsEvent事件,其中包含了所有的轉換後的所有args配置,如下觀察AbstractRateLimiter類,其實現了ApplicationListener接口,並監聽FilterArgsEvent事件,

public abstract class AbstractRateLimiter<C> extends AbstractStatefulConfigurable<C> implements RateLimiter<C>, ApplicationListener<FilterArgsEvent> {
  .....
   @Override
   public void onApplicationEvent(FilterArgsEvent event) {
      Map<String, Object> args = event.getArgs();

      if (args.isEmpty() || !hasRelevantKey(args)) {
         return;
      }

      String routeId = event.getRouteId();
      C routeConfig = newConfig();
      ConfigurationUtils.bind(routeConfig, args,
            configurationPropertyName, configurationPropertyName, validator);
      getConfig().put(routeId, routeConfig);
   }
..
}

AbstractRateLimiter類是抽象類,此處真正使用的是RedisRateLimiter類,其除了最核心的isAllowed方法,還有如下參數配置定義

@ConfigurationProperties("spring.cloud.gateway.redis-rate-limiter")
public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config> implements ApplicationContextAware {
    @Validated
   public static class Config {
      @Min(1)
      private int replenishRate;

      @Min(1)
      private int burstCapacity = 1;
      ......
   }
}

根據spring.cloud.gateway.redis-rate-limiter爲前綴,replenishRateburstCapacity值綁定過程定義在AbstractRateLimiter抽象類中

public void onApplicationEvent(FilterArgsEvent event) {
   Map<String, Object> args = event.getArgs();

   if (args.isEmpty() || !hasRelevantKey(args)) {
      return;
   }

   String routeId = event.getRouteId();
   C routeConfig = newConfig();
   ConfigurationUtils.bind(routeConfig, args,
         configurationPropertyName, configurationPropertyName, validator);
   getConfig().put(routeId, routeConfig);
}

綁定方式仍然是採用的ConfigurationUtils工具類,最後一行將routeId作爲了鍵,routeConfig作爲value值存儲在Map中,故後續在isAllowed方法中將直接根據routeId取出當前routeConfig配置,同時也避免了每次請求均需要加載路由參數的配置(同理,CachingRouteLocator中也定義了對應的Map來緩存路由信息),僅有首次請求需要加載。最後來看看isAllowed方法定義:

public Mono<Response> isAllowed(String routeId, String id) {
   if (!this.initialized.get()) {
      throw new IllegalStateException("RedisRateLimiter is not initialized");
   }

   Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);

   if (routeConfig == null) {
      throw new IllegalArgumentException("No Configuration found for route " + routeId);
   }

   // How many requests per second do you want a user to be allowed to do?
   int replenishRate = routeConfig.getReplenishRate();

   // How much bursting do you want to allow?
   int burstCapacity = routeConfig.getBurstCapacity();

   try {
      List<String> keys = getKeys(id);


      // The arguments to the LUA script. time() returns unixtime in seconds.
      List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
            Instant.now().getEpochSecond() + "", "1");
      // allowed, tokens_left = redis.eval(SCRIPT, keys, args)
      Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
            // .log("redisratelimiter", Level.FINER);
      return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
            .reduce(new ArrayList<Long>(), (longs, l) -> {
               longs.addAll(l);
               return longs;
            }) .map(results -> {
               boolean allowed = results.get(0) == 1L;
               Long tokensLeft = results.get(1);

               Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));

               if (log.isDebugEnabled()) {
                  log.debug("response: " + response);
               }
               return response;
            });
   }
   catch (Exception e) 
      log.error("Error determining if user allowed from redis", e);
   }
   return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
}

其中自定義參數通過routeId即可從上一個步驟的getConfig()中提取,最終通過執行lua腳本來判斷是否能夠放行。

小節
通過對請求的處理過程解析,可以看到其實際是分析了自定義參數如何被綁定到對應的配置實例。此處雖然僅僅是分析了RequestRateLimiterGatewayFilterFactory的相關參數綁定原理,但在SpringCloudGateway中所有的過濾器均遵循一樣的執行流程以及數據綁定模式。

如何刷新路由配置

CachingRouteLocator中可以看到如下代碼段

@EventListener(RefreshRoutesEvent.class)
/* for testing */ void handleRefresh() {
   refresh();
}

其監聽RefreshRoutesEvent事件,然後執行路由器配置緩存的刷新操作。該事件的發佈可以通過GatewayControllerEndpoint提供的refresh來完成

@PostMapping("/refresh")
public Mono<Void> refresh() {
    this.publisher.publishEvent(new RefreshRoutesEvent(this));
   return Mono.empty();
}

同理在CachingRouteDefinitionLocator中也會同步監聽該事件。此處需要特別注意,該端點依賴於spring-boot-starter-actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

同時需要在配置文件中暴露gateway端點信息

management:
  endpoint:
    gateway:
      enabled: true
  endpoints:
    web:
      exposure:
        include: ["health","info","gateway"]

更多可以參考官方文檔

總結

通過本章的4部分介紹,無論是對rateLimiter過濾器進行定製化,亦或是對其他的過濾器定製化,甚至是添加完全自定義的過濾器均會有指導性的作用。其主體的執行流程與配置模式基本是固定的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章