关于分布式限流

分布式限流的几种维度
在这里插入图片描述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";
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值