限流设计
限流的目的是在流量过大对数据库或其他下游服务造成较大影响时的不得已的手段。
限流策略
限流行为如下:
- 拒绝请求。好的限流系统在遭遇流量暴增时,会统计当前哪个客户端的请求最多,把请求多的客户端拒绝掉。
- 服务降级。临时关闭某些功能,将CPU、内存等资源让给重要的功能;或者不返回全部数据,只返回部分数据。
- 特权请求。比如把资源让给vip用户。
- 延时处理。用队列来缓冲请求,队列满了就拒绝访问了,用于削峰。
- 弹性伸缩。自动化运维的方式对相应的服务做自动化伸缩。这个系统需要能感知到目前最忙的top5的服务是哪几个。
限流的实现方式
- 计数器。
计数器是最简单的限流方式。
维护一个计数器counter,来请求就+1,处理完就-1,如果counter大于设定的限流阈值时,就拒绝请求。
- 队列算法
- 漏斗算法 Leaky bucket
- 令牌桶算法 Token Bucket
- 基于响应时间的动态限流
队列算法
维护一个队列存放请求,当请求来时先判断队列是否已满,满了则拒绝请求,否则放入队列,业务进行处理。
升级
两个优先级队列,先处理高优先级队列的请求, 当高优先级队列空了再处理低优先级队列。
但是,这种情况下可能存在低优先级队列长时间得不到处理。
为了避免这种情况,可再升级——带权重的队列。
带权重队列,假设三个队列,权重分别为3,2,1,那么在处理流程上,先处理权重为3的队列,处理3个or3的倍数个请求,再处理权重为2的队列,最后处理权重为1的队列,如此往复。
漏斗算法
令牌桶算法
关于令牌桶,需要一个中间人,在一个桶内,按照一定速率放入一些token,然后处理请求时,要拿到token才能处理,拿不到则不能处理。
基于响应时间的动态限流
上面的算法需要一个限流阈值,这个阈值需要通过性能压测得到。
然而这个值很难确定,原因是:
- 实际情况下,业务服务依赖数据库,所以,不同的用户请求的数据集也是不一样的。而且数据库的数据也是在增加的,性能可能会存在差异,所以,很难给出一个一成不变的值。
- 不同的API的性能也不同,为每个API配置一个阈值是很难管理的。
- 在自动化伸缩的运维下,动态的配置阈值也是很困难的。
基于上述这些原因,我们限流的值是很难被静态地设置成恒定的一个值。
我们想使用一种动态限流的方式。这种方式,不再设定一个特定的流控值,而是能够动态地感知系统的压力来自动化地限流。
这方面设计的典范是tcp的拥塞控制的算法。TCP使用RTT - Round Trip Time来探测网络的延迟和性能,从而设定相应的滑动窗口的大小,使得发送的速率和网络的相匹配。
这里,我们要记录接口每次的响应时间,然后在一个时区内(比如10秒)的请求计算一个响应时间的P90或P99值,也就是把过去10秒内的请求的响应时间排个序,然后看90%和99%的位置是多少。
这样,我们就知道有多少请求大于某个响应时间。如果这个P90或P99超过我们设定的阈值,就自动限流。
设计要点:
计算一定时间内的P90和P99。因为要进行排序,请求量大的情况下会耗费内存和CPU。解决方案有两种:
- 一种是不记录所有请求,只采样。
- 另一种是使用蓄水池的近似算法。
- 动态流控要像TCP那样,记录一个当前的QPS。如果发现P90/P99响应太慢,可以把QPS减半,像TCP慢启动的方式。