SpringCloud gateway 제한 소스 코드 분석

9996 단어 자바
경로 필 터 는 들 어 오 는 HTTP 요청 이나 들 어 오 는 HTTP 응답 을 어떤 방식 으로 수정 할 수 있 습 니 다. 경로 필터 의 범 위 는 특정 경로 로 제한 되 어 있 습 니 다. Spring Cloud Gateway 는 내 장 된 Gateway Filter 공장 을 많이 포함 하고 있 습 니 다.
Spring Cloud Gateway 제한 흐름 은 내 장 된 RequestRate Limiter GateWay FilterFactory 공장 을 통 해 이 루어 진다.
물론 공식 적 으로 는 우리 의 일부 업무 수 요 를 만족 시 킬 수 없 기 때문에 스 트림 필 터 를 사용자 정의 할 수 있 습 니 다.
\ # \ # yml 은 다음 과 같이 설정 하면 이 경로 에 차단 기 를 추가 할 수 있 습 니 다.
spring:
   cloud:
      gateway:
        routes:
          - id: test_route
            uri: localhost
            predicates:
            - Path=/host/address
            filters:
            - name: RequestRateLimiter
              args:
                ##             ,         。          。
                redis-rate-limiter.replenishRate: 1
                ##                  。             。              。
                redis-rate-limiter.burstCapacity: 3
                ## KeyResolver                        key    
                key-resolver: "#{@hostAddrKeyResolver}"

 
 
\ # \ # RequestRateLimiterGateWayFilterFactory 코드:
//AbstractGatewayFilterFactory  GatewayFilterFactory  ,            
//AbstractGatewayFilterFactory   apply  
public class RequestRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory {

	public static final String KEY_RESOLVER_KEY = "keyResolver";

	private final RateLimiter defaultRateLimiter;
	private final KeyResolver defaultKeyResolver;

	public RequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
												  KeyResolver defaultKeyResolver) {
		super(Config.class);
		this.defaultRateLimiter = defaultRateLimiter;
		this.defaultKeyResolver = defaultKeyResolver;
	}

	public KeyResolver getDefaultKeyResolver() {
		return defaultKeyResolver;
	}

	public RateLimiter getDefaultRateLimiter() {
		return defaultRateLimiter;
	}

	@SuppressWarnings("unchecked")
	@Override
	public GatewayFilter apply(Config config) {
                //yml      hostAddrKeyResolver
		KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
                //           ,    RedisRateLimiter
		RateLimiter limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;

		return (exchange, chain) -> {
			Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);

			return resolver.resolve(exchange).flatMap(key ->
                                    //   isAllowed      ,       id   key(       hostAddress)
					// TODO: if key is empty?
					limiter.isAllowed(route.getId(), key).flatMap(response -> {

						for (Map.Entry header : response.getHeaders().entrySet()) {
							exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
						}
                                        //    ,    
						if (response.isAllowed()) {
							return chain.filter(exchange);
						}
                                        //    http  429,too many request
						exchange.getResponse().setStatusCode(config.getStatusCode());
						return exchange.getResponse().setComplete();
					}));
		};
	}


}

분석:
1. KeyResolver 를 불 러 오고 설정 파일 에서 불 러 옵 니 다. hostAddrKeyResolver 를 설 정 했 습 니 다. 즉, host 주소 에 따라 흐름 을 제한 합 니 다.비어 있 으 면 기본 PrincipalNameKeyResolver 사용 하기
2. RateLimiter 를 불 러 옵 니 다. 기본적으로 RedisRateLimiter 를 사용 합 니 다.
3. Redis Rate Limiter 의 isAllowed 방법 을 실행 하여 response 를 얻 습 니 다. isAllowed 가 true 이면 차단 을 통 해 429 (isAllowed 방법 은 다음 설명 을 구체 적 으로 실현 합 니 다) 로 돌아 갑 니 다.
 
## HostAddrKeyResolver:
@Slf4j
public class HostAddrKeyResolver implements KeyResolver {
    @Override
    public Mono resolve(ServerWebExchange exchange) {
        log.info("HostAddrKeyResolver   ");
        return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}

시작 클래스 에 bean 주입
@Bean
    public HostAddrKeyResolver hostAddrKeyResolver() {
        return new HostAddrKeyResolver();
    }

 
## RedisRateLimiter:
        @Override
	@SuppressWarnings("unchecked")
	public Mono 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);
		}

                //       
		int replenishRate = routeConfig.getReplenishRate();

                //         
		int burstCapacity = routeConfig.getBurstCapacity();

		try {
                    //  redis key,  lua     
			List keys = getKeys(id);


                    //    ,  lua     
			List scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
					Instant.now().getEpochSecond() + "", "1");
			Flux> 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(), (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) {
			/*
			 * We don't want a hard dependency on Redis to allow traffic. Make sure to set
			 * an alert so you know if this is happening too much. Stripe's observed
			 * failure rate is 0.01%.
			 */
			log.error("Error determining if user allowed from redis", e);
		}
		return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
	}

	@NotNull
	public HashMap getHeaders(Config config, Long tokensLeft) {
		HashMap headers = new HashMap<>();
		headers.put(this.remainingHeader, tokensLeft.toString());
		headers.put(this.replenishRateHeader, String.valueOf(config.getReplenishRate()));
		headers.put(this.burstCapacityHeader, String.valueOf(config.getBurstCapacity()));
		return headers;
	}

	static List getKeys(String id) {
		// use `{}` around keys to use Redis Key hash tags
		// this allows for using redis cluster

		// Make a unique key per user.
		String prefix = "request_rate_limiter.{" + id;
                //        
		String tokenKey = prefix + "}.tokens";
                //           
		String timestampKey = prefix + "}.timestamp";
		return Arrays.asList(tokenKey, timestampKey);
	}

분석:
1. 초기 화 여 부 를 판단 하고 설정 을 불 러 오 며 토 큰 충전 속도 와 토 큰 통 크기 를 가 져 옵 니 다.
2. 경로 id 에 따라 두 개의 redis 의 key 값 으로 조합 하여 lua 스 크 립 트 에 전송 합 니 다.
request_rate_limiter.{id}.tokens   영패 통 남 은 영패 수
request_rate_limiter.{id}.timestamp  토 큰 통 마지막 토 큰 시간 채 우기
3. 토 큰 충전 속도, 토 큰 통 크기, 현재 시간 (단위: 초), 토 큰 소모 (기본 값 1) 조합 을 lua 스 크 립 트 에 전송
4. lua 스 크 립 트 실행
5.flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))  이것 은 lua 스 크 립 트 를 실행 하 는 과정 에서 이상 한 처리 입 니 다. 이상 을 무시 하고 토 큰 을 되 돌려 줍 니 다.이렇게 하면 redis 와 결합 을 풀 수 있 고 강하 게 의존 하지 않 을 수 있 습 니 다.
이 실현 핵심 은 주로 lua 스 크 립 트 에 나타 나 는데 영패 통 알고리즘 을 사용 합 니 다.
spring - cloud - gateway - core 의 request 참조rate_limiter.lua
##         redis key
local tokens_key = KEYS[1]
##              
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)

##       
local rate = tonumber(ARGV[1])
##      
local capacity = tonumber(ARGV[2])
##     
local now = tonumber(ARGV[3])
##      ,  1
local requested = tonumber(ARGV[4])

##             
local fill_time = capacity/rate
##   key     
local ttl = math.floor(fill_time*2)

--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)

##         
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)

##           
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)

local delta = math.max(0, now-last_refreshed)
##           
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
##          allowed   true
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)

redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)

return { allowed_num, new_tokens }

문제 가 있 으 면 지적 을 환영 합 니 다.
 
저자: 커 다란 열매 링크:https://blog.csdn.net/shuoshuo132 출처: CSDN 저작권 은 작가 의 소유 이 며, 어떠한 형식의 전재 도 출처 를 밝 혀 주 십시오.

좋은 웹페이지 즐겨찾기