分布式限流的几种维度
QPS和连接数控制
针对上图中的连接数和QPS(query per second)限流来说,我们可以设定IP维度的限流,也可以设置基于单个服务器的限流。在真实环境中通常会设置多个维度的限流规则,比如设定同一个IP每秒访问频率小于10,连接数小于5,再设定每台机器QPS最高1000,连接数最大保持200。更进一步,我们可以把某个服务器组或整个机房的服务器当做一个整体,设置更high-level的限流规则,这些所有限流规则都会共同作用于流量控制。
传输速率
对于“传输速率”大家都不会陌生,比如资源的下载速度。有的网站在这方面的限流逻辑做的更细致,比如普通注册用户下载速度为100k/s,购买会员后是10M/s,这背后就是基于用户组或者用户标签的限流逻辑。
黑白名单
黑白名单是各个大型企业应用里很常见的限流和放行手段,而且黑白名单往往是动态变化的。举个例子,如果某个IP在一段时间的访问次数过于频繁,被系统识别为机器人用户或流量攻击,那么这个IP就会被加入到黑名单,从而限制其对系统资源的访问,这就是我们俗称的“封IP”。
分布式环境
所谓的分布式限流,其实道理很简单,一句话就可以解释清楚。分布式区别于单机限流的场景,它把整个分布式环境中所有服务器当做一个整体来考量。比如说针对IP的限流,我们限制了1个IP每秒最多10个访问,不管来自这个IP的请求落在了哪台机器上,只要是访问了集群中的服务节点,那么都会受到限流规则的制约。
- 网关层限流 : 将限流规则应用在所有流量的入口处
- 中间件限流: 将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量
基于Guava的单机RateLimiter限流
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
//每秒钟放出的令牌
RateLimiter limiter = RateLimiter.create(2.0);
// 非阻塞限流
@GetMapping("/tryAcquire")
public String tryAcquire(Integer count){
if (limiter.tryAcquire(count)) {
log.info("success, rate is {}",limiter.getRate());
return "scuccess";
}else {
log.info("fail ,rate is {}",limiter.getRate());
return "fail";
}
}
//限定时间的非阻塞限流
@GetMapping("/tryAcquireWithTimeout")
public String tryAcquireWithTimeout(Integer count,Integer timeout){
if (limiter.tryAcquire(count,timeout, TimeUnit.SECONDS)) {
log.info("success, rate is {}",limiter.getRate());
return "scuccess";
}else {
log.info("fail ,rate is {}",limiter.getRate());
return "fail";
}
}
// 同步阻塞限流
@GetMapping("/acquire")
public String acquire(Integer count){
limiter.acquire(count);
log.info("success, rate is {}",limiter.getRate());
return "success";
}
基于Nignx网关层的限流
nginx.conf
#根据IP地址限制速度
# 1)第一个参数 $binary_remote_addr
# binary_目的是缩写内存占用量,remote_addr表示通过ip地址来限流
# 2)第二个参数 zone=iplimit:20m
# iplimit是一块内存区域(记录访问频率信息),20m是指这个块内存区域的大小
# 3)第三个参数 rate=1r/s
# 比如100/m, 表示访问的限流频率
limit_req_zone $binary_remote_addr zone=iplimit:20m rate=10r/s;
#根据服务器级别做限流
limit_req_zone $server_name zone=serverlimit:10m rate=100r/s;
#基于连接数级别的限流
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server{
server_name www.imooc-trining.com;
location /access-limit/ {
proxy_pass http://127.0.0.1:10086/;
# 基于ip地址的限制
# 1)第一个参数 zone=iplimit => 引用limit_req_zone中的zone变量
# 2)第二个参数 burst=2, 设置一个大小为2的缓冲区域,当大量请求到来。
# 请求数量超过限流频率时,将其放入缓冲区域
# 3)第三个参数 nodelay => 缓存区满了以后返回503异常
limit_req zone=iplimit burst=2 nodelay;
#基于服务器级别的限制
#通常情况下server级别的限流速率是最大的
limit_req zone=serverlimit burst=1 nodelay;
#每个server最大保持100个连接
limit_conn perserver 100;
#每个ip地址最多保持一个连接
limit_conn perip 1;
# 异常情况返回504 (默认是503)
limit_req_status 504;
limit_conn_status 504;
}
}
// Nginx 专用
//1.修改hosts文件 -》 www.imooc-trining.com = localhost 127.0.0.1
//(127.0.0.1 www.imooc-trining.com)
//2.修改nginx -》 将步骤1中的域名,添加到路由规则当中
// 配置文件地址 /usr/local/nginx/conf/nginx.conf
// 3.添加配置项:参考resource文件夹下面的nginx.conf
// 4.重新加载nginx => nginx -s reload
@GetMapping("/nginx")
public String nginx(){
log.info("Nginx seccess");
return "success";
}
@GetMapping("/nginx-conn")
public String nginxConn(@RequestParam(defaultValue = "0") int seconds){
try {
Thread.sleep(1000*seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "sccess";
}
基于Redis+Lua的服务端限流
Lua是如何和Redis交互的:
在redis-cli里面执行Lua脚本
公式:eval script numkeys key [key ...] args [args]
例子:
无参数:eval "return 'hello redis+lua'" 0
有参数 eval "return {KEYS[1] ARGV[1]}" 2 K1 K2 V1 V2
redis预加载Lua脚本
公式:script load script
例子:
无参数:
定义:script load "return 'hello redis+lua'" 返回一个序列
调用:evalsha "序列"
有参数:
定义:script load "return 'hello redis' ..KEYS[1]" 返回一个序列
调用:evalsha "序列" 1 key1
判断预加载的是否存在?
调用:script exists "序列"
删除预加载的lua脚本:
调用:script flush
服务端限流Demo
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {
int limit();
String methodKey() default "";
}
@Aspect
@Component
@Slf4j
public class AccessLimiterAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisScript<Boolean> rateLimitLua;
@Pointcut("@annotation(com.imooc.springcloud.annotation.AccessLimiter)")
public void cut(){
log.info("cut");
}
@Before("cut()")
public void before(JoinPoint joinPoint){
// 1.获得方法签名,作为method Key
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
AccessLimiter annotation = method.getAnnotation(AccessLimiter.class);
if (annotation == null){
return;
}
String key = annotation.methodKey();
Integer limit = annotation.limit();
//如果没有设置methodkey,从调用方法签名自动生成一个key
if(StringUtils.isEmpty(key)){
Class[] types = method.getParameterTypes();
key = method.getName();
if(types != null){
String paramTypes = Arrays.stream(types).map(Class::getName).collect(Collectors.joining(","));
log.info("params types: " + paramTypes);
key += "#" + paramTypes;
}
}
//2.调用redis
//step 1: request Lua script
boolean acquire = stringRedisTemplate.execute(
rateLimitLua, // Lua 脚本的真身
Lists.newArrayList(key), //Lua脚本的key列表
limit.toString() //Lua脚本的value脚本
);
if(!acquire){
log.error("your access is blocked,key={}",key);
throw new RuntimeException("Your access is bolcked");
}
}
}
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
return new StringRedisTemplate(factory);
}
@Bean
public DefaultRedisScript loadRedisScript(){
DefaultRedisScript redisScript = new DefaultRedisScript();
redisScript.setLocation(new ClassPathResource("ratelimiter.lua"));
redisScript.setResultType(Boolean.class);
return redisScript;
}
}
Lua脚本:
-- 获取方法签名特征
local methonKey = KEYS[1]
redis.log(redis.LOG_DEBUG, 'key is',methonKey)
-- 调用脚本的传入的限流大小
local limit = tonumber(ARGV[1])
-- 获取当前流量大小
local count = tonumber(redis.call('get',methonKey) or "0")
-- 判断是否超出限流阈值
if count + 1 > limit then
-- 拒绝服务访问
return false
else
-- 没有超出阈值
--设置当前访问的数量加1
redis.call("INCRBY", methonKey,1);
redis.call("EXPIRE",methonKey,1);
return true
end
调用:
@RestController
@Slf4j
public class Controller {
// 提醒! 注意配置扫包路径
@GetMapping("/test-annotation")
@com.imooc.springcloud.annotation.AccessLimiter(limit = 1)
public String testAnnotation(){
return "access";
}
}