限流
分布式系统中,由于接口API无法控制上游调用方的行为,因此当瞬时请求量突增时,会导致服务器占用过多资源,发生响应速度降低、超时、乃至宕机,甚至引发雪崩造成整个系统不可用。
限流,Rate Limiting,就是对API的请求量进行限制,对于超出限制部分的请求作出快速拒绝、快速失败、丢弃处理,以保证本服务以及下游资源系统的稳定。
哪些原因会带来瞬时请求量突增?
1,热点业务、突发热点数据带来的激增。例如微博热搜的爆点。
2,上游系统的bug导致。
3,恶意的攻击流量。
实现限流的方法很多,一句话来讲,就是限制每秒钟内API可处理的请求量。常见的有以下几种算法:固定窗口计数法、滑动窗口计数法、漏桶算法、令牌桶算法。
固定窗口
思路
- 将时间划分为固定的窗口,例如2s
- 当请求进来,在此窗口内的计数器加1
- 在此窗口内的计数器达到了最大请求值,直接抛弃
- 窗口过期,计数器清零
伪代码(貌似不对哦)
local max = 1000
local duration = 2
local now = current.time() //以时间戳来当key
// 清除前一个窗口计数器
redis.call("del", now-duration)
// 获得当前窗口计数器
count = redis.call("get", now)
if count >= max
return false
else
redis.call("incr", now, 1)
return true
滑动窗口
滑动窗口和固定窗口的区别是,如果以2秒为例,
思路
-
定义窗口大小,以2秒为窗口
-
每进来一个请求都插入到redis的有序数列中
-
删除 [当前时间-窗口,当前时间] 这个区间外的元素
-
以当前时间为最后的最小区间(认为是最后一秒)
-
统计**[当前时间-窗口,当前时间]** 这个区间内的元素个数,大于就抛弃
伪代码
package ratelimiter
import (
"sync"
"time"
)
var limit = NewSlidingWindow()
type SlidingWindow struct {
maxRequest int // 窗口下的最大请求数
window int // 窗口大小
timeSlice int // 时间间隔
LastTimeSeek int // 最后一次更新的时间余数
countData map[int]int // 每个时间间隔的统计数量
mux sync.Mutex // 线程安全锁
}
// SlidingWindow 滑动窗口
func NewSlidingWindow() *SlidingWindow {
return &SlidingWindow{
window: 60,
timeSlice: 1,
LastTimeSeek: 0,
countData: make(map[int]int),
}
}
// Entry 入口
func (s *SlidingWindow) Entry() {
if s.count() {
// todo::success
} else {
// todo::reject request
}
}
func (s *SlidingWindow) count() bool {
// 统计需要加锁,并发做到线程安全
s.mux.Lock()
defer s.mux.Unlock()
//当前时间戳
now := time.Now().Unix()
// 获取余数,当前请求保存到哪个slice节点
seek := int(now) % s.window
// 判断跟上次余数是否一样,如果不一样说明要把上一轮的统计清除
if seek != s.LastTimeSeek {
s.countData[seek] = 1
} else {
s.countData[seek]++
}
var res int
for _, v := range s.countData {
res = res + v
}
if res > s.maxRequest {
return false
}
return true
}
漏桶算法
漏斗桶(Leaky Bucket)算法是一种用于流量控制的算法,它确保流入系统的请求以固定的速率被处理,不会超过系统的处理能力
我们使用Redis来存储漏斗桶的参数,
- 包括容量(capacity)
- 漏水速率(rate)
- 上次漏水时间(last_leak_time)
- 当前的水位(water_level): 当前请求量
在每次请求到达时,我们根据时间的流逝来计算漏水的数量,然后更新漏斗的状态。如果漏斗已满,请求就会被限制。
伪代码
// 初始化漏斗桶
function initLeakyBucket($bucketKey, $capacity, $rate) {
redisSet($bucketKey . ":capacity", $capacity);
redisSet($bucketKey . ":rate", $rate);
redisSet($bucketKey . ":last_leak_time", time());
redisSet($bucketKey . ":water_level", 0);
}
// 处理请求
function processRequest($bucketKey) {
// 获取漏斗桶参数
$capacity = redisGet($bucketKey . ":capacity");
$rate = redisGet($bucketKey . ":rate");
$lastLeakTime = redisGet($bucketKey . ":last_leak_time");
$waterLevel = redisGet($bucketKey . ":water_level");
// 获取当前时间戳
$currentTimestamp = time();
// 计算经过的时间
$timePassed = $currentTimestamp - $lastLeakTime;
// 计算漏水的数量 , 时间 * 流出速率 = 最后一次请求 到 当前请求的固定流出量
$leakAmount = $timePassed * $rate;
// 更新漏斗的状态 桶内当前水位 - 最后一次请求 到 当前请求的固定流出量 + 1(本次请求进来,肯定是1) = 本次请求进来后的水位
$newWaterLevel = max(0, $waterLevel - $leakAmount) + 1;
// 如果漏斗已满,则限制请求 水位不大于容量就可以访问,否则反之
if ($newWaterLevel > $capacity) {
return false;
}
// 更新漏斗状态
redisSet($bucketKey . ":last_leak_time", $currentTimestamp);
redisSet($bucketKey . ":water_level", $newWaterLevel);
return true;
}
令牌桶算法
在令牌桶算法中,不需要使用过期时间戳来管理令牌,因为令牌桶的本质是通过固定速率向桶中添加令牌,而不是根据令牌的到达时间来处理。我之前的回答中的示例代码可能引入了不必要的复杂性。
我们使用Redis来存储漏斗桶的参数,
- 包括容量(capacity)
- 令牌生产速率(rate)
- 上次发放令牌的时间(last_leak_time)
在每次请求到达时,我们根据时间的流逝来计算漏水的数量,然后更新漏斗的状态。如果漏斗已满,请求就会被限制。
代码
// 初始化令牌桶
function initTokenBucket($bucketKey, $capacity, $rate) {
redisSet($bucketKey . ":capacity", $capacity);
redisSet($bucketKey . ":rate", $rate);
redisSet($bucketKey . ":tokens", $capacity);
redisSet($bucketKey . ":tokens", 0);
}
// 处理请求
function processRequest($bucketKey) {
// 获取令牌桶参数
$capacity = redisGet($bucketKey . ":capacity");
$rate = redisGet($bucketKey . ":rate");
$tokens = redisGet($bucketKey . ":tokens");
$last_update = redisGet($bucketKey . ":last_update");
// 获取当前时间戳
$currentTimestamp = time();
// 计算经过的时间
$timePassed = $currentTimestamp - redisGet($bucketKey . ":last_update");
// 计算新的令牌数量,但不超过容量。 原桶内token数量 + 水流量 = 此时应该有的数量,如果超出了桶最大容纳量,则取桶最大值
$newTokens = min($capacity, $tokens + $timePassed * $rate);
// 更新令牌数量
redisSet($bucketKey . ":tokens", $newTokens);
redisSet($bucketKey . ":last_update", $currentTimestamp);
if ($newTokens >= 1) {
// 如果有足够的令牌,则允许请求
redisSet($bucketKey . ":tokens", $newTokens - 1);
return true;
} else {
// 令牌桶为空,请求被限制
return false;
}
}
包推荐
- symfony:Rate Limiter
- laravel:Rate Limiting