对一些客流访问量比较大,存在高并发情况的系统,就需要在系统中进行限流,一方面是为了防止大量的请求致使服务器宕机,导致服务不可用,另一方面是为了防止网络攻击。
一般的应用服务器,都是通过限制线程池的线程数来控制并发,也有通过单位时间内窗口的平均速度来控制流量。常见的限流纬度有比如通过Ip来限流、通过uri来限流、通过用户访问频次来限流。
常见的限流算法:
引用一位博主所介绍的,个人觉得说的很容易理解与透彻,原文地址:https://blog.csdn.net/forezp/article/details/85081162
计数器算法:
计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”。
漏桶算法:
漏桶算法为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。
这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。
令牌桶算法:
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
Spring Cloud Gateway 实现限流
在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器,因此Spring Cloud Gateway可以使用内置的限流过滤器工厂来实现限流。
首先看下项目结构:
一个服务注册中心eurekaserver、一个服务实例gateway-service-hi、一个网关实现限流gateway-limiter
接下来详细介绍gateway-limiter的配置
在pom.xml文件中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring-boot-starter-data-redis-reactive:使用redis实现限流
application.yml配置如下-
server:
port: 8887
spring:
application:
name: gateway-limiter
cloud:
gateway:
routes:
- id: gateway-service-hi
uri: lb://gateway-service-hi
predicates:
- Path=/gateway/**
filters:
- name: RequestRateLimiter
args:
#用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象
key-resolver: "#{@hostAddKeyResolver}"
#令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 2
#令牌桶总容量
redis-rate-limiter.burstCapacity: 4
- StripPrefix=1
discovery:
locator:
enabled: true
# 服务名可以小写
lowerCaseServiceId: true
redis:
host: localhost
port: 6379
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
在上面的配置文件,指定程序的端口为8887,也配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
burstCapacity,令牌桶总容量。
replenishRate,令牌桶每秒填充平均速率,也可以理解为每秒允许的请求访问次数
key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
Bean 对象,这里是hostAddKeyResolver,根据IP进行限流,要实现抽线类KeyResolver,重写esolver方法
package com.sinosoft.gatewaylimiter.resolver;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class HostAddKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
也可以根据uri去限流,代码如下:
public class UriKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getURI().getPath());
}
}
同样可以以用户的维度去限流,代码如下:
public class UserKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
}
启始类中配置如下:
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import reactor.core.publisher.Mono;
@SpringBootApplication
public class GatewayLimiterApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayLimiterApplication.class, args);
}
@Bean
public KeyResolver hostAddKeyResolver(){
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
需要在启始类启动的时候将这些KeyResolver放到spring容器中
运行项目
首先先运行redis:在redis的安装目录路径下打开命令窗口,执行redis-server.exe redis.windows.conf,这样redis就启动了,这个窗口不要关闭!
接下来依次运行:eurekaserver、gateway-service-hi、gateway-limiter
这时在redis的安装目录路径下重新打开一个命令窗口,执行redis-cli.exe -h 127.0.0.1 -p 6379
接着执行monitor实时查看redis状态
最后访问:http://localhost:8887/gateway/hi
可见,RequestRateLimiter是使用Redis来进行限流的。