简介
限流:在高并发系统中,往往需要在系统中限流,一方面是为了防止大量请求使服务器过载,导致服务的不可用,另一方面是为了防止网络攻击。
一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如 nginx 的 limit_conn 模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如 Guava 的 RateLimiter、nginx 的 limit_req 模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制 MQ 的消费速率。另外还可以根据网络连接数、网络流量、CPU 或内存负载等来限流。
限流算法
计数器
简单的做法计数维护一个单位时间的 计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间过去,再将计数器置为0。此算法有一个弊端,例如单位时间为1s,设定的阈值为100,在10ms的时候就已经有100个请求通过,那后面的990ms,就浪费掉了,所有的请求都不会被处理,这种现象称为突刺现象。
常用的更平滑的限流算法有两种:漏桶算法和令牌桶算法。
漏桶算法
漏桶算法思路很简单,水(请求)先进入到漏桶中,漏桶以一定的速度出水(接口响应速率),当水流入的速度过大时(访问频率超过接口响应速率)会直接溢出(直接拒接请求),可以看到漏桶算法可以强行限制数据的传输速率。
这里有两个变量,一个时桶的大小,支持流量突发增多时可以存放多少水(burst),另一个是水桶漏洞的大小(rate),因为漏桶的漏出速度是固定的参数,即使网络中不存在资源冲突(没有发生阻塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
令牌桶算法
令牌桶算法 和漏桶算法 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。
令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。
限流实现
在 Spring Cloud Gateway 上实现限流是个不错的选择,只需要编写一个过滤器就可以了。
Spring Cloud Gateway 已经内置了一个RequestRateLimiterGatewayFilterFactory,我们可以直接使用。
目前RequestRateLimiterGatewayFilterFactory的实现依赖于 Redis,所以我们还要引入spring-boot-starter-data-redis-reactive。
pom依赖
<!-- 限流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
yml
spring:
application:
name: gateway-service
redis:
host: 127.0.0.1
port: 6379
database: 0
password: 123456
cloud:
gateway:
discovery:
locator:
#是否与服务发现组件结合,通过serviceId转发到具体的实例。默认为false,设为true开始根据serviceId创建路由功能
enabled: false
#是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了)
lower-case-service-id: true
routes:
#自定义 全局唯一路由ID
- id: feign-service
#uri以lb://开头(lb代表从注册中心获取服务),后面接的就是你需要转发到的服务名称
uri: lb://FEIGN-SERVICE
#谓词
predicates:
#匹配路由 http://ip:port/feign/** 的请求
- Path=/feign/**
#过滤器
filters:
#剥离请求路径 例如 http://ip:port/feign/FEIGN-SERVICE/hello ==> http://ip:port/FEIGN-SERVICE/hello
- StripPrefix=1
#熔断降级
- name: Hystrix
args:
name: feignHystrixCommand
fallbackUri: 'forward:/fallbackCommand'
#限流
- name: RequestRateLimiter
args:
# 使用SpEL名称引用Bean,与上面新建的RateLimiterConfig类中的bean的name相同
key-resolver: '#{@ipKeyResolver}'
# 每秒最大访问次数 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 5
# 令牌桶最大容量
redis-rate-limiter.burstCapacity: 10
在上面的配置文件,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
- burstCapacity:令牌桶总容量。
- replenishRate:令牌桶每秒填充平均速率。
- key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
IP限流
获取请求用户ip作为限流key
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
测试网关是否做到了限流,使用 jmeter 测试工具,测试配置如下,100个线程,2秒内,循环2次,总共200个请求
查看结果
可以看到只有部分请求通过,其它请求全部被拒接。想更直观的了解,可以修改burstCapacity、replenishRate来测试一下。
还可以使用用户限流、接口限流,如果同时定义多个bean,启动的时候会报错,可以使用@Primary或@Qualifier来解决
用户限流
用户限流,使用这种方式限流,请求路径中必须携带userId参数
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
接口限流
获取请求地址的uri作为限流key。
@Bean
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
在真实场景中,限流数的调整需要依赖配置中心,当网站做活动时,动态调整限流数,新服务上线时,通过配置中心做动态路由等。
吐槽一下csdn 3级一下竟然不可以自定义标签了,这是在歧视我们啊!!!