风也温柔

计算机科学知识库

java 令牌桶算法 流量控制算法——漏桶算法和令牌桶算法

  一、写在最前

  轰轰烈烈的双十二已经过去小半个月了,程序猿的我坐在办公桌上思考,双十二这么大的访问量,这群电商是怎么扛住的,接口分分钟会变得不可用,并引发连锁反应导致整个系统崩溃。好吃懒做的小编,被可怕的好奇心驱使着去调研流量控制算法。好奇心害死猫,才有了这篇文章。

  二、流量控制算法简介

  流量控制在计算机领域称为过载保护。何为过载保护?所谓“过载”,即需求超过了负载能力;而“保护”则是指当“过载”发生了,采取必要的措施保护自己不受“伤害”。在计算机领域,尤其是分布式系统领域,“过载保护”是一个重要的概念。一个不具备“过载保护”功能的系统,是非常危险和脆弱的,很可能由于瞬间的压力激增,引起“雪崩效应”,导致系统的各个部分都同时崩溃,停止服务。这就好像在没有保险丝的保护下,电压突然变高,导致所有的电器都会被损坏一样,“过载保护”功能是系统的“保险丝”。

  如今互联网领域,也借鉴了这一思路扛住双十二, 控制网络数据传输的速率,使流量以比较均匀的速度向外发送。最终实现优化性能,减少延迟和提高带宽等。

  三、常用的限流算法

  常用的限流算法有两种:漏桶算法和令牌桶算法。本篇文章将介绍自己造轮子限流算法、漏桶算法和令牌桶算法。

  3.1 自己造轮子限流算法

  作为一名小白,我是不愿意自己造轮子的,但是真要早轮子,有一个简单粗暴的思路:

  1)设置单位时间T(如10s)内的最大访问量,在单位时间T内维护计数器Count;

  2)当请求到达时,判断时间是否进入下一个单位时间;

  3)如果是,则重置计数器为0;

  4)如果不是,计数器Count++,并判断计数器Count是否超过最大访问量,如超过,则拒绝访问。

   long timeStamp = getNowTime();

    int reqCount = 0; 
    const int maxReqCount = 10000;//时间周期内最大请求数
    const long effectiveDuration = 10;//时间控制周期
    public static bool control(){
         long now = getNowTime();
         if (now < timeStamp + effectiveDuration){//在时间控制范围内
             reqCount++; 
             return reqCount > maxReqCount;//当前时间范围内超过最大请求控制数
         }else{
             timeStamp = now;//超时后重置
             reqCount = 0; 
             return true;
         } 
    } 
    public static int getNowTime(){
        long time = System.currentTimeMillis();
        return   (int) (time/1000);
    }

  该算法实现看似确实完美的实现了“单位时间内最大访问量控制”,但它在两个单位时间的临界值上的处理是有缺陷的。如:设需要控制的最大请求数为1w, 在第一个单位时间(0-10s)的最后一秒(即第9s)里达到的请求数为1w,接下来第二个单位时间(10-20s)的第一秒(即第10s)里达到请求数也是1w,由于超时重置发生在两个单位时间之间,所以这2w个请求都将通过控制,也就是说在2s里处理2w个请求java 令牌桶算法 流量控制算法——漏桶算法和令牌桶算法,与我们设置的10s里1w个请求的设想是相违背。

  学术一点的说法是该算法处理请求不够平滑,不能很好的满足限流需求。

  3.2 漏桶算法

  漏桶算法思路很简单,请求先进入到漏桶里,漏桶以固定的速度出水,也就是处理请求,当水加的过快,则会直接溢出,也就是拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

  java 令牌桶算法_令牌桶算法程序框图_漏桶算法 java实现

  漏桶算法

   long timeStamp = getNowTime();

    int capacity = 10000;// 桶的容量
    int rate = 1;//水漏出的速度
    int water = 100;//当前水量
    public static bool control() {   
        //先执行漏水,因为rate是固定的,所以可以认为“时间间隔*rate”即为漏出的水量
        long  now = getNowTime();
        water = Math.max(0, water - (now - timeStamp) * rate);
        timeStamp = now;
        if (water < capacity) { // 水还未满,加水
            water ++; 
            return true; 
        } else { 
            return false;//水满,拒绝加水
       } 
    } 

  该算法很好的解决了时间边界处理不够平滑的问题,因为在每次请求进桶前都将执行“漏水”的操作,再无边界问题。

  但是对于很多场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

  3.3 令牌桶算法

  令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

  java 令牌桶算法_漏桶算法 java实现_令牌桶算法程序框图

  令牌桶算法

  3.3.1 原理

  令牌桶是网络设备的内部存储池,而令牌则是以给定速率填充令牌桶的虚拟信息包。每个到达的令牌都会从数据队列领出相应的数据包进行发送,发送完数据后令牌被删除。

  请求注解(RFC)中定义了两种令牌桶算法——单速率三色标记算法和双速率三色标记算法,其评估结果都是为报文打上红、黄、绿三色标记。QoS会根据报文的颜色,设置报文的丢弃优先级,其中单速率三色标记比较关心报文尺寸的突发,而双速率三色标记则关注速率上的突发,两种算法都可工作于色盲模式和非色盲模式。以下结合这两种工作模式介绍一下RFC中所描述的这两种算法。

  1)单速率三色标记算法

  网络工程师任务小组(IETF)的RFC文件定义了单速率三色标记算法,评估依据以下3个参数:承诺访问速率(CIR),即向令牌桶中填充令牌的速率;承诺突发尺寸(CBS),即令牌桶的容量,每次突发所允许的最大流量尺寸(注:设置的突发尺寸必须大于最大报文长度);超额突发尺寸(EBS)。

  一般采用双桶结构:C桶和E桶。Tc表示C桶中的令牌数,Te表示E桶中令牌数,两桶的总容量分别为CBS和EBS。初始状态时两桶是满的,即Tc和Te初始值分别等于CBS和EBS。令牌的产生速率是CIR,通常是先往C桶中添加令牌,等C桶满了,再往E桶中添加令牌,当两桶都被填满时,新产生的令牌将会被丢弃。

  色盲模式下,假设到达的报文长度为B。若报文长度B小于C桶中的令牌数Tc,则报文被标记为绿色,且C桶中的令牌数减少B;若TcTe,标记为红色,两桶总令牌数都不减少。

  在非色盲模式下,若报文已被标记为绿色或B Te,则标记为红色,Tc和Te都不减少。

  2)双速率三色标记算法

  IETF的RFC文件定义了双速率三色算法,主要是根据4种流量参数来评估:CIR、CBS、峰值信息速率(PIR),峰值突发尺寸(PBS)。前两种参数与单速率三色算法中的含义相同,PIR这个参数只在交换机上才有,路由器没有这个参数。该值必须不小于CIR的设置值,如果大于CIR,则速率限制在CIR于PRI之间的一个值。

  与单速率三色标记算法不同,双速率三色标记算法的两个令牌桶C桶和P桶填充令牌的速率不同,C桶填充速率为CIR,P桶为PIR;两桶的容量分别为CBS和PBS。用Tc和Tp表示两桶中的令牌数目,初始状态时两桶是满的,即Tc和Tp初始值分别等于CBS和PBS。

  色盲模式下,如果到达的报文速率大于PIR,超过Tp+Tc部分无法得到令牌,报文被标记为红色,未超过Tp+Tc而从P桶中获取令牌的报文标记为黄色,从C桶中获取令牌的报文被标记为绿色;当报文速率小于PIR,大于CIR时,报文不会得不到令牌,但超过Tp部分报文将从P桶中获取令牌,被标记为黄色报文,从C桶中获取令牌的报文被标记为绿色;当报文速率小于CIR时,报文所需令牌数不会超过Tc,只从C桶中获取令牌,所以只会被标记为绿色报文。

  在非色盲模式下,如果报文已被标记为红色或者超过Tp+Tc部分无法得到令牌的报文,被标记为红色;如果标记为黄色或者超过Tc未超过Tp部分报文记为黄色;如果报文被标记为绿或未超过Tc部分报文,被标记为绿色。

  3.3.2 算法描述与实现

   long timeStamp=getNowTime();

    int capacity; // 桶的容量
    int rate ;//令牌放入速度
     int tokens;//当前水量
    bool control() {
       //先执行添加令牌的操作
       long  now = getNowTime();
       tokens = max(capacity, tokens+ (now - timeStamp)*rate); 
       timeStamp = now;   //令牌已用完,拒绝访问
       if(tokens nextFreeTicketMicros) {
            storedPermits = min(maxPermits,
            storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
            nextFreeTicketMicros = nowMicros;
        }
    }

  另外,对于的使用,存在两种策略,二者区别主要体现在使用时候需要等待的时间。这个逻辑由ime函数实现:

   /**

     * Translates a specified portion of our currently stored permits which we want to
     * spend/acquire, into a throttling time. Conceptually, this evaluates the integral
     * of the underlying function we use, for the range of
     * [(storedPermits - permitsToTake), storedPermits].
     *
     * <p>This always holds: {@code 0  0.0) {
                    double permitsAboveHalfToTake = min(availablePermitsAboveHalf, permitsToTake);
                    micros = (long) (permitsAboveHalfToTake * (permitsToTime(availablePermitsAboveHalf)
                            + permitsToTime(availablePermitsAboveHalf - permitsAboveHalfToTake)) / 2.0);
                    permitsToTake -= permitsAboveHalfToTake;
                }
                // measuring the integral on the left part of the function (the horizontal line)
                micros += (stableIntervalMicros * permitsToTake);
                return micros;
            }
            private double permitsToTime(double permits) {
                return stableIntervalMicros + permits * slope;
            }
        }

  等于热身()期间能产生的令牌数,比如QPS=4,为2秒,则=8。为的一半。

  参考注释中的神图:

<p><pre>* ^ throttling

  • |
  • 3*stable + /
  • interval | /.
  • (cold) | / .
  • | / .