转载请标明出处: www.fangzhipeng.com 本文出自方志朋的博客html
在高并发的系统中,每每须要在系统中作限流,一方面是为了防止大量的请求使服务器过载,致使服务不可用,另外一方面是为了防止网络攻击。java
常见的限流方式,好比Hystrix适用线程池隔离,超过线程池的负载,走熔断的逻辑。在通常应用服务器中,好比tomcat容器也是经过限制它的线程数来控制并发的;也有经过时间窗口的平均速度来控制流量。常见的限流纬度有好比经过Ip来限流、经过uri来限流、经过用户访问频次来限流。react
通常限流都是在网关这一层作,好比Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也能够在应用层经过Aop这种方式去作限流。git
本文详细探讨在 Spring Cloud Gateway 中如何实现限流。github
计数器算法采用计数器实现限流有点简单粗暴,通常咱们会限制一秒钟的可以经过的请求数,好比限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,若是累加的数字达到了100,那么后续的请求就会被所有拒绝。等到1s结束后,把计数恢复成0,从新开始计数。具体的实现能够是这样的:对于每次服务调用,能够经过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,经过这个最新值和阈值进行比较。这种实现方式,相信你们都知道有一个弊端:若是我在单位时间1s内的前10ms,已经经过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,咱们把这种现象称为“突刺现象”redis
漏桶算法为了消除"突刺现象",能够采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,相似生活用到的漏斗,当请求进来时,至关于水倒入漏斗,而后从下端小口慢慢匀速的流出。无论上面流量多大,下面流出的速度始终保持不变。无论服务调用方多么不稳定,经过漏桶算法进行限流,每10毫秒处理一次请求。由于处理的速度是固定的,请求进来的速度是未知的,可能忽然进来不少请求,没来得及处理的请求就先放在桶里,既然是个桶,确定是有容量上限,若是桶满了,那么新进来的请求就丢弃。算法
在算法实现方面,能够准备一个队列,用来保存请求,另外经过一个线程池(ScheduledExecutorService)来按期从队列中获取请求并执行,能够一次性获取多个并发执行。spring
这种算法,在使用事后也存在弊端:没法应对短期的突发流量。tomcat
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法可以限制请求调用的速率,而令牌桶算法可以在限制调用的平均速率的同时还容许必定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以必定的速率往桶中放令牌。每次请求调用须要先获取令牌,只有拿到令牌,才有机会继续执行,不然选择选择等待可用的令牌、或者直接拒绝。放令牌这个动做是持续不断的进行,若是桶中令牌数达到上限,就丢弃令牌,因此就存在这种状况,桶中一直有大量的可用令牌,这时进来的请求就能够直接拿到令牌执行,好比设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没彻底启动好,等启动完成对外提供服务时,该限流器能够抵挡瞬时的100个请求。因此,只有桶中没有令牌时,请求才会进行等待,最后至关于以必定的速率执行。服务器
实现思路:能够准备一个队列,用来保存令牌,另外经过一个线程池按期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
在Spring Cloud Gateway中,有Filter过滤器,所以能够在“pre”类型的Filter中自行实现上述三种过滤器。可是限流做为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在以下图所示的文件夹中:
具体源码不打算在这里讲述,读者能够自行查看,代码量较少,先以案例的形式来说解如何在Spring Cloud Gateway中使用内置的限流过滤器工厂来实现限流。
首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码以下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifatId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
复制代码
在配置文件中作如下的配置:
server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://httpbin.org:80/get
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@hostAddrKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
application:
name: gateway-limiter
redis:
host: localhost
port: 6379
database: 0
复制代码
在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器须要配置三个参数:
KeyResolver须要实现resolve方法,好比根据Hostname进行限流,则须要用hostAddress去判断。实现完KeyResolver以后,须要将这个类的Bean注册到Ioc容器中。
public class HostAddrKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
return new HostAddrKeyResolver();
}
复制代码
能够根据uri去限流,这时KeyResolver代码以下:
public class UriKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getURI().getPath());
}
}
@Bean
public UriKeyResolver uriKeyResolver() {
return new UriKeyResolver();
}
复制代码
也能够以用户的维度去限流:
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
复制代码
用jmeter进行压测,配置10thread去循环请求lcoalhost:8081,循环间隔1s。从压测的结果上看到有部分请求经过,由部分请求失败。经过redis客户端去查看redis中存在的key。以下:
可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义能够看lua源代码。
www.spring4all.com/article/138…
扫一扫,支持下做者吧
(转载本站文章请注明做者和出处 方志朋的博客)