分布式系统设计:弹性设计-限流设计和算法

限流是避免系统在过载的情况出现问题。

常见的有:数据的数据库连接池,dubbo的线程池,nginx的并发连接数配置等等

限流的策略:

限流的目的是对并发访问的速度控制,一旦到达限定的阈值,就会触发限流行为,常见的限流行为有:

拒绝服务:流量突增,拒绝掉流量最大的来源,eg根据ip,访问量统计,直接拒绝掉请求

服务降级:关闭或者后端服务降级处理,关闭不重要的服务。不返回全量数据,只返回部分数据。

特权请求:资源不够时,分配给重要的用户,eg:服务vip用户的权利更大些,尽量优先处理。非vip用户优先限流。

延时处理:使用队列减少峰值压力,缓存大量的请求。如果队列任务超时,也要返回错误。应对短时的峰刺请求。达到削峰填谷效果。

弹性伸缩:使用自动化运维,对服务自动化伸缩,需要对应用的监控系统。eg:k8s中自动扩容,缩容服务器的部署数量。

    如果是数据库的压力过大,弹性伸缩应用作用不大,以限流为主。

限流的实现方式:

  • 令牌桶:Token Bucket
  • 漏桶算法:Leaky Bucket
  • 固定窗口计数器:Fixed Window Counter
  • 滑动日志:Sliding Logs
  • 滑动窗口计数器:Sliding Window Counter

固定窗口计数器:Fixed Window Counter

Fixed Window Image

 使用N秒的时间窗口,(eg:60秒)如果计数器超过阈值则丢弃盖青青。

 主要的问题是临界问题:在窗口边界附近发生的突发流量可能会请求处理量翻倍,因为该算法,允许短时间内当前窗口和下一个窗口进行请求。

eg:上图上一个时间窗口末尾算瞬间请求100个,下一个时间窗口请求100个,2秒内请求数量和为200个。 

滑动日志:Sliding Logs

Sliding Log Image

滑动日志算法,跟踪每个请求,并记录时间戳日志,这些日志通常存储在按时间排序的hashset或表中,时间戳超规格阈值的日志将被丢弃。新的请求进来是,计数日志的总和确认请求量,是否超规定阈值。

优点:

不受固定窗口边界条件影响。速率限制将被精确执行,每个客户的滑动日志都被跟踪,所以不会需要固定窗口的问题。

缺点:每个请求都存储的日志和计算可能会消耗大量资源,计算还可能压垮服务器。不能很好的扩展处理大量突发流量或dDos拒绝服务攻击。

滑动窗口计数器:Sliding Window Counter

滑动窗口算法结合了固定窗口滑动日志算法。

 窗口时间被分解为更小的桶——每个桶的大小取决于速率限制阈值。每个桶存储与桶范围相对应的请求计数, 它不断地随时间移动,平滑突发流量。

eg:“ 5 个 请求/分钟”速率限制器的滑动窗口计数器,使用作为窗口。

如上图:

1.user_1发出了一个新请求,请求时间为9:01:01作为其哈希键,并将计数器设置为1

2.用户没有进行任何其他的请求,直到_:_:02-第二窗口,里,请求了5次

后续没有请求,直到9:03:01 之前的超过1分钟,之前的被丢弃

令牌桶:Token Bucket

Token Bucket Image

漏桶算法:Leaky Bucket

 

 

 

队列算法:FIFO,优先级队列

请求速度波动的,处理速度均速。像 FIFO,先高优先级,再处理低优先级

 

 

低队列被饿死,有带权重的队列。下图:三个队列的权重分布是 3:2:1,权重 3 的这个队列上处理 3 个请求后,再去权重 2 的队列上处理 2 个请求,最后去 1 的队列上处理 1 个请求,反复。

队列满,触发限流:用队列长度来控制流量,配置上难操作。过长导致没满就挂掉了。不能做 push ,pull 方式会好一些

常见算法代码:

固定窗口计数器:Fixed Window Counter

/**
     * 固定窗口时间算法
     * @return
     */
    boolean fixedWindowsTryAcquire() {
        long currentTime = System.currentTimeMillis();  //获取系统当前时间
        if (currentTime - lastRequestTime > windowUnit) {  //检查是否在时间窗口内
            counter = 0;  // 计数器清0
            lastRequestTime = currentTime;  //开启新的时间窗口
        }
        if (counter < threshold) {  // 小于阀值
            counter++;  //计数器加1
            return true;
        }

        return false;
    }

滑动窗口计数器:Sliding Window Counter

 /**
     * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
     */
    private int SUB_CYCLE = 10;

    /**
     * 每分钟限流请求数
     */
    private int thresholdPerMin = 100;

    /**
     * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
     */
    private final TreeMap<Long, Integer> counters = new TreeMap<>();

   /**
     * 滑动窗口时间算法实现
     */
    boolean slidingWindowsTryAcquire() {
        long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
        int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数

        //超过阀值限流
        if (currentWindowNum >= thresholdPerMin) {
            return false;
        }

        //计数器+1
        counters.get(currentWindowTime)++;
        return true;
    }

   /**
    * 统计当前窗口的请求数
    */
    private int countCurrentWindow(long currentWindowTime) {
        //计算窗口开始位置
        long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
        int count = 0;

        //遍历存储的计数器
        Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Long, Integer> entry = iterator.next();
            // 删除无效过期的子窗口计数器
            if (entry.getKey() < startTime) {
                iterator.remove();
            } else {
                //累加当前窗口的所有计数器之和
                count =count + entry.getValue();
            }
        }
        return count;
    }

漏桶算法:Leaky Bucket

/**
     * 每秒处理数(出水率)
     */
    private long rate;

    /**
     *  当前剩余水量
     */
    private long currentWater;

    /**
     * 最后刷新时间
     */
    private long refreshTime;

    /**
     * 桶容量
     */
    private long capacity;

    /**
     * 漏桶算法
     * @return
     */
    boolean leakybucketLimitTryAcquire() {
        long currentTime = System.currentTimeMillis();  //获取系统当前时间
        long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
        long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
        refreshTime = currentTime; // 刷新时间

        // 当前剩余水量还是小于桶的容量,则请求放行
        if (currentWater < capacity) {
            currentWater++;
            return true;
        }
        
        // 当前剩余水量大于等于桶的容量,限流
        return false;
    }

令牌桶:Token Bucket

 /**
     * 每秒处理数(放入令牌数量)
     */
    private long putTokenRate;
    
    /**
     * 最后刷新时间
     */
    private long refreshTime;

    /**
     * 令牌桶容量
     */
    private long capacity;
    
    /**
     * 当前桶内令牌数
     */
    private long currentToken = 0L;

    /**
     * 漏桶算法
     * @return
     */
    boolean tokenBucketTryAcquire() {

        long currentTime = System.currentTimeMillis();  //获取系统当前时间
        long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
        currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量
        refreshTime = currentTime; // 刷新时间
        
        //桶里面还有令牌,请求正常处理
        if (currentToken > 0) {
            currentToken--; //令牌数量-1
            return true;
        }
        
        return false;
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值