漏桶算法:https://en.wikipedia.org/wiki/Leaky_bucket
它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。 漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 简单的理解为:漏桶算法思路很简单,水(数据或者请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
在桶满水之后,常见的两种处理方式为:
1)暂时拦截住上方水的向下流动,等待桶中的一部分水漏走后,再放行上方水。
2)溢出的上方水直接抛弃。
将水看作网络通信中数据包的抽象,则
方式1起到的效果称为Traffic Shaping(流量整形),
方式2起到的效果称为Traffic Policing(流量策略)。
由此可见,Traffic Shaping 的核心理念是“等待”,Traffic Policing 的核心理念是“丢弃”。它们是两种常见的流速控制方法。
在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的参数(恒定的速率往下漏水),所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使某一个单独的流突发到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法可以结合起来为网络流量提供更大的控制。
令牌桶算法:https://en.wikipedia.org/wiki/Token_bucket
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 令牌桶的另外一个好处是可以方便的改变速度。 一旦需要提高速率,则按需提高放入桶中的令牌的速率。 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量, 比如华为的专利"采用令牌漏桶进行报文限流的方法"(CN 1536815 A),提供了一种动态计算可用令牌数的方法, 相比其它定时增加令牌的方法, 它只在收到一个报文后,计算该报文与前一报文到来的时间间隔内向令牌漏桶内注入的令牌数, 并计算判断桶内的令牌数是否满足传送该报文的要求。
从整个架构的稳定性角度看,一般 SOA 架构的每个接口在有限资源的情况下,所能提供的单位时间服务能力是有限的。假如超过服务能力,一般会造成整个接口服务停顿,或者应用 Crash(宕机),或者带来连锁反应,将延迟传递给服务调用方造成整个系统的服务能力丧失。有必要在服务能力超限的情况下快速失败(Fail Fast)。另外,根据排队论,由于 API 接口服务具有延迟随着请求量提升迅速提升的特点,为了保证 SLA 的低延迟,需要控制单位时间的请求量。这也是 Little’s law 所说的。
还有,公开 API 接口服务,速率限制(Rate limiting)应该是一个必备的功能,否则公开的接口不知道哪一天就会被服务调用方有意无意的打垮。所以,提供资源能够支撑的服务,将过载请求快速抛弃对整个系统架构的稳定性非常重要。这就要求在应用层实现 Rate limiting 限制。
常见的 Rate limiting 的实现方式:
- Proxy 层的实现,针对部分 URL 或者 API 接口进行访问频率限制
Nginx 模块
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
location /search/ {
limit_req zone=one burst=5;
}
详细参见: ngx_http_limit_req_module
- Haproxy(负载均衡) 提供的功能
详细参见: Haproxy Rate limit 模块
- RateLimiters是令牌桶和漏桶在.NET 中实现。
这些策略可用于速率限制请求不同的网站中,后端或 API 调用等场景。
- 基于 Redis 功能的实现
这个在 Redis 官方文档有非常详细的实现。一般适用于所有类型的应用,比如 PHP、Python 等等。Redis 的实现方式可以支持分布式服务的访问频率的集中控制。Redis 的频率限制实现方式还适用于在应用中无法状态保存状态的场景。
- WebApiThrottle限流框架
- 限流工具类RateLimiter
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流,非常易于使用。RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取许可令牌(permits)接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到.
- 平滑突发限流(SmoothBursty)
Eg:阻塞等待
//每秒创建五个令牌,即没200ms创建一个令牌
RateLimiter rateLimiter = RateLimiter.create(5.0);
public static void main(String[] args) throws Exception {
RetaLimiterLearn rll = new RetaLimiterLearn();
rll.oneTest();
}
public void oneTest() {
for (int i = 0; i < 5; i++) {
//消耗一个令牌,线程会阻塞。
System.out.println(rateLimiter.acquire());
}
}
如果当前桶中有足够令牌则成功(返回值为0),如果桶中没有令牌则暂停一段时间,比如发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌,这种实现将突发请求速率平均为了固定请求速率。
再看一个例子:
public void twoTest() {
//一次获取5个令牌
System.out.println(rateLimiter.acquire(5));
for (int i = 0; i < 5; i++) {
System.out.println(rateLimiter.acquire());
}
}
令牌桶算法允许一定程度的突发,所以可以一次性消费5个令牌,但接下来的limiter.acquire(1)将等待差不多1秒桶中才能有令牌,且接下来的请求也整形为固定速率了。
public void threeTest() throws Exception{
//创建了一个桶容量为2且每秒新增2个令牌
RateLimiter limiter = RateLimiter.create(2);
//消费一个令牌,此时令牌桶可以满足(返回值为0)
System.out.println(limiter.acquire());
//线程暂停2秒
Thread.sleep(2000L);
for (int i = 0; i < 5; i++) {
System.out.println(limiter.acquire());
}
}
我们发现到第四个acquire时就需要等待500毫秒了。此处可以看到我们设置的桶容量为2(即允许的突发量),这是因为平滑突发限流(SmoothBursty)中有一个参数:最大突发秒数(maxBurstSeconds)默认值是1s,突发量/桶容量=速率*maxBurstSeconds,所以本示例桶容量/突发量为2,例子中前两个是消费了之前积攒的突发量,而第三个开始就是正常计算的了。令牌桶算法允许将一段时间内没有消费的令牌暂存到令牌桶中,留待未来使用,并允许未来请求的这种突发。
SmoothBursty通过平均速率和最后一次新增令牌的时间计算出下次新增令牌的时间的,另外需要一个桶暂存一段时间内没有使用的令牌(即可以突发的令牌数)。
- 平滑预热限流(smoothWarmingUp)
RateLimiter还提供了tryAcquire方法来进行无阻塞或可超时的令牌消费。因为SmoothBursty允许一定程度的突发,会有人担心如果允许这种突发,假设突然间来了很大的流量,那么系统很可能扛不住这种突发。因此需要一种平滑速率的限流工具,从而系统冷启动后慢慢的趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于我们设置的固定速率)。Guava也提供了平滑预热限流(SmoothWarmingUp)来实现这种需求,其可以认为是漏桶算法,但是在某些特殊场景又不太一样。
SmoothWarmingUp创建方式:
RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
permitsPerSecond表示每秒新增的令牌数,warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔。
public void fourTest() throws InterruptedException {
/**
* permitsPerSecond表示每秒新增的令牌数
* warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔。
* unit 时间单位 毫秒
*/
RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);
for(int i = 1; i < 5;i++) {
System.out.println(limiter.acquire());
}
//线程暂停1秒
Thread.sleep(1000L);
for(int i = 1; i < 5;i++) {
System.out.println(limiter.acquire());
}
}
tryAcquire()的用法:
If(limiter.tryAcquire()){ //未请求到limiter则立即返回false
doSomething();
}else{
doSomethingElse();
}
- dubbo线程模式http://dubbo.io/Thread+Model-zh.htm
<dubbo:protocol name="dubbo" port="20881"threadpool="limited" threads="200"/>
来源:oschina
链接:https://my.oschina.net/u/200769/blog/803942