限流算法
令牌桶算法
漏桶算法
应用级限流
限制总并发数/连接/请求数
限制接口的总并发/请求数
限流接口每秒的请求数
平滑限流接口的请求数
平滑突发限流(SmoothBursty)
平滑预热限流(SmoothWarmingUp)
在开发高并发的系统时,有很多手段来保护系统,如缓存、降级和限流等。缓存可以提升系统的访问速度,降级可以暂时屏蔽掉非核心业务,使得核心业务不受影响。限流的目的通过对并发访问进行限速,一旦达到一定的速率就可以拒绝服务(定向到错误页或告知资源没有了)、排队等待(如秒杀、评论、下单等)、降级(直接返回兜底数据、如商品库存默认有货)。
常见的限流方式有:限制总并发数(数据库连接池、线程池)、限制瞬时并发数(如Nginx的limit_conn模块)、限制时间窗口的平均速率(如Guava的RateLimiter、Nginx的limit_req模块)、限制远程接口的调用速率、限制MQ的消费速率等。从应用的层面上来讲,又可以分为:接入层限流、应用层限流和分布式限流等。
限流算法
令牌桶算法
令牌桶算法是一个存放固定容量令牌的容器,按照固定速率添加令牌,算法描述如下:
假设限制2r/s,则按照500ms的固定速率添加令牌。
桶的总容量为N,当达到总容量时,新添加的令牌则被丢弃或拒绝。
当一个n个字节大小的数据包到达,则从桶中删除n个令牌,然后处理数据包。
如果桶中的令牌不足n个,则不会删除令牌,但是数据包将会被限流。
漏桶算法
漏桶可以用于流量整型和流量控制,算法描述如下:
一个固定容量的漏桶,会按照固定的速率流出水滴。
如果桶中无水,则不需要流出水滴。
可以以任意速率流入水滴。
如果流入的水滴超出了桶容量,则新添加的则会被丢弃。
综上可以看出,令牌桶允许一定程度的突发请求(有令牌就可以处理),漏桶的主要目的是来平滑流入的速率。
应用级限流
限制总并发数/连接/请求数
对于一个应用来说,总会有一个TPS/QPS的阀值,如果超过了阀值,则系统就会变得非常慢跟甚至无法响应。因此需要对系统进行过载保护,避免大量请求击垮系统。
如Tomcat的Connector中的以下几个参数:
acceptCount:如果Tomcat的线程都忙于响应,新来的连接将会进入队列,如果超出队列大小,则会拒绝连接。
maxConnections:瞬时最大连接数,超出的会排队等待。
maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于线程数,则会引起响应变慢甚至会僵死。
类似于Tomcat配置最大连接数等参数,Redis和MySQL也有相关的配置。
限制接口的总并发/请求数
在Java中可以用线程安全的AtomicLong或者Semaphore进行处理,如下使用了AtomicLong进行简单的统计:
try {
if (atomic.incrementAndSet() > 阀值) {
// 拒绝请求
}
// 处理请求
} finally {
atomic.decrementAndGet();
}
这种方式实现起来比较简单暴力,没有平滑处理,这需要根据实际情况选择使用。
限流接口每秒的请求数
限制每秒的请求数,可以使用Guava的Cache来存储计数器,设置过期时间为2S(保证能记录1S内的计数)。下面代码使用当前时间戳的秒数作为key进行统计,这种限流的方式也比较简单。
LoadingCache
CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader
@Override
public AtomicLong load(Long seconds) throws Exception {
return new AtomicLong(0);
}
});
long limit = 1000;
while (true) {
//得到当前秒
long currentSeconds = System.currentTimeMillis() / 1000;
if (counter.get(currentSeconds).incrementAndGet() > limit) {
System.out.println("限流了:" + currentSeconds);
continue;
}
//业务处理
}
上面介绍的2中限流方案都是对于单机接口的限流,当系统进行多机部署时,就无法实现整体对外功能的限流了。当然这也看具体的应用场景,如果平行的应用服务器需要共享限流阀值指标,可以使用Redis作为共享的计数器。
平滑限流接口的请求数
Guava的RateLimiter提供的令牌桶算法可以用于平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
平滑突发限流(SmoothBursty)
平滑突发限流顾名思义,就是允许突发的流量进入,后面再慢慢的平稳限流。下面给出几个Demo
# 创建了容量为5的桶,并且每秒新增5个令牌,即每200ms新增一个令牌
RateLimiter limiter = RateLimiter.create(5);
while (true) {
// 获取令牌(可以指定一次获取的个数),获取后可以执行后续的业务逻辑
System.out.println(limiter.acquire());
}
上面代码执行结果如下所示:
0.0
0.188216
0.191938
0.199089
0.19724
0.19997
上面while循环中执行的limiter.acquire(),当没有令牌时,此方法会阻塞。实际应用当中应当使用tryAcquire()方法,如果获取不到就直接执行拒绝服务。
下面在介绍一下中途休眠的场景:
RateLimiter limiter = RateLimiter.create(2);
System.out.println(limiter.acquire());
Thread.sleep(1500L);
while (true) {
System.out.println(limiter.acquire());
}
上面代码执行结果如下:
0.0
0.0
0.0
0.0
0.499794
0.492334
从上面结果可以看出,当线程休眠时,会囤积令牌,以给后续的acquire()使用。但是上面的代码只能囤积1S的令牌(也就是2个),当睡眠时间超过1.5S时,执行结果还是相同的。
平滑预热限流(SmoothWarmingUp)
平滑突发限流有可能瞬间带来了很大的流量,如果系统扛不住的话,很容易造成系统挂掉。这时候,平滑预热限流便可以解决这个问题。创建方式:
// permitsPerSecond表示每秒钟新增的令牌数,warmupPeriod表示从冷启动速率过渡到平均速率所需要的时间间隔
RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);
for (int i = 1; i < 5; i++) {
System.out.println(limiter.acquire());
}
Thread.sleep(1000L);
for (int i = 1; i < 50; i++) {
System.out.println(limiter.acquire());
}
执行结果如下:
0.0
0.513566
0.353789
0.215167
0.0
0.519854
0.359071
0.219118
0.197874
0.197322
0.197083
0.196838
上面结果可以看出来,平滑预热限流的耗时是慢慢趋近平均值的。
参考:《亿级流量网站架构核心技术》
链接:http://moguhu.com/article/detail?articleId=73
基于分布式环境下限流系统的设计
前提
业务背景
就拿前些天的双十一的 “抢券活动” 来说,一般是设置整点开始抢的,你想想,淘宝的用户群体非常大,可以达到亿级别,而服务接口每秒能处理的量是有限的,那么这个时候问题就会出现,我们如何通过程序来控制用户抢券呢,于是就必须加上这个限流功能了。
生产环境
1、服务接口所能提供的服务上限(limit)假如是 500次/s
2、用户请求接口的次数未知,QPS可能达到 800次/s,1000次/s,或者更高
3、当服务接口的访问频率超过 500次/s,超过的量将拒绝服务,多出的信息将会丢失
4、线上环境是多节点部署的,但是调用的是同一个服务接口
于是,为了保证服务的可用性,就要对服务接口调用的速率进行限制(接口限流)。
什么是限流?
限流是对系统的出入流量进行控制,防止大流量出入,导致资源不足,系统不稳定。
限流系统是对资源访问的控制组件,控制主要的两个功能:限流策略和熔断策略,对于熔断策略,不同的系统有不同的熔断策略诉求,有的系统希望直接拒绝、有的系统希望排队等待、有的系统希望服务降级、有的系统会定制自己的熔断策略,这里只针对限流策略这个功能做详细的设计。
限流算法
1、限制瞬时并发数
Guava RateLimiter 提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
2、限制某个接口的时间窗最大请求数
即一个时间窗口内的请求数,如想限制某个接口/服务每秒/每分钟/每天的请求数/调用量。如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是怕因为更新量比较大将基础服务打挂,这时我们要对每秒/每分钟的调用量进行限速;一种实现方式如下所示:
LoadingCache
CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(newCacheLoader() {
@Override
publicAtomicLongload(Long seconds)throwsException{
returnnewAtomicLong(0);
}
});
longlimit =1000;
while(true) {
//得到当前秒
longcurrentSeconds = System.currentTimeMillis() /1000;
if(counter.get(currentSeconds).incrementAndGet() > limit) {
System.out.println("限流了:"+ currentSeconds);
continue;
}
//业务处理
}
使用Guava的Cache来存储计数器,过期时间设置为2秒(保证1秒内的计数器是有的),然后我们获取当前时间戳然后取秒数来作为KEY进行计数统计和限流,这种方式也是简单粗暴,刚才说的场景够用了。
3、令牌桶
算法描述:
假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中
假设桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃
当流量以速率v进入,从桶中以速率v取令牌,拿到令牌的流量通过,拿不到令牌流量不通过,执行熔断逻辑
属性
长期来看,符合流量的速率是受到令牌添加速率的影响,被稳定为:r
因为令牌桶有一定的存储量,可以抵挡一定的流量突发情况
M是以字节/秒为单位的最大可能传输速率。 M>r
T max = b/(M-r) 承受最大传输速率的时间
B max = T max * M 承受最大传输速率的时间内传输的流量
优点:流量比较平滑,并且可以抵挡一定的流量突发情况
4、Google guava 提供的工具库中 RateLimiter 类(内部也是采用令牌桶算法实现)
最快的方式是使用 RateLimit 类,但是这仅限制在单节点,如果是分布式系统,每个节点的 QPS 是一样的,请求量到服务接口那的话就是 QPS * 节点数 了。所以这种方案在分布式的情况下不适用!
5、基于 Redis 实现,存储两个 key,一个用于计时,一个用于计数。请求每调用一次,计数器增加 1,若在计时器时间内计数器未超过阈值,则可以处理任务。
这种能够很好地解决了分布式环境下多实例所导致的并发问题。因为使用redis设置的计时器和计数器均是全局唯一的,不管多少个节点,它们使用的都是同样的计时器和计数器,因此可以做到非常精准的流控。
代码就不公布了,毕竟涉及公司隐私了。
最后
参考文章:
基于Redis的限流系统的设计
感兴趣的可以看看别人的代码是怎么写的:https://github.com/wukq/rate-limiter
转载请注明文章地址为:http://www.54tianzhisheng.cn/2017/11/18/flow-control/
流量调整和限流技术
在早期的计算机领域,限流技术(time limiting)被用作控制网络接口收发通信数据的速率。 可以用来优化性能,减少延迟和提高带宽等。 现在在互联网领域,也借鉴了这个概念, 用来为服务控制请求的速率, 如果双十一的限流, 12306的抢票等。 即使在细粒度的软件架构中,也有类似的概念。
两种常用算法
令牌桶(Token Bucket)和漏桶(leaky bucket)是 最常用的两种限流的算法。
漏桶算法
它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。 漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 用说人话的讲:
漏桶算法思路很简单,水(数据或者请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使某一个单独的流突发到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法可以结合起来为网络流量提供更大的控制。
令牌桶算法
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 令牌桶的另外一个好处是可以方便的改变速度。 一旦需要提高速率,则按需提高放入桶中的令牌的速率。 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量, 比如华为的专利"采用令牌漏桶进行报文限流的方法"(CN 1536815 A),提供了一种动态计算可用令牌数的方法, 相比其它定时增加令牌的方法, 它只在收到一个报文后,计算该报文与前一报文到来的时间间隔内向令牌漏桶内注入的令牌数, 并计算判断桶内的令牌数是否满足传送该报文的要求。
从最终用户访问安全的角度看,设想有人想暴力碰撞网站的用户密码;或者有人攻击某个很耗费资源的接口;或者有人想从某个接口大量抓取数据。大部分人都知道应该增加 Rate limiting,做请求频率限制。从安全角度,这个可能也是大部分能想到,但不一定去做的薄弱环节。
从整个架构的稳定性角度看,一般 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 调用等场景。
ASP.NET Web API rate limiter for IIS and Owin hosting
基于 Redis 功能的实现
这个在 Redis 官方文档有非常详细的实现。一般适用于所有类型的应用,比如 PHP、Python 等等。Redis 的实现方式可以支持分布式服务的访问频率的集中控制。Redis 的频率限制实现方式还适用于在应用中无法状态保存状态的场景。
参见:Redis INCR rate limiter
网上有众多关于这方面的文章,这里列出了本文参考的一些文档。
http://en.wikipedia.org/wiki/Token_bucket
http://en.wikipedia.org/wiki/Leaky_bucket
http://www.cnblogs.com/LBSer/p/4083131.html
http://www.cnblogs.com/mushroom/p/4659200.html
5.电商课题I:集群环境下业务限流