http://support.huawei.com/huaweiconnect/enterprise/thread-279033.html
CIR,CBS,EBS,PIR,PBS傻傻分不清楚?看这里!----揭秘令牌桶
|
概述
春暖花开的时候,大家都开着汽车外出旅游欣赏美丽的风景,却被堵在高速公路上,你是否为此感到痛苦?但如果有一种机制可以评估高速公路上的车流量、控制车流情况,确保进入高速公路的汽车都能在路上安全畅行,你是不是会觉得很开心?
与此相似,网络发生拥塞的时候,也是一件非常痛苦的事情,如图1和图2所示。
图1 网络拥塞场景1
图2 网络拥塞场景2
如果不限制用户发送的业务流量大小,大量不断突发的业务数据会使网络更加拥挤,严重时会出现网络拥塞,造成业务出现异常,同时也浪费网络资源,如图3和图4所示。
图3 网络拥塞造成资源浪费
图4 网络拥塞引起业务异常
那么,你是否很期待有一种机制可以在网络上通过监督进入网络的流量速率,以达到限制流量、提高网络资源使用效率的目的,从而保证有限的网络资源提供更好的网络服务?
流量评估
为了达到上述目的,我们需要对进入网络的流量进行监督,实现CAR(Committed Access Rate)。
CAR:将进入网络的用户流量的速率限制在约定的范围之内,从而避免引起网络拥塞。
要实现CAR,就需要对流量进行评估,然后根据评估的结果对流量采取相应的动作:
l 如果流量没有超速,设备会为报文奖励绿牌(将报文染色为绿色)。报文可畅通无阻,即被转发。
l 如果流量稍微超速,设备会发出黄牌警告(将报文染色为***)。通常报文会被降级,即修改报文的内部优先级,然后进行尽力而为的转发。
l 如果流量超速太多,设备会发出红牌将报文罚下(将报文染色为红色)。报文被禁止通行,即丢弃。
然而,报文不像汽车那样可以通过测速仪之类的仪器进行测速。那么,如何对报文的速率进行评估呢?——答案在这里:令牌桶。
令牌桶可以看作是一个存放令牌的容器,预先设定一定的容量。系统按给定的速度向桶中放置令牌,当桶中令牌满时,多余的令牌溢出。令牌桶是一种流量测量方法。
不得不说的令牌桶
接着上面高速公路的例子,假设进入高速公路的车辆需要在入口处领取到通行卡才能进入高速公路。为了节约人力成本,入口处放置自动出卡机。按照国家高速公路交通安全法的规定,在高速公路上行驶的车辆,车速超过100km/h时,应与同车道前车保持100米以上距离。为了保持最小安全行车距离100米,按车速100km/h计算,需要间隔至少3.6秒才能放行一辆车,因此出卡机每隔3.6秒出一张通行卡。在自动出卡机下放置一个盒子,自动出卡机按照3.6秒的间隔向盒子中投放通行卡。每辆进入高速公路的车辆,从盒子中领取通行卡之后才可以进入高速公路。
令牌桶算法与此类似。简单来说,令牌桶可以看作是一个存放一定数量令牌的容器。系统按设定的速度向桶中放置令牌。当桶中令牌满时,多出的令牌溢出,桶中令牌不再增加。在使用令牌桶对流量规格进行评估时,是以令牌桶中的令牌数量是否足够满足报文的转发为依据的。每个需要被转发的报文,都要从令牌桶中领取一定数量的令牌(具体数量视报文大小而定),才可以被正常转发。如果桶中存在足够的令牌可以用来转发报文,称流量遵守或符合约定值,否则称为不符合或超标。
按照系统向令牌桶投放令牌的速率和令牌桶的数量划分,令牌桶算法有三种模式:
l 单速单桶
l 单速双桶
l 双速双桶
下面我们以色盲模式为例详细介绍这三种模式。
单速单桶
假设有一套自动出卡系统,包括一台自动出卡机和一个盒子C。自动出卡机以固定的速率(3.6秒出一张通行卡)向盒子中投放通行卡。这个固定的速率,就相当于是单速单桶模式中的CIR参数。
CIR(Committed Information Rate):承诺信息速率,表示向C桶(单桶模式中只有一个令牌桶,称为C桶)中投放令牌的速率,即C桶允许传输或转发报文的平均速率。
如果平均每3.6秒来了不止1辆车,通行卡很快就领完了。这时自动出卡机上的红灯亮起,新到的车辆禁止进入高速公路。这就起到了限定作用,将放行车辆的速率限制在放卡速率范围内。
如果平均每3.6秒来了不到1辆车,盒子里就会有一些积累起来的通行卡。这些累积起来的通行卡可以应付车队(比如车友俱乐部的车队)要进入高速公路这样的情况。我们规定每辆车都要领取通行卡,都由领队的车领取。
假设某时刻来了一个车队共8辆车,但盒子里只有6张通行卡,领队车拿起通行卡一数,发现不够,那么这个车队不允许进入高速,通行卡放回盒子中。
盒子中的通行卡不停累积,总有盒子装满的时候。这个盒子的容积,就相当于单速单桶模式中的CBS参数。
CBS(Committed Burst Size):承诺突发尺寸,表示C桶的容量,即C桶瞬间能够通过的承诺突发流量。相当于盛放通行卡的盒子里最多可以放多少张通行卡,也就是说,该自动出卡系统允许通过的车队中最多可以有多少辆车。
在单速单桶模式中,系统按照CIR速率向C桶中投放令牌。
l 如果可用令牌的总数量(Tc)小于CBS,则令牌数继续增加。
l 如果令牌桶已满,则令牌数不再增加。
对于到达的报文(报文大小为B),
l 如果B ≤ Tc,报文被标记为绿色,且Tc减少B。
l 如果B > Tc,报文被标记为红色,Tc不减少。
假设设备端口的CIR设置为1Mbit/s,CBS为2000bytes,初始状态时C桶满。
说明:为方便计算,此处1Mbit/s按1*106bit/s计算。
l 假设第1个到达的报文是1500bytes时,检查C桶发现令牌数大于数据包的长度,所以数据包被标为绿色,C桶减少令牌1500bytes,还剩500bytes。
l 假设1ms之后到达第2个报文1500bytes。在此间隔内,C桶新增令牌 = CIR * 1ms = 1000bit = 125bytes,加上C桶原来剩余的令牌500bytes,此时C桶共有625bytes。令牌数量不够,报文标记为红色。
l 假设又过1ms后到达第3个报文1000bytes。在此间隔内,C桶新增令牌125bytes,加上C桶原来剩余的令牌625bytes,此时C桶共有750bytes。令牌数量不够,因此报文被标记为红色。
l 假设又过20ms后到达第4个报文1500bytes。在此间隔内,C桶新增令牌 = CIR * 20ms = 20000bit = 2500bytes,加上C桶原来剩余的令牌750bytes,C桶此时令牌数为3250bytes。而CBS = 2000bytes,因此溢出1250bytes令牌被丢弃。此时C桶令牌数大于报文长度,报文标记为绿色,C桶减少令牌1500bytes,剩500bytes。
报文处理过程汇总见下表。
包序号 | 时刻 (ms) | 包长 (bytes) | 与上次添加令牌的间隔 | 本轮增加令牌 | 令牌增加后C桶令牌 | 报文处理后C桶剩余令牌 | 报文标记结果 |
| | | | | 2000 | 2000 | - |
1 | 0 | 1500 | 0 | 0 | 2000 | 500 | 绿色 |
2 | 1 | 1500 | 1 | 125 | 625 | 625 | 红色 |
3 | 2 | 1000 | 1 | 125 | 750 | 750 | 红色 |
4 | 22 | 1500 | 20 | 2500 | 2000 | 500 | 绿色 |
单速双桶
在单速单桶模式中说到,如果平均每3.6秒来了不到1辆车,盒子里就会有一些积累起来的通行卡。如果一直没有车辆过来,盒子中的通行卡不停地累积。盒子的容量是有限的,当盒子中装满通行卡之后,不断投放的通行卡就溢出盒子,会造成浪费。
为了避免这种浪费,我们改进了这个自动出卡系统,在原来的基础上增加一个盒子E(改进后的系统对应单速双桶模式,盒子E对应单速双桶中的E桶)。自动出卡机首先向C盒中投放通行卡。当C盒满了,自动出卡机就向E盒中投放通行卡。
为了保证通行卡有序领取,我们规定先领取C盒中的通行卡。如果C盒中的通行卡不够用,就把卡放回C盒,再从E盒中重新领取通行卡。C盒和E盒中的通行卡不能同时取用。
按照通行卡的取用,可以分为三种情况:
l 如果C盒中的通行卡够用,绿灯亮,车辆领取C盒中的通行卡后通行。
l 如果C盒中的通行卡不够用但E盒中的通行卡够用,黄灯亮,从C盒中领取的通行卡领取的通行卡要归还,车辆领取E盒中的通信卡后通行。
l 如果E盒中的通行卡也不够用,红灯亮,车辆禁止通行,从E盒中领取的通行卡领取通行卡要归还。
和单速单桶模式一样,对于车队,有几辆车,就领取几张通行卡。当然,E盒的容量也是有限的。E盒的容量就相当于单速双桶模式中的EBS。
EBS(Excess Burst Size):超额突发尺寸,表示E桶的容量,即E桶瞬间能够通过的超出突发流量。
假设某时刻来了一个车队有8辆车,但C盒里只有5张通行卡,而E盒中有9张通行卡,那么黄灯亮起,这辆长车从E盒中领取8张通行卡。
在单速双桶模式中,系统按照CIR速率向桶中投放令牌。
l 如果C桶中可用令牌的总数量(Tc)小于CBS,则C桶中令牌数增加。
l 如果Tc等于CBS且E桶中的可用令牌总数量(Te)小于EBS,则C桶中令牌数不增加,E桶中令牌数增加。
l 如果C桶和E桶中的令牌都已满,则两个桶中的令牌数都不再增加。
对于到达的报文(报文大小为B),
l 如果B ≤ Tc,报文被标记为绿色,且Tc减少B。
l 如果Tc < B ≤ Te,报文被标记为***,且Te减少B,Tc不减少。
l 如果B > Te,报文被标记为红色,且Tc和Te都不减少。
假设设备端口的CIR设置为1Mbit/s,CBS为2000bytes,EBS为2000bytes,初始状态时C桶和E桶满。
说明:为方便计算,此处1Mbit/s按1*106bit/s计算。
l 假设第1个到达的报文是1500bytes时,检查C桶发现令牌数大于数据包的长度,所以数据包被标为绿色,C桶减少令牌1500bytes,还剩500bytes,E桶令牌数量保持不变。
l 假设1ms之后到达第2个报文1500bytes。在此间隔内,C桶新增令牌 = CIR * 1ms = 1000bit = 125bytes,加上C桶原来剩余的令牌500bytes,此时C桶共有625bytes,检查发现C桶内令牌数量不够。检查E桶发现有足够令牌,因此报文标记为***,E桶减少令牌1500bytes,剩余500bytes,C桶剩余625byte保持不变。
l 假设又过1ms后到达第3个报文1000bytes。在此间隔内,C桶新增令牌125bytes,加上C桶原来剩余的令牌625bytes,此时C桶共有750bytes,检查发现C桶内令牌数量不够。检查E桶发现令牌数量也不够,因此报文被标记为红色,C桶、E桶令牌数不变。
l 假设又过20ms后到达第4个报文1500bytes。在此间隔内,C桶新增令牌 = CIR * 20ms = 20000bit = 2500bytes,加上C桶原来剩余的令牌750bytes,C桶此时令牌数为3250bytes。而CBS = 2000bytes,因此溢出的1250bytes添加到E桶,此时E桶有1750bytes。由于C桶中令牌数大于报文长度,报文标记为绿色,C桶减少令牌1500bytes,剩余500bytes,E桶不变。
报文处理过程汇总见下表。
包序号 | 时刻 (ms) | 包长 (bytes) | 与上次添加令牌的间隔 | 本轮增加令牌 | 令牌增加后各桶令牌 | 报文处理后各桶剩余令牌 | 报文标记结果 |
C桶 | E桶 | C桶 | E桶 |
| | | | | 2000 | 2000 | 2000 | 2000 | - |
1 | 0 | 1500 | 0 | 0 | 2000 | 2000 | 500 | 2000 | 绿色 |
2 | 1 | 1500 | 1 | 125 | 625 | 2000 | 625 | 500 | *** |
3 | 2 | 1000 | 1 | 125 | 750 | 500 | 750 | 500 | 红色 |
4 | 22 | 1500 | 20 | 2500 | 2000 | 1750 | 500 | 1750 | 绿色 |
双速双桶
前面说到的自动出卡机,都只有一个口可以输出通行卡。而这里说到的高级自动出卡机,有两个口可以出卡,一个口输出的是通行卡,一个口输出的是服务卡。当然,这里也有两个盒子用于盛放卡,分别是盒C盒和P盒。自动出卡机上的两个口分别以各自固定的速率向两个盒子中投放卡。(这个高级自动出卡机系统对应双速双桶模式,C盒和P盒对应双速双桶模式中的C桶和P桶。)
领取卡的规则和前面单速的情况有所不同。我们规定:
l 先领取服务卡。如果服务卡不够,把卡放回P盒,红灯亮,车辆禁止通行。
l 如果服务卡足够但通行卡不够,黄灯亮,服务卡可以取走,通行卡放回C盒。
l 如果服务卡和通行卡都足够,绿灯亮,车辆可以通行,服务卡和通行卡都取走。
自动出卡机向P盒投放服务卡的速率和P盒的容量,就分别相当于双速双桶模式中的PIR和PBS。
PIR(Peak information rate):峰值信息速率,表示向P桶中投放令牌的速率,即P桶允许传输或转发报文的峰值速率。PIR的值应大于CIR(存在服务卡足够而通行卡不够的情况)。
PBS(Peak Burst Size):峰值突发尺寸,表示P桶的容量,即P桶瞬间能够通过的峰值突发流量。
按照国家高速公路交通安全法的规定,在高速公路上行驶的车辆,最高时速为120km/h。前面领取的通行卡,保证车辆的时速为100km/h。而服务卡的作用,则是允许车辆时速可以达到120km/h。自动出卡机向P盒投放服务卡的速率就是允许的最高车速,相当于PIR。
在双速双桶模式中,系统按照PIR速率向P桶中投放令牌,按照CIR速率向C桶中投放令牌。
l 如果P桶中可用令牌的总数量(Tp)小于PBS,则P桶中令牌数增加。
l 如果C桶中可用令牌的总数量(Tc)小于CBS,则C桶中令牌数增加。
对于到达的报文(报文大小为B),
l 如果Tp < B,报文被标记为红色,且Tc和Tp都不减少。
l 如果Tc < B ≤ Tp,报文被标记为***,且Tp减少B,Tc不减少。
l 如果B ≤ Tc,报文被标记为绿色,且Tp和Tc都减少B。
假设设备端口的CIR设置为1Mbit/s,PIR设置为2Mbit/s,CBS为2000 bytes,PBS为3000 bytes,初始状态时C桶和P桶满。
说明:为方便计算,此处1Mbit/s按1*106bit/s计算。
l 第1个到达的报文假设是1500bytes时,检查发现报文长度不超过P桶也不超过C桶,所以数据包被标为绿色,C桶和P桶都减少令牌1500bytes,C桶还剩500bytes,P桶还剩1500bytes。
l 假设1ms后到达第2个报文1800bytes。在此间隔内,P桶新增令牌 = PIR * 1ms = 2000bit = 250bytes,加上P桶原来剩余的令牌1500bytes,此时P桶共有1750bytes,小于报文长度。C桶新增令牌 = CIR * 1ms = 1000bit = 125bytes,加上C桶原来剩余的令牌500bytes,此时C桶共有625bytes。报文标记为红色,P桶、C桶令牌数不变。
l 假设又过1ms后到达第3个报文1000bytes。在此间隔内,P桶新增令牌250byte,加上P桶原来剩余的令牌1750byte,此时P桶共有令牌2000bytes,大于报文长度。再检查C桶,C桶新增令牌250bytes,加上C桶原来剩余的令牌625byte,此时C桶共有750bytes,仍然小于报文长度。因此报文被标记为***,P桶减少令牌1000bytes,剩余1000bytes,C桶令牌不变。
l 假设又过20ms之后到达报文1500bytes。在此间隔内,P桶新增令牌 = PIR * 20ms = 40000bit = 5000bytes,超过P桶容量PBS,因此P桶令牌数 = PBS = 3000bytes,溢出的令牌丢弃。这样P桶有2000bytes,大于报文长度。此时C桶增加令牌 = CIR * 20ms = 20000bit = 2500bytes,超过C桶容量CBS,因此C桶令牌数 = CBS = 2000byte,溢出的令牌丢弃。C桶此时令牌数2000 bytes,大于报文长度。报文被标记为绿色,P桶减少令牌1500bytes,剩余1500bytes;C桶减少令牌1500bytes,剩余500bytes。
报文处理过程汇总见下表。
包序号 | 时刻 (ms) | 包长 (bytes) | 与上次添加令牌的间隔 | 本轮增加令牌 | 令牌增加后各桶令牌 | 报文处理后各桶剩余令牌 | 报文标记结果 |
C桶 | P桶 | C桶 | P桶 | C桶 | P桶 |
| | | | | | 2000 | 3000 | 2000 | 3000 | - |
1 | 0 | 1500 | 0 | 0 | 0 | 2000 | 3000 | 500 | 1500 | 绿色 |
2 | 1 | 1800 | 1 | 125 | 250 | 625 | 1750 | 625 | 1750 | 红色 |
3 | 2 | 1000 | 1 | 125 | 250 | 750 | 2000 | 750 | 1000 | *** |
4 | 22 | 1500 | 20 | 2500 | 5000 | 2000 | 3000 | 500 | 1500 | 绿色 |
三种令牌桶模式的区别和应用场景
由前文描述可以看出,三种令牌桶模式之间既有区别也有演进关系,具体见下表。
| 单速单桶 | 单速双桶 | 双速双桶 |
关键参数 | CIR和CBS | CIR、CBS和EBS | CIR、CBS、PIR和PBS |
令牌投放 | 以CIR速率向C桶投放令牌。C桶满时令牌溢出。 | C桶满时令牌投放到E桶。C桶和E桶都不满时,只向C桶投放令牌。 | 以CIR速率向C桶投放令牌,以PIR速率向P桶中投放令牌。两个桶相对独立。桶中令牌满时令牌溢出。 |
是否允许流量突发 | 不允许流量突发。报文的处理以C桶中是否有足够令牌为依据。 | 允许报文尺寸的突发。先使用C桶中的令牌,C桶中令牌数量不够时,使用E桶中的令牌。 | 允许报文速率的突发。C桶和P桶中的令牌足够时,两个桶中的令牌都使用。C桶中令牌不够时,只使用P桶中的令牌。 |
报文颜色标记结果 | 绿色或红色 | 绿色、***或红色 | 绿色、***或红色 |
演进关系 | 单速双桶模式中,如果EBS等于0,其效果和单速单桶是一样的。 双速双桶模式中,如果PIR等于CIR,其效果和单速单桶是一样的 |
基于上述三种令牌桶模式之间的区别,其功能和使用场景也有所不同,具体见下表。
令牌桶模式 | 功能 | 选用场景 |
单速单桶 | 限制带宽 | 优先级较低的业务(如企业外网HTTP流量),对于超过额度的流量直接丢弃保证其他业务,不考虑突发。 |
单速双桶 | 限制带宽,还可以容许一部分流量突发,并且可以区分突发业务和正常业务 | 较为重要的业务,容许有突发的业务(如企业邮件数据),对于突发流量有宽容。 |
双速双桶 | 限制带宽,可以进行流量带宽划分,可以区别带宽小于CIR还是在CIR ~PIR之间 | 重要业务,可以更好的监控流量的突发程度,对流量分析起到指导作用。 |
参数设置有讲究
在令牌桶算法中,CIR的值越大,即令牌产生的速率越大,报文可以获取的令牌就越多,流向网络的流量也就越大。因此,CIR的值是控制流入网络中流量多少的关键。CBS也是一个重要参数。CBS的值越大,C桶中可以积累令牌的数目也越多,允许通过的报文尺寸就越大。
由于设备进行的是逐包转发,CBS的值不应该小于当前网络上允许传输的报文的最大长度。
例如,在单速单桶模式下,假设要把流量限定在10Mbit/s,而CBS值太小(如设置成1000byte)。如果某个时间段内流量的报文,每个报文大小都大于1000byte,那么这些报文全都被丢弃。这段时间内,没有报文被转发,报文的转发速率为0,导致网络资源被浪费,业务也出现异常。
那是不是CBS的值越大就越好呢?显然不是。CBS值太大,会失去限速的意义。例如,假设要把流量限定在10Mbit/s,CBS设置成7200Mbyte。某个时刻,令牌桶中的令牌已满,如果接下来1小时内流量的报文,其报文长度不一,但共计7200Mbyte,这些报文都能获得令牌并被转发,那么这段时间内的报文速率为16Mbit/s(7200M * 8 / 3600)而不是10Mbit/s,即没有实现限速。
同理,对于双速双桶模式,PIR和PBS的值也应设置在合理范围内。
说明:目前华为以太网交换机只支持单速单桶和双速双桶模式。
简单来说,带宽参数的设置取决于实际业务的限速需要。原则上,令牌桶容量需要大于等于网络中可能出现的最大的报的长度和业务流量的正常突发量。对于华为的以太网交换机,我们有总结的经验性公式:
l 带宽 ≤ 100Mbit/s时,令牌桶容量(Bytes) = 带宽(kbit/s) * 1000(s)/8
l 带宽 > 100Mbit/s时,令牌桶容量(Bytes) = 100000(kbit/s) * 1000 (s)/8
另外,华为的以太网交换机,不同系列的单板支持的CAR粒度不同。在进行流量监管和限速时,
l 如果配置的CIR、PIR是最小粒度的整数倍,则按照配置的速率进行监管和限速。
l 如果配置的CIR、PIR不是最小粒度的整数倍,则按照最小粒度的整数倍进行流量监管和限速。
例如,设备上的监管和限速粒度为64kbit/s,如果CIR值 ≤ 64kbit/s,按照64kbit/s处理;如果128kbit/s < CIR值 ≤ 192kbit/s,按照192kbit/s处理。
令牌桶原理应用之接口限速
令牌桶原理可以应用到设备的入方向和出方向。根据令牌桶原理在不同方向的应用,可以实现不同的功能,见下图。
流量监管、流量整形、接口限速与令牌桶算法之间的关系,见下表。
应用方向 | 单速单桶 | 双速双桶 |
设备入方向 | 基于接口,实现接口限速 | 基于流,实现流量监管 |
设备出方向 | 基于接口,实现接口限速 | 基于队列,实现流量整形 |
流量监管是一种通过对流量规格进行监督,以限制流量及网络资源使用的流控策略。如果这种流控策略应用到设备接口的入方向,也可以实现入方向的接口限速。与基于接口实现的入方向的接口限速相比,这种方式引入了MQC(Modular QoS Command-Line Interface),因此应用更加灵活。
通常我们所说的限速是广义上的接口限速,包括基于接口和基于MQC实现的入方向的接口限速。
http://www.javaranger.com/archives/1769
基于漏桶(Leaky bucket)与令牌桶(Token bucket)算法的流量控制
Eric 2015.11.06
互联网服务赖以生存的根本是流量, 产品和运营会经常通过各种方式来为应用倒流,比如淘宝的双十一等,如何让系统在处理高并发的同时还是保证自身系统的稳定,通常在最短时间内提高并发的做法就是加机器,但是如果机器不够怎么办?那就需要做业务降级或系统限流,流量控制中用的比较多的两个算法就是漏桶和令牌桶.
漏桶算法(Leaky bucket)
漏桶算法强制一个常量的输出速率而不管输入数据流的突发性,当输入空闲时,该算法不执行任何动作.就像用一个底部开了个洞的漏桶接水一样,水进入到漏桶里,桶里的水通过下面的孔以固定的速率流出,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率.如下图所示:
令牌桶(Token bucket)
令牌桶算法的基本过程如下:
- 每秒会有 r 个令牌放入桶中,或者说,每过 1/r 秒桶中增加一个令牌
- 桶中最多存放 b 个令牌,如果桶满了,新放入的令牌会被丢弃
- 当一个 n 字节的数据包到达时,消耗 n 个令牌,然后发送该数据包
- 如果桶中可用令牌小于 n,则该数据包将被缓存或丢弃
漏桶和令牌桶比较
“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输数据外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的上限,因此它适合于具有突发特性的流量。
RateLimiter
我们可以使用 Guava 的 RateLimiter 来实现基于令牌桶的流量控制。RateLimiter 令牌桶算法的单桶实现,RateLimiter 对简单的令牌桶算法做了一些工程上的优化,具体的实现是 SmoothBursty。需要注意的是,RateLimiter 的另一个实现 SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。
SmoothBursty 有一个可以放 N 个时间窗口产生的令牌的桶,系统空闲的时候令牌就一直攒着,最好情况下可以扛 N 倍于限流值的高峰而不影响后续请求,就像三峡大坝一样能扛千年一遇的洪水.
(转载本站文章请注明作者和出处 JavaRanger – javaranger.com ,请勿用于任何商业用途)
本文链接: http://www.javaranger.com/archives/1769
http://www.cnblogs.com/exceptioneye/p/4783904.html
一、场景描述
很多做服务接口的人或多或少的遇到这样的场景,由于业务应用系统的负载能力有限,为了防止非预期的请求对系统压力过大而拖垮业务应用系统。
也就是面对大流量时,如何进行流量控制?
服务接口的流量控制策略:分流、降级、限流等。本文讨论下限流策略,虽然降低了服务接口的访问频率和并发量,却换取服务接口和业务应用系统的高可用。
实际场景中常用的限流策略:
按照一定的规则如帐号、IP、系统调用逻辑等在Nginx层面做限流
1、客户端限流
2、服务端限流
红线区,力保数据库
二、常用的限流算法
常用的限流算法由:楼桶算法和令牌桶算法。本文不具体的详细说明两种算法的原理,原理会在接下来的文章中做说明。
1、漏桶算法
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。
因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.
2、令牌桶算法
令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.
三、基于Redis功能的实现
简陋的设计思路:假设一个用户(用IP判断)每分钟访问某一个服务接口的次数不能超过10次,那么我们可以在Redis中创建一个键,并此时我们就设置键的过期时间为60秒,每一个用户对此服务接口的访问就把键值加1,在60秒内当键值增加到10的时候,就禁止访问服务接口。在某种场景中添加访问时间间隔还是很有必要的。
1)使用Redis的incr命令,将计数器作为Lua脚本
1 local current
2 current = redis.call("incr",KEYS[1])
3 if tonumber(current) == 1 then
4 redis.call("expire",KEYS[1],1)
5 end
Lua脚本在Redis中运行,保证了incr和expire两个操作的原子性。
2)使用Reids的列表结构代替incr命令
1 FUNCTION LIMIT_API_CALL(ip)
2 current = LLEN(ip)
3 IF current > 10 THEN
4 ERROR "too many requests per second"
5 ELSE
6 IF EXISTS(ip) == FALSE
7 MULTI
8 RPUSH(ip,ip)
9 EXPIRE(ip,1)
10 EXEC
11 ELSE
12 RPUSHX(ip,ip)
13 END
14 PERFORM_API_CALL()
15 END
Rate Limit使用Redis的列表作为容器,LLEN用于对访问次数的检查,一个事物中包含了RPUSH和EXPIRE两个命令,用于在第一次执行计数是创建列表并设置过期时间,
RPUSHX在后续的计数操作中进行增加操作。
四、基于令牌桶算法的实现
令牌桶算法可以很好的支撑突然额流量的变化即满令牌桶数的峰值。
1 import java.io.BufferedWriter;
2 import java.io.FileOutputStream;
3 import java.io.IOException;
4 import java.io.OutputStreamWriter;
5 import java.util.Random;
6 import java.util.concurrent.ArrayBlockingQueue;
7 import java.util.concurrent.Executors;
8 import java.util.concurrent.ScheduledExecutorService;
9 import java.util.concurrent.TimeUnit;
10 import java.util.concurrent.locks.ReentrantLock;
11
12 import com.google.common.base.Preconditions;
13 import com.netease.datastream.util.framework.LifeCycle;
14
15
20 public class TokenBucket implements LifeCycle {
21
22 // 默认桶大小个数 即最大瞬间流量是64M
23 private static final int DEFAULT_BUCKET_SIZE = 1024 * 1024 * 64;
24
25 // 一个桶的单位是1字节
26 private int everyTokenSize = 1;
27
28 // 瞬间最大流量
29 private int maxFlowRate;
30
31 // 平均流量
32 private int avgFlowRate;
33
34 // 队列来缓存桶数量:最大的流量峰值就是 = everyTokenSize*DEFAULT_BUCKET_SIZE 64M = 1 * 1024 * 1024 * 64
35 private ArrayBlockingQueue<Byte> tokenQueue = new ArrayBlockingQueue<Byte>(DEFAULT_BUCKET_SIZE);
36
37 private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
38
39 private volatile boolean isStart = false;
40
41 private ReentrantLock lock = new ReentrantLock(true);
42
43 private static final byte A_CHAR = 'a';
44
45 public TokenBucket() {
46 }
47
48 public TokenBucket(int maxFlowRate, int avgFlowRate) {
49 this.maxFlowRate = maxFlowRate;
50 this.avgFlowRate = avgFlowRate;
51 }
52
53 public TokenBucket(int everyTokenSize, int maxFlowRate, int avgFlowRate) {
54 this.everyTokenSize = everyTokenSize;
55 this.maxFlowRate = maxFlowRate;
56 this.avgFlowRate = avgFlowRate;
57 }
58
59 public void addTokens(Integer tokenNum) {
60
61 // 若是桶已经满了,就不再家如新的令牌
62 for (int i = 0; i < tokenNum; i++) {
63 tokenQueue.offer(Byte.valueOf(A_CHAR));
64 }
65 }
66
67 public TokenBucket build() {
68
69 start();
70 return this;
71 }
72
73 /**
74 * 获取足够的令牌个数
75 *
76 * @return
77 */
78 public boolean getTokens(byte[] dataSize) {
79
80 Preconditions.checkNotNull(dataSize);
81 Preconditions.checkArgument(isStart, "please invoke start method first !");
82
83 int needTokenNum = dataSize.length / everyTokenSize + 1;// 传输内容大小对应的桶个数
84
85 final ReentrantLock lock = this.lock;
86 lock.lock();
87 try {
88 boolean result = needTokenNum <= tokenQueue.size(); // 是否存在足够的桶数量
89 if (!result) {
90 return false;
91 }
92
93 int tokenCount = 0;
94 for (int i = 0; i < needTokenNum; i++) {
95 Byte poll = tokenQueue.poll();
96 if (poll != null) {
97 tokenCount++;
98 }
99 }
100
101 return tokenCount == needTokenNum;
102 } finally {
103 lock.unlock();
104 }
105 }
106
107 @Override
108 public void start() {
109
110 // 初始化桶队列大小
111 if (maxFlowRate != 0) {
112 tokenQueue = new ArrayBlockingQueue<Byte>(maxFlowRate);
113 }
114
115 // 初始化令牌生产者
116 TokenProducer tokenProducer = new TokenProducer(avgFlowRate, this);
117 scheduledExecutorService.scheduleAtFixedRate(tokenProducer, 0, 1, TimeUnit.SECONDS);
118 isStart = true;
119
120 }
121
122 @Override
123 public void stop() {
124 isStart = false;
125 scheduledExecutorService.shutdown();
126 }
127
128 @Override
129 public boolean isStarted() {
130 return isStart;
131 }
132
133 class TokenProducer implements Runnable {
134
135 private int avgFlowRate;
136 private TokenBucket tokenBucket;
137
138 public TokenProducer(int avgFlowRate, TokenBucket tokenBucket) {
139 this.avgFlowRate = avgFlowRate;
140 this.tokenBucket = tokenBucket;
141 }
142
143 @Override
144 public void run() {
145 tokenBucket.addTokens(avgFlowRate);
146 }
147 }
148
149 public static TokenBucket newBuilder() {
150 return new TokenBucket();
151 }
152
153 public TokenBucket everyTokenSize(int everyTokenSize) {
154 this.everyTokenSize = everyTokenSize;
155 return this;
156 }
157
158 public TokenBucket maxFlowRate(int maxFlowRate) {
159 this.maxFlowRate = maxFlowRate;
160 return this;
161 }
162
163 public TokenBucket avgFlowRate(int avgFlowRate) {
164 this.avgFlowRate = avgFlowRate;
165 return this;
166 }
167
168 private String stringCopy(String data, int copyNum) {
169
170 StringBuilder sbuilder = new StringBuilder(data.length() * copyNum);
171
172 for (int i = 0; i < copyNum; i++) {
173 sbuilder.append(data);
174 }
175
176 return sbuilder.toString();
177
178 }
179
180 public static void main(String[] args) throws IOException, InterruptedException {
181
182 tokenTest();
183 }
184
185 private static void arrayTest() {
186 ArrayBlockingQueue<Integer> tokenQueue = new ArrayBlockingQueue<Integer>(10);
187 tokenQueue.offer(1);
188 tokenQueue.offer(1);
189 tokenQueue.offer(1);
190 System.out.println(tokenQueue.size());
191 System.out.println(tokenQueue.remainingCapacity());
192 }
193
194 private static void tokenTest() throws InterruptedException, IOException {
195 TokenBucket tokenBucket = TokenBucket.newBuilder().avgFlowRate(512).maxFlowRate(1024).build();
196
197 BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("/tmp/ds_test")));
198 String data = "xxxx";// 四个字节
199 for (int i = 1; i <= 1000; i++) {
200
201 Random random = new Random();
202 int i1 = random.nextInt(100);
203 boolean tokens = tokenBucket.getTokens(tokenBucket.stringCopy(data, i1).getBytes());
204 TimeUnit.MILLISECONDS.sleep(100);
205 if (tokens) {
206 bufferedWriter.write("token pass --- index:" + i1);
207 System.out.println("token pass --- index:" + i1);
208 } else {
209 bufferedWriter.write("token rejuect --- index" + i1);
210 System.out.println("token rejuect --- index" + i1);
211 }
212
213 bufferedWriter.newLine();
214 bufferedWriter.flush();
215 }
216
217 bufferedWriter.close();
218 }
219
220 }
作者:三石雨
出处:http://www.cnblogs.com/exceptioneye
参考:
http://xiaobaoqiu.github.io/blog/2015/07/02/ratelimiter/
http://redisdoc.com/string/incr.html
http://www.cnblogs.com/zhengyun_ustc/archive/2012/11/17/topic1.html