SpringCloud微服务网关做边缘服务限流方案
优质文章,第一时间送达
在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。
常见的限流方式,比如Hystrix适用线程池隔离,超过线程池的负载,走熔断的逻辑。在一般应用服务器中,比如tomcat容器也是通过限制它的线程数来控制并发的;也有通过时间窗口的平均速度来控制流量。常见的限流纬度有比如通过Ip来限流、通过uri来限流、通过用户访问频次来限流。
一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也可以在应用层通过Aop这种方式去做限流。这里主要讲讲常用的限流方式。
一、常见的限流算法
目前常用的限流算法有两个:漏桶算法和令牌桶算法。
1·计数器算法
计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”
2.漏桶算法
漏桶算法的原理比较简单,请求进入到漏桶中,漏桶以一定的速率漏水。当请求过多时,水直接溢出。可以看出,漏桶算法可以强制限制数据的传输速度。但高并发情况下最容易出现瓶颈。
3.令牌桶算法
令牌桶算法的原理是系统以一定速率向桶中放入令牌,如果有请求时,请求会从桶中取出令牌,如果能取到令牌,则可以继续完成请求,否则等待或者拒绝服务。这种算法可以应对突发程序的请求,因此比漏桶算法好。
在Wikipedia上,令牌桶算法是这么描述的:
·每秒会有r个令牌放入桶中,或者说,每过1/r 秒桶中增加一个令牌
·桶中最多存放b个令牌,如果桶满了,新放入的令牌会被丢弃
·当一个n字节的数据包到达时,消耗n个令牌,然后发送该数据包
·如果桶中可用令牌小于n,则该数据包将被缓存或丢弃
Spring Cloud Gateway限流
在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: 8088spring: cloud: gateway: routes: - id: limit_route uri: http://httpbin.org.com: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
在上面的配置文件,指定程序的端口为8088,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
burstCapacity,令牌桶总容量。
replenishRate,令牌桶每秒填充平均速率。
key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
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:8088,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过redis客户端去查看redis中存在的key。
可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义可以看lua源代码。
使用Guava的RateLimiter做限流
Guava中开源出来一个令牌桶算法的工具类RateLimiter,可以轻松实现限流的工作。RateLimiter对简单的令牌桶算法做了一些工程上的优化,具体的实现是SmoothBursty。需要注意的是,RateLimiter的另一个实现SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。也许是出于简单起见,RateLimiter中的时间窗口能且仅能为1S,如果想搞其他时间单位的限流,只能另外造轮子。
RateLimiter有一个有趣的特性是[前人挖坑后人跳],也就是说RateLimiter允许某次请求拿走了超出剩余令牌数的令牌,但是下一次请求将为此付出代价,一直等到令牌亏空补上,并且桶中有足够本次请求使用的令牌为止。这里面就涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,还是让它走掉后面的请求等一等呢?Guava的设计者选择的是后者,先把眼前的活干了,后面的事后面再说。
测试代码:
public class RateLimiterMain {
public static void main(String[] args) {
RateLimiter rateLimiter = RateLimiter.create(2);
System.out.println(rateLimiter.acquire(5));
System.out.println(rateLimiter.acquire(2));
System.out.println(rateLimiter.acquire(1));
}
}
输出内容:
0.0
2.496889
0.992149
可以看出,令牌桶每秒只能产生2个令牌,我们可以第一次取出5个,但是第二次再去取令牌的时候,需要等2.5s,也就是第一次令牌取完后,需要等2.5s才能取到令牌。同样的,第三次取1个令牌的时候,也需要等待第二次的1s的时间。也就是,取的速率可以超过令牌产生的速率,但是下一次再次去取的时候,需要阻塞等待。
当然也可以使用tryAcquire来非阻塞的获取,可以实时返回结果。另外tryAcquire也可以传入参数,也就是等待的时间,超时直接返回false。这点等同于常见的lock,tryLock。
并发控制Semapphore
一般来说,在网关系统中,还有一个参数叫并发控制,就是某一个资源可以被同时访问的个数。这种情况下,我们可以使用Semaphore来控制。
Semaphore不同于互斥锁。互斥锁是某个资源只能支持同时一个访问,而Semaphore可以支持多个访问,但是加上了总数的控制。
示例
4.1 在pom中加入guava依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
把限流服务封装到一个类中LimitService,提供tryAcquire()方法,用来尝试获取令牌,返回true表示获取到,如下所示:
@Servicepublic class LimitService {
//每秒只发出5个令牌 RateLimiter rateLimiter = RateLimiter.create(5.0);
/** * 尝试获取令牌 * @return */ public boolean tryAcquire(){ return rateLimiter.tryAcquire(); }}
调用方是个普通的controller,每次收到请求的时候都尝试去获取令牌,获取成功和失败打印不同的信息,如下:
@Controller
public class HelloController {
private static SimpleDateFormat sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss');
@Autowired
private LimitService limitService;
@RequestMapping('/access')
@ResponseBody
public String access(){
//尝试获取令牌
if(limitService.tryAcquire()){
//模拟业务执行500毫秒
try {
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
return 'aceess success [' + sdf.format(new Date()) + ']';
}else{
return 'aceess limit [' + sdf.format(new Date()) + ']';
}
}
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。