做限流 (Rate Limiting/Throttling) 的时候,除了简单的控制并发,如果要准确的控制 TPS,简单的做法是维护一个单位时间内的 Counter,如判断单位时间已经过去,则将 Counter 重置零。此做法被认为没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,也就是在两毫秒内发生了两倍的 TPS。
常用的更平滑的限流算法有两种:漏桶算法和令牌桶算法。很多传统的服务提供商如华为中兴都有类似的专利,参考采用令牌漏桶进行报文限流的方法。
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。
可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。
令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。Guava 中的 RateLimiter 采用了令牌桶的算法,设计思路参见 How is the RateLimiter designed, and why?,详细的算法实现参见源码。
<!-- commons-pool2对象池依赖--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
<!--RequestRateLimiter限流--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
spring: redis: host: 106.14.24.156 port: 6380 password: dm_webapi database: 0 timeout: 20s # 连接超时时间(毫秒)默认是2000ms lettuce: pool: max-active: 1024 # 连接池最大连接数(使用负值表示没有限制) max-wait: -1s # 连接池最大阻塞等待时间(使用负值表示没有限制) max-idle: 200 # 连接池中的最大空闲连接 min-idle: 5 # 连接池中的最小空闲连接
@Configuration public class KeyResolverConfig { @Bean @Primary KeyResolver apiKeyResolver() { //按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态 return exchange -> Mono.just(exchange.getRequest().getPath().toString()); } @Bean KeyResolver userKeyResolver() { //按用户限流 return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); } @Bean KeyResolver ipKeyResolver() { //按IP来限流 return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } }
spring: gateway: routes: - id: oneserver-service uri: lb://oneserver-service #lb代表负载均衡 member为注册中心上的服务名 predicates: - Path=/oneserver/** filters: #截取调oneserver - StripPrefix=1 # 限流过滤器,使用gateway内置令牌算法 - name: RequestRateLimiter args: # 令牌桶每秒填充平均速率,即行等价于允许用户每秒处理多少个请求平均数 redis-rate-limiter.replenishRate: 1 # 令牌桶的容量,允许在一秒钟内完成的最大请求数 redis-rate-limiter.burstCapacity: 2 # 用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。 key-resolver: "#{@apiKeyResolver}" discovery: locator: enabled: true lower-case-service-id: true
多次请求 http://localhost:7001/oneserver/getLcoUsers 结果如下:
Redis结果如下: