限流
Salted Fish 1991/6/26 面试架构
# 什么是限流
每个系统都有服务的上线,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。
# 三种限流的算法
令牌桶(Token Bucket)、漏桶(leaky bucket)和计数器算法是最常用的三种限流的算法:
# 1、令牌桶方式(Token Bucket)
- Guava RateLimiter
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。先有一个木桶,系统按照固定速度,往桶里加入Token,如果桶已经满了就不再添加。当有请求到来时,会各自拿走一个Token,取到Token 才能继续进行请求处理,没有Token 就拒绝服务。

这里如果一段时间没有请求时,桶内就会积累一些Token,下次一旦有突发流量,只要Token足够,也能一次处理,所以令牌桶算法的特点是允许突发流量。
# 2、漏桶
水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。
因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.
# 3、计数器
- 计数器限流算法也是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。也是最简单粗暴的算法。
- 采用AtomicInteger
- 使用AomicInteger来进行统计当前正在并发执行的次数,如果超过域值就简单粗暴的直接响应给用户,说明系统繁忙,请稍后再试或其它跟业务相关的信息。
- 弊端:使用 AomicInteger 简单粗暴超过域值就拒绝请求,可能只是瞬时的请求量高,也会拒绝请求。
- 采用令牌Semaphore:
- 使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。
- 相对Atomic优点:如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的。
- 采用ThreadPoolExecutor java线程池:
- 固定线程池大小,超出固定先线程池和最大的线程数,拒绝线程请求;
- 采用AtomicInteger
# 限流令牌桶和漏桶对比
- 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
- 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
- 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
- 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
- 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
- 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
# 在单机情况下如何实现限流?
应用级限流方式只是单应用内的请求限流,不能进行全局限流。
- 限流总资源数
- 限流总并发/连接/请求数
- 限流某个接口的总并发/请求数
- 限流某个接口的时间窗请求数
- 平滑限流某个接口的请求数
- Guava RateLimiter
# 在分布式环境下如何实现限流?
我们需要分布式限流和接入层限流来进行全局限流。
- redis+lua实现中的lua脚本
- 使用Nginx+Lua实现的Lua脚本