前言
在高并发场景下,为避免系统被压垮,经常会采用限流的方式来控制流量,这对系统的稳定性和可靠性至关重要,限流有很多成熟的解决方案,例如Hystrix,今天不聊Hystrix,而是从限流的基本算法进行刨析,理解各算法的优劣和使用场景,常用的限流算法主要有以下几种:
- 计数器法
- 滑动窗口计数法
- 漏桶算法
- 令牌桶算法
计数器法
这种算法最简单,把时间划分为窗口,比如1分钟一个窗口,在一段窗口内只接受固定数量的请求,比如60个,到达窗口结束时重新开一个新窗口
这种方法最简单,当然也有个明显的问题,比如如下图1min~2min时间段内,如果之前都没请求只在最后10ms发生大量请求,此时通过限流机制确实只收了60个,但由于马上进入第二个窗口,又开始重新计数,马上又进入大量请求,相当与在一窗口和二窗口切换这段时间内进入了双倍120个请求,显然瞬间量超过预期
滑动窗口计数法
基于上述问题,滑动窗口计数法在计数法之上进行了改进,为了避免上述情况发生,时间窗口不再是固定的时间段,而是根据时间的流转动态的调整窗口,增加更小的粒度来拆分窗口,且窗口随着时间的移动而移动,说法略微抽象,举个例子,比如还是控制1分钟60个请求,再把一分钟划分为60小格,每一格一秒,那么每过一秒就可以把窗口向前推一秒,即窗口范围永远是1分钟前到当前时间
如下图同一场景下,01:59时进入60个请求,假设当前时间到了02:01,进来大量请求,此时实际的窗口为01:01~02:01,计数器判断出窗口内已大到最大请求量60所以直接触发了限流,也就是说直到02:59秒后采能接收新的请求
漏桶算法
这也是一个经典算法,它的工作原理类似于一个装满水的桶,水以恒定的速度从桶底的孔中流出。当有新的水流(数据包)进入桶时,如果桶还没有满,则水流会被接纳并逐渐流出;如果桶已经满了,则多余的水流会被溢出,从而达到限流的目的
这种算法明显的特点是可以严格控制固定时间接收请求的次数,缺点也很明显:太严格了,想想一个一般系统的并发处理量只是一个平均值,系统的并发处理能力受硬件网络等各种情况影响,哪有那么准确的数
使用漏桶算法,你如果把流出速率控制到极限比如每秒1000个,某时系统突然来了1000个请求,系统能勉强能正常处理,但紧接着下一个时间节点又来了1000个请求你就吃不消了,因为上1000个可能还没处理完,然后下一秒又来1000个结果系统慢慢就崩溃了
那怎么办,设小点,比如设500,那么假如突然在某个时间点来了1000个请求,前后并没有其它大量请求(比如并发场景),此时明明系统可以处理这1000个请求,却白白丢弃了500个。。。
所以漏桶算法适合比较有规律的请求数或处理能力,对突发流量场景非常不友好
令牌桶算法
令牌桶算法实现如下
- 令牌生成:系统以一个恒定的速率向桶中添加令牌。
- 桶的容量:桶有一个固定的容量,用于存储令牌。
- 数据包处理:当数据包到达时,需要从桶中获取一个令牌。如果有可用的令牌,则数据包被处理;如果没有足够的令牌,则数据包被延迟或丢弃。
令牌桶算法就太适合上述场景了,有一个最大令牌数应对突发流量,有一个令牌添加速度来控制流量,在上述场景下,可以把令牌数设为1000,这样在突发1000请求时由于之前没有大量请求积累了很多令牌,因此1000个请求大部分都被接收,而令牌耗尽后有一定的生成频率,又可以防止下一秒又来1000个干崩系统
其实令牌算法更符合系统的实际处理能力,即可以再空闲状态下瞬间处理大量请求,但处理这些请求的需要一段时间,所以不能长期处理这么大量的请求
就好比线程池有核心线程和最大线程,就是为了不浪费资源同时又能处理瞬间的高并发,令牌桶的涉及有异曲同工之妙,而漏桶算法的容量则是为了避免消费积压,实际关键的数据只有流出速率
总结
以上就是常见的限流算法,要说哪个好,只能说没有最合适的技术,只有最合适的场景,结合实际场景选择才是最理智的,而每一种结合场景都是个比较难的话题,而每种算法只是尽量合理不能做到绝对合理
扩展
我仔细想了下现实世界是如何实现限流的,马上就想到了一个最适当的场景:医院看病,医院看病时医院当然也要限流,因为每个诊室外面占不了那么多人排队啊,所以都是在外面等着叫号,叫完号再去里面排队,而他是怎么限流的? 他没有固定的算法(就像上面说的每个算法都并不玩们),一般是有个护士看门口排队的情况,如果人少了就放一些号进去排队,这是一种根据实际处理情况决定如何控制流量的方式,我想起软件界的一个词:背压,那如果限流是根据实际处理能力来实时决定如何限流,是不是更为合理,但这样做代价很大,现实中需要个人力一直观察,代码里就需要一个很复杂的逻辑实时监控处理进度