SpringCloudGateway限流原理与实践(一)

SpringCloudGateway限流原理与实践(一)

1 概述
Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,旨在为微服务架构提供一种简单有效的统一API路由管理方式。Spring Cloud Gateway做为Spring Cloud生态系中的网关,目标是替代Netflix ZUUL,其不只提供统一的路由方式,而且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。git

2 原理
缓存、降级和限流是开发高并发系统的三把利器。缓存的目的是提高系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;降级是当服务出现问题或者影响到核心流程的性能则须要暂时屏蔽,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,好比稀缺资源、写服务、频繁的复杂查询,所以需有一种手段来限制这些场景的并发/请求量,即限流。
限流的目的是经过对并发访问/请求进行限速,或对一个时间窗口内的请求进行限速来保护系统。一旦达到限制速率则能够拒绝服务、排队或等待、降级。
通常开发高并发系统常见的限流有:限制总并发数、限制瞬时并发数、限制时间窗口内的平均速率、限制远程接口的调用速率、限制MQ的消费速率,或根据网络链接数、网络流量、CPU或内存负载等来限流。
本文主要就分布式限流方法,对Spring Cloud Gateway的限流原理进行分析。
分布式限流最关键的是要将限流服务作成原子化,常见的限流算法有:令牌桶、漏桶等,Spring Cloud Gateway使用Redis+Lua技术实现高并发和高性能的限流方案。github

令牌桶算法
这里写图片描述
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述以下:
假如用户配置的平均速率为r,则每隔1/r秒一个令牌被加入到桶中;
假设桶最多能够存发b个令牌。若是令牌到达时令牌桶已经满了,那么这个令牌会被丢弃;
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
若是令牌桶中少于n个令牌,那么不会删除令牌,而且认为这个数据包在流量限制以外;
算法容许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。对于在流量限制外的数据包能够以不一样的方式处理:
它们能够被丢弃;
它们能够排放在队列中以便当令牌桶中累积了足够多的令牌时再传输;
它们能够继续发送,但须要作特殊标记,网络过载的时候将这些特殊标记的包丢弃。
漏桶算法
这里写图片描述
漏桶做为计量工具(The Leaky Bucket Algorithm as a Meter)时,能够用于流量整形(Traffic Shaping)和流量控制(Traffic Policing),漏桶算法的描述以下:
一个固定容量的漏桶,按照常量固定速率流出水滴;
若是桶是空的,则不需流出水滴;
能够以任意速率流入水滴到漏桶;
若是流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
3 实践
SpringCloudGateway限流方案web

Spring Cloud Gateway 默认实现 Redis限流,若是扩展只须要实现Ratelimter接口便可,同时也能够经过自定义KeyResolver来指定限流的Key,好比咱们须要根据用户、IP、URI来作限流等等,经过exchange对象能够获取到请求信息,好比:redis

用户限流
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}算法

SpringCloudGateway默认提供的RedisRateLimter 的核心逻辑为判断是否取到令牌的实现,经过调用 META-INF/scripts/request_rate_limiter.lua 脚本实现基于令牌桶算法限流,代码以下 :
1: local tokens_key = KEYS1
2: local timestamp_key = KEYS2
3:
4: local rate = tonumber(ARGV1)
5: local capacity = tonumber(ARGV2)
6: local now = tonumber(ARGV3)
7: local requested = tonumber(ARGV4)
8:
9: local fill_time = capacity/rate
10: local ttl = math.floor(fill_time*2)
11:
12: local last_tokens = tonumber(redis.call(“get”, tokens_key))
13: if last_tokens == nil then
14: last_tokens = capacity
15: end
16:
17: local last_refreshed = tonumber(redis.call(“get”, timestamp_key))
18: if last_refreshed == nil then
19: last_refreshed = 0
20: end
21:
22: local delta = math.max(0, now-last_refreshed)
23: local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
24: local allowed = filled_tokens >= requested
25: local new_tokens = filled_tokens
26: local allowed_num = 0
27: if allowed then
28: new_tokens = filled_tokens - requested
29: allowed_num = 1
30: end
31:
32: redis.call(“setex”, tokens_key, ttl, new_tokens)
33: redis.call(“setex”, timestamp_key, ttl, now)
34:
35: return { allowed_num, new_tokens }spring