一、 闲言
每个系统都有服务的上线,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。
限流主要限制请求流量,保证当前服务、依赖服务不会被大流量彻底压死。
举个例子:电商网站大促期间,如果服务能力实在有限,可以对社区功能进行降级,全力保促销、交易流程;如果交易系统还是无法承担流量的压力,通过限制并发可以让部分用户无法下单,因为部分人不能用总比让整个网站挂掉好。这里仅仅是举一个例子,并不代表建议大家这样做。
做过高并发开发,或者开发过营销活动的程序员,可能都会遇到过限流、降级的场景,所以准备聊一聊限流的实现方案,降级的方案以后专门写一期。
当然我们的目标还是要尽可能满足业务的需要,承担起更多的服务,这里讨论的所有方案都是无奈的防御性方案。
二、 限流方案
一般线上服务,都会多机部署,所以限流分单机限流和集群限流两种。
1. 访问量限流
1) 原理
实现的要旨是:确定方法的最大访问量MAX,每次进入方法前计数器+1,将结果和最大并发量MAX比较,如果大于等于MAX,则直接返回;如果小于MAX,则继续执行。
这种方式基本上都需要对时间进行分片,即多少时间内最大访问量不能超过多少。
² 示例代码:
String key= "method_limit_in_min_"+System.currentTimeMillis()/(1000*60);
int count= cacheManager.inc(key) ;
// 如果请求量大于啊最大限制,直接返回
if(count > 1000 ){
return ;
}
//执行业务逻辑
2) 优缺点
优点:逻辑简单,实现简单;适用于绝大多数场景;通过时间切片细分,可以很简单的控制切分粒度。
缺点:因为超过最大访问量会直接拒绝请求,这种方式过于暴力,所以切分时间片时,如果选择的时间短不合适,对用户的体验会很差。
试想如果按照每小时分片,如果在第一分钟内,流量剧增,超过我们设置的预设值,那么后面59分钟的服务都不可用。幸运的时,绝大多时候,没有人会愚蠢的将时间设置为60分或者更长。
2. 并发量限流
1) 原理
这种比较好理解,也比较好实现。实现的要旨是:确定方法的最大并发量MAX,每次进入方法前计数器+1,将结果和最大并发量MAX比较,如果大于等于MAX,则直接返回;如果小于MAX,则继续执行;退出方法后计数器-1;
扩展:控制单位之间内的最大并发量。在上述的基础上,对计数器的key通过时间进行分片。
2) 示例:
集群环境中,一个方法每分钟内的最大并发量为1000次,这种场景下可以将时间截取到分钟,以这个字符串作为inc、desc的key的一部分。即通过将key切分时间片,来实现单位时间内的调用量控制。
代码:
String key= "method_concurrent_in_min_"+System.currentTimeMillis()/(1000*60);
try{
int concurrent= cacheManager.inc(key) ;
if( concurrent> 100 ){
return ;
}
// 执行业务逻辑
} finally {
cacheManager.desc(key);
}
3. 访问量限流、并发量限流分析
访问量限流的逻辑简单,实现简单;适用于绝大多数场景。并发量限流一般用于对于服务资源有严格限制的场景。指的注意的是:在做分布式开发的时候,经常会有多个服务都依赖同一个底层服务的情况,此时要注意服务之间的相互影响。如下图所示
如果A的请求量过大,将服务D压垮后,将会直接影响B服务。所以设置阀值的时候,一定和底层服务提供者沟通清楚服务能力,要留足余量。另外:访问量限流、并发量限流,其实都是限制请求量,对于流量尖刺(从低流量急剧升到高流量)没有任何防御能力;对于不能承担流量暴增的系统,不适合使用这两种方式。
实现要点:
l 注意线程安全,建议使用AtomicInteger、AtomicLong这些线程安全的类。
l 集群下可以使用分布式缓存的inc、desc方法来实现。一般分布式缓存都有此功能,例如:tair、redis。
l 计数器只是一种实现方式,也可以使用Semaphore等来控制。
4. 漏桶(Leaky Bucket)算法限流
漏桶算法是网络中流量整形的常用算法之一,能够强行限制请求调用的速率。
1) 原理
它有点像我们生活中用到的漏斗,液体倒进去以后,总是从下端的小口中以固定速率流出,漏桶算法也类似,不管突然流量有多大,漏桶都保证了流量的常速率输出,也可以类比于调用量,比如,不管服务调用多么不稳定,我们只固定进行服务输出。既然是一个桶,那就肯定有容量,由于调用的消费速率已经固定,那么当桶的容量堆满了,则只能丢弃了。
漏桶算法如下图:
2) 实现
看到有容量限制,超出处理能力以后,新的请求就要丢弃,估计丢弃,估计有些小伙伴就立即想到线程池了。其实完全可以使用类似于线程池或者队列来实现,例如:创建一个集合(队列)来作为桶,将请求都放到这个集合中,另外开一个线程池,定期从队列中取出若干数据并发处理。
3) 适用场景
漏桶算法严格限制了系统的吞吐量,可以预防突刺型的流量。漏桶算法比较适应于下游服务能力有限的场景中。由于它对服务吞吐量有着严格固定的限制,如果处理不当,进行限流的地方很容易成为性能瓶颈。其实对于大多数分布式开发场景,大部分场景中,下游的服务能力可以通过水平扩展、缓存等手段来解决。
5. 令牌桶(Token Bucket)算法限流
令牌桶算法能够在限制调用的平均速率的同时还允许某种程度的突发调用。
1) 原理
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
2) 实现
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流。
3) 示例代码
long permitsPerSecond = 2 ;
RateLimiter limiter =RateLimiter.create(permitsPerSecond) ;
for( int i= 0 ; i<5; i++ ){
int tokenCount = 1 ;
limiter.acquire( tokenCount) ;
// 业务逻辑
}
6. 总结
以上的四种方案,每一种都有其适用场景和局限性,需要根据业务进行选择。而很多时候可能是需要将他们组合使用。访问量限流、并发限流在集群、单机场景中,实现起来都很容易,而且使用场景也比较广泛。后两种有一些比较成熟的jar可以帮助我们实现,使用起来也比较简单。