在微服务架构中一个会被经常提及的概念就是“服务的熔断与限流”。而之所以如此频繁的提及这个概念,是因为在高并发场景下,瞬间的流量洪峰很容易超出微服务中各个系统的最大承受能力,从而造成服务的整体不可用。
所以在设计高并发场景下的微服务架构时,要根据服务所能承受的最大流量制定限流策略,从而保证在高并发场景下服务的稳定性。今天的文章就和大家聊一聊关于限流的内容,包括常见的限流算法及目前微服务架构中主要的限流框架。
限流的概念
先介绍下什么是限流?其实日常生活中的限流场景随处可见,例如北京地铁早高峰,每天都是人山人海,如果大家一起蜂拥进站,将很容易造成站内拥堵,所以地铁站一般都会进站口设置像迷宫一样的栅栏,大家转圈圈分批进站,这就是一种典型的限流场景。
那么在微服务中的限流具体是指什么呢?从字面上看,限流限的是流量,在不同场景下流量的定义是不同的,可以是QPS(每秒请求数)、TPS(每秒事务处理数),也可以指纯粹的网络流量(如网卡通过的字节数)等。
但我们通常所说的限流,是指限制达到系统的并发请求数,使得系统能够在自身能力允许的情况下正常处理部分用户的请求,而对超出自身处理能力的用户请求则予以拒绝,从而保证系统的稳定运行。
限流不可避免的会造成用户请求失败或变慢的情况,从而在一定程度上影响用户体验,所以限流策略的制定需要以系统压测的结果为参考,并在用户体验与系统稳定性之间进行平衡取舍。
限流的必要性?
前面我们提到限流的主要目的是为了保证系统的稳定性。在日常的业务中,如果遇到像双十一之类的促销活动,或者遇到爬虫等不正常的流量等情况,用户流量突增,但后端服务的处理能力是有限的,如果不能有效处理突发流量,那么后端服务就很容易被打垮。
可以设想这样一个场景:"某服务单节点可以承受的QPS是1000,该服务共有5个节点,日常情况下服务的QPS为3000"。那么正常情况下该服务毫无压力,根据负载均衡配置3000/5=600,每个节点的日常QPS才600左右。
直到某一天,老板突然搞了一波促销,系统的整体QPS达到了8000。此时每个节点的平均承载QPS为1600,节点A率先扛不住直接挂了,此时集群中还剩下4个节点,每个节点的平均承载QPS将达到2000,于是,剩下的4个节点也一台接一台挂了,整个服务就此雪崩。
而如果我们的系统有限流机制,那么情况将会如何发展呢?
系统整体QPS达到8000,但由于集群整体限流了5000,所以超出集群承受力的那3000个请求将被拒绝,系统则会正常处理5000个用户请求,这是对集群整体限流的情况。而对于各个节点来说,由于我的承受力只是1000QPS,那么超出1000的部分也将被拒绝。这样虽然损失了部分用户请求,但保证了整个系统的稳定性,也给开发运维留下了系统扩容时间。
由此可见,限流对于系统的自我保护是非常重要的存在。那么限流具体怎么做呢?接下来我们总结下常见的限流算法。
常见的限流算法
常见的限流算法主要有:计数器、固定窗口,滑动窗口、漏桶、令牌桶。接下来我们分别介绍下这几种限流算法。
<计数器限流>
计数器限流是最简单粗暴的一种限流算法,例如系统能同时处理100个请求,那么可以在保存一个计数器,处理一个请求,计数器加一,一个请求处理完毕后计数器减一。每次请求进来的时候,先看一眼计数器的值,如果超过阀值则直接拒绝。
在具体实现时,如果该计数器是存在单机内存中,那么就实现了单机限流;而如果存在例如Redis中,集群中的所有节点依次为限流依据,那么就算实现了集群限流算法。
优点:实现简单,单机例如诸如Java的Atomic等原子类就能实现,集群则通过Redis的incr操作就能快速实现。
缺点:计数器限流无法应对突发的流量增长。例如我们允许的阀值是1W,此时计数器的值是0,那么当1W个请求瞬间全部打进来的时候,很可能服务就顶不住了。这是因为流量的缓缓增加和一下子涌入,对系统所产生的压力是不一样的。
况且一般限流都是限制在指定时间间隔内的访问量,而不是全时段服务的总体处理能力,所以计数器限流不太适合高并发场景下的限流实现。
<固定窗口限流>
相对于计数器来说,固定窗口限流是以一段时间窗口内的访问量作为限流的依据,计数器每过一个时间窗口就自动重置。其规则如下:
请求次数小于阀值,允许访问,计数器加1;
请求次数大于阀值,拒绝访问;
本时间窗口过了之后,计数器自动清零;
固定窗口限流虽然看起来挺完美,但是它有固定窗口临界的问题。例如系统每秒允许1000个请求,假如第一个时间窗口的间隔是0~1秒,但在第0.55秒处一下子涌入了1000个请求,过了1秒后计数清零,此时在1.05秒的时候又一下子涌入了1000个请求。
此时虽然在固定时间窗口内的计数没有超过阀值,但在全局看来0.55秒~1.05秒这0.5秒内一下子却涌入了2000个请求,而这对于阀值为1000/s的系统来说是不可承受的。如下图所示:
而为了解决这个问题,衍生出了滑动窗口限流的算法!
<滑动窗口限流>
滑动窗口限流解决了固定窗口临界值的问题,可以保证任意时间窗口内都不会超过限流阀值。相对于固定窗口,滑动窗口除了需要引入计数器外,还需要额外记录时间窗口内每个请求到达的时间点。
以时间窗口为1秒为例,规则如下:
记录每次请求的时间;
统计每次请求的时间向前推1秒这个时间窗口内的请求数,且1秒前的数据可以删除;
统计的请求数小于阀值则记录该请求的时间,并允许通过,反之则拒绝该请求;
虽然看起来很OK,但是滑动窗口也无法解决短时间之内集中流量的冲击。例如每秒限制1000个请求,但是有可能存在前5毫秒的时候,阀值就被打满的情况,理想情况下每10毫秒来100个请求,那么系统对流量的处理就会更加平滑。
但在真实场景中是很难控制请求的频率的。所以为了解决时间窗口类算法的痛点,又出现了漏桶算法。
<漏桶限流>
漏桶算法的基本思想是,流量持续进入漏桶中,底部则定速处理请求,如果流量进入的速率高于底部请求被处理的速率,且当桶中的流量超过桶的大小时,流量就会被溢出。具体如下图所示:
漏桶算法的特点是宽进严出,无论请求的速率有多大,底部的处理速度都匀速进行。这种算法的特点有点类似于消息队列的处理机制,一般来说漏桶算法也是由队列来实现的。
但漏桶算法的这种特点,实际上即是它的优点也是缺点。有时候面对突发流量,我们往往会希望在保持系统稳定的同时,能更快地处理用户请求以提升用户体验,而不是按部就班的佛系工作。在这种情况下又出现了令牌桶这样的限流算法,它在应对突发流量时,可以比漏桶算法更加激进。
<令牌桶限流>
令牌桶与漏桶的原理类似,只是漏桶是底部匀速处理,而令牌桶则是定速的向桶里塞入令牌,然后请求只有拿到了令牌才会被服务器处理。具体规则如下:
定速的向桶中放入令牌;
令牌数量超过桶的限制,则丢弃;
请求来了先向桶中索取令牌,索取成功则通过被处理,否则拒绝;
可以看出令牌桶在应对突发流量时,不会想漏桶那样匀速的处理,而是在短时间内请求可以同时取走桶中的令牌,并及时的被服务器处理。所以在应对突发流量的场景下,令牌桶表现更强。
限流算法总结
经过上述的描述,好像漏桶、令牌桶比时间窗口类算法好多了,那么时间窗口类算法是不是就没啥用了呢?
其实并不是,虽然漏桶、令牌桶对比时间窗口类算法对流量的整形效果更好,但是它们也有各自的缺点,例如令牌桶,假如系统上线时没有预热,那么可能会出现由于此时桶中还没有令牌,而导致请求被误杀的情况;而漏桶中由于请求是暂存在桶中的,所以请求什么时候能被处理,则是有延时的,这并不符合互联网业务低延时的要求。
所以令牌桶、漏桶算法更适合阻塞式限流的场景,即后台任务类的限流。而基于时间窗口的限流则更适合互联网实施业务限流的场景,即能处理快速处理,不能处理及时响应调用方,避免请求出现过长的等待时间。
微服务限流组件
如果你有兴趣实际上也是可以自己实现一个限流组件的,只不过这种轮子已经早有人造好了。目前市面上比较流行的限流组件主要有:Google Guava提供的限流工具类“RateLimiter”、阿里开源的Sentinel。
其中Google Guava提供的限流工具类“RateLimiter”,是基于令牌桶实现的,并且扩展了算法,支持了预热功能。而阿里的Sentinel中的匀速限流策略,就是采用了漏桶算法。