SpringCloudGateway限流原理与实践(一)

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

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

令牌桶算法

springcloud sentinel 限流 springcloud限流原理_ci


令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:

假如用户配置的平均速率为r,则每隔1/r秒一个令牌被加入到桶中;

假设桶最多可以存发b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃;

当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;

如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在流量限制之外;

算法允许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。对于在流量限制外的数据包可以以不同的方式处理:

它们可以被丢弃;

它们可以排放在队列中以便当令牌桶中累积了足够多的令牌时再传输;

它们可以继续发送,但需要做特殊标记,网络过载的时候将这些特殊标记的包丢弃。

漏桶算法

springcloud sentinel 限流 springcloud限流原理_ci_02


漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(Traffic Policing),漏桶算法的描述如下:

一个固定容量的漏桶,按照常量固定速率流出水滴;

如果桶是空的,则不需流出水滴;

可以以任意速率流入水滴到漏桶;

如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

3 实践

SpringCloudGateway限流方案

Spring Cloud Gateway 默认实现 Redis限流,如果扩展只需要实现Ratelimter接口即可,同时也可以通过自定义KeyResolver来指定限流的Key,比如我们需要根据用户、IP、URI来做限流等等,通过exchange对象可以获取到请求信息,比如:

用户限流

@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 }