1. 分布式限流介绍
1.1 限流的定义
限流就是某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求,但是在真正的场景里,一般设置不只一种限流规则,而是使用多种限流规则,共同作用。
1.2 常见限流规则
1.2.1 QPS和连接数控制
- 访问评率:
QPS(query per second)
- 可以设置IP维度的限流,例如:直接对
yihua.com
这个IP
进行限制 - 也可以设置单个服务器的限流,例如:对
192.168.73.131
这个IP
进行限制 - 生产实践中通常会设置多个维度的限流规则
设置举例
传输速率:例如资源下载的速度
- 有的网站对这方面的限流逻辑做的更细致,例如百度云盘
-
1.2.3 黑白名单
黑白名单是各大企业常用的限流和方形的手段,而且黑白名单往往是动态变化的
-
1.3 限流的思考
未处理的请求可以设置消息队列缓冲,超过队列长度就丢弃,限流本身就是要丢弃过多请求,没有问题
理解:限流本来就是丢弃过多的请求2. 限流算法
https://class.imooc.com/lesson/1238#mid=29659
2.1 令牌桶算法
2.2 漏桶算法
2.3 滑动窗口
3. 常用解决方案
3.1分布式环境的限流思考
3.2 网关层限流
3.3 中间件限流
3.4 限流组件
3.5 各种限流比较
网关层限流可以避免你的流量冲击到服务器
- 中间件、限流组件在应用层面才会拦截住,但是它们在限流措施上更加灵活,例如最后的Redis注解方式
-
4. 基于Nginx的网关层限流
4.1 IP限流
完全的是一些配置,暂时不记,主要IP限流是限制的服务器的ip不是用户的ip地址
4.2 配置单机限流
4.3 连接数限制
4.4 限制下载速度
5. 基于Redis和Lua的分布式限流
5.1 为什么选择redis
性能:Redis作为缓存组件,如果不考虑持久化方案的话,Redis的大部分操作都是基于内存的
- 线程安全:只用单线程承接网络请求,其它模块任然是多线程,天然具有线程安全的特性,且对原子性的操作支持非常到位
限流服务不仅需要承担超高的QPS,还要保证限流层面具备线程安全的特性,所以利用Redis的天然特性做限流既能保证线程安全,也能保持良好性能
5.2 架构模式
5.2.1 流程架构
访问请求:需要被限流的对象
- 限流规则:定义一段程序或者脚本,当请求来的时候执行
存储介质:用来存储限流信息的地方,比如令牌的个数或者访问请求的计数
5.2.2 问题:限流逻辑应该放在哪里?
5.2.2.1 放在java程序中
性能压力:代码中执行一段限流的程序并不会带来很大的性能压力
- 网络开销
- 在一个限流逻辑中,往往需要发起多个Redis命令和修改指令
- 比如获取令牌这一步,就涉及到查询令牌、发放令牌等步骤
- 这些步骤发起的Redis请求指令,造成了更多的网络开销
- 线程安全
-- 不同的请求传入不同的key,这样才知道你的请求是来自同一个服务,还是不同的服务
-- 这里声明一个本地的变量key,它其实应该是外面传递的,这里模拟
-- 用作限流的key
local key = "lua key"
-- 当前限流规定的上线是多少
-- 限流的最大阈值
local limit = 2
-- 当前的流量大小
local current_limit = 2
-- 判断逻辑: 超出了为false,没超过是true
if current_limit + 1 > limit then
print "reject"
return false
else
print "accept"
return true
end
5.3 Redis预编译Lua
先通过Java程序将Lua脚本上传到Redis进行编译,后续的流程只需要使用编译好的脚本即可
- 第一个是预加载
- 第二个是执行
- 总的来说就是生成一个脚本ID,后续执行都是使用脚本ID+传递的参数
5.4 限流组件封装
最终效果:通过在方法上加注解即可对这个方法实现限流,借用AOP机制
5.5 SpringCloud Gateway组件的限流脚本
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
return { allowed_num, new_tokens }