限流器
我们项目是新闻类的站点,每天需要限制各种爬虫,保证服务在可承受范围内,需要保护后台服务正常。需要使用限流器。
- 固定
- 滑动
- 令牌桶
一 固定窗口:
当前请求是否在最后一次请求时间点+设置的窗口的时间范围内,然后累计请求次数,判断是否达到上限,如果不在范围会重新初始化请求数(0)并记录当前请求时间为最后一次请求时间。
例如当程序启动的时候设置最后一次请求时间为当前系统是,当有一个请求到来是,判断请求时刻在(最后一次请求时间+固定的窗口是否)之间,如果在之间说明需要判断是否允许请求,即累加当前窗口的次数并判断是否达到最大的请求数
缺点是:时间窗口会承受2倍的请求。比如窗口时间每秒允许10个请求, 当第一秒的后半秒请求了10,在下一秒的上半秒也请求了10个,这样也相当于1秒内承受了20个请求。如果每秒允许请求的数量过大会导致服务器需要承受2倍的压力。可能会搞垮服务
二 滑动窗口:
基于固定窗口出现的半秒问题,滑动窗口主要解决了这个半秒问题,但是毛刺现象还会出现。出现一种改进版的滑动窗口限流
整体思路是将固定串口等分若干份,基于当前的请求时间点向前推算一个窗口并找到对应的时间点,计算这段时间点产生请求数是否达到上限。
缺点是:
- 需要分多少份, 如果分多了,需要记录每个时间点请求的个数。如果分少了,当请求时间推算一个时间窗口,可能出现时间窗口的起始时间被包含在某个等份中不利于精确计算。
- 提高精度需要多份,越多越好。
- 但是份数越多需要对每一份都需要记录。这个是非常耗CPU
如果未读越来越多比如基于ip、榜单id、话题id、这个存储和计算都会非常高的。
三 令牌桶:
主要解决请求毛刺(请求不均匀,有突发请求)的问题
以固定的速率向一定大小的桶中产生令牌,当请求获取令牌是,如果桶中存在令牌返回即可,如果桶中不存在会触发计算当前请求时间与最后一次请求时间
为了防止在单点采用redis+lua脚本实现, 主要参数 last_request_time: 最后一次请求的时间,用于计算与上一次时间产生的令牌数,注意如果大于上线去total_token数。 total_token: 总的令牌个数 rate: 令牌的产生速率,例如 5/s,每秒5个 current_token: 当前token的个数
如果不能保证原子的操作,这是会出现放入令牌
,获取令牌
协同的问题。 还有分布式单点问题,故在用lua,简单封装
11. 根据key获取当前的token是否存在
22. 如果存在可用的token则直接返回
33. 如果不存在此时需要计算
4 3.1 根据上一次的时间点与当前时间点,与token产生的速率计算一共生成的token
5 3.2 与total_token和计算后的token总数,如果超过允许最大的,只取最大的
6 3.3 必须设置当前计算的时间点,以为是lua,在redis中是原子顺序的执行(单线程的)保证数据正常的设置
当前业务,如果出现爬虫请求过频繁,会影响正常用户。所以基于请求用户的http头标识将爬虫请求到配置相对低一点的服务并限制请求上限
1--- @param key 令牌的唯一标识
2--- @param permits 请求令牌数量
3--- @param curr_mill_second 当前时间
4--- 0 没有令牌桶配置;-1 表示取令牌失败,也就是桶里没有令牌;1 表示取令牌成功
5local function acquire(key, permits, curr_mill_second)
6 local local_key = key --- 令牌桶key ,使用 .. 进行字符串连接
7 if tonumber(redis.pcall("EXISTS", local_key)) < 1 then --- 未配置令牌桶
8 return 0
9 end
10
11 --- 令牌桶内数据:
12 --- last_mill_second 最后一次放入令牌时间
13 --- curr_permits 当前桶内令牌
14 --- max_permits 桶内令牌最大数量
15 --- rate 令牌放置速度
16 local rate_limit_info = redis.pcall("HMGET", local_key, "last_mill_second", "curr_permits", "max_permits", "rate")
17 local last_mill_second = rate_limit_info[1]
18 local curr_permits = tonumber(rate_limit_info[2])
19 local max_permits = tonumber(rate_limit_info[3])
20 local rate = rate_limit_info[4]
21
22 --- 标识没有配置令牌桶
23 if type(max_permits) == 'boolean' or max_permits == nil then
24 return 0
25 end
26 --- 若令牌桶参数没有配置,则返回0
27 if type(rate) == 'boolean' or rate == nil then
28 return 0
29 end
30
31 local local_curr_permits = max_permits;
32
33 --- 令牌桶刚刚创建,上一次获取令牌的毫秒数为空
34 --- 根据和上一次向桶里添加令牌的时间和当前时间差,触发式往桶里添加令牌,并且更新上一次向桶里添加令牌的时间
35 --- 如果向桶里添加的令牌数不足一个,则不更新上一次向桶里添加令牌的时间
36 --- ~=号在Lua脚本的含义就是不等于!=
37 if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= nil) then
38 if(curr_mill_second - last_mill_second < 0) then
39 return -1
40 end
41 --- 生成令牌操作
42 local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate) --- 最关键代码:根据时间差计算令牌数量并匀速的放入令牌
43 local expect_curr_permits = reverse_permits + curr_permits;
44 local_curr_permits = math.min(expect_curr_permits, max_permits); --- 如果期望令牌数大于桶容量,则设为桶容量
45 --- 大于0表示这段时间产生令牌,则更新最新令牌放入时间
46 if (reverse_permits > 0) then
47 redis.pcall("HSET", local_key, "last_mill_second", curr_mill_second)
48 end
49 else
50 redis.pcall("HSET", local_key, "last_mill_second", curr_mill_second)
51 end
52 --- 取出令牌操作
53 local result = -1
54 if (local_curr_permits - permits >= 0) then
55 result = 1
56 redis.pcall("HSET", local_key, "curr_permits", local_curr_permits - permits)
57 else
58 redis.pcall("HSET", local_key, "curr_permits", local_curr_permits)
59 end
60 return result
61end
62
63--- 初始化令牌桶
64local function initTokenBucket(key, max_permits, rate)
65 if(key == nil or string.len(key) < 1) then
66 return 0
67 end
68 local local_max_permits = 100
69 if(tonumber(max_permits) > 0) then
70 local_max_permits = max_permits
71 end
72
73 local local_rate = 100
74 if(tonumber(rate) > 0) then
75 local_rate = rate
76 end
77 redis.pcall("HMSET", key, "max_permits", local_max_permits, "rate", local_rate)
78 return 1;
79end
80
81--- 获取当前时间,单节点获取,避免集群模式下(无论业务系统集群,还是redis集群)获取的时间不同,导致桶不匀速
82local function currentTimeMillis()
83 local times = redis.pcall("TIME")
84 return tonumber(times[1]) * 1000 + tonumber(times[2]) / 1000
85end
86
87--- key,即redis中的key。
88local key = KEYS[1]
89--- args第一个参数即要调用的方法名。
90local method = ARGV[1]
91--- 请求令牌
92if method == 'acquire' then
93 return acquire(key, ARGV[2], ARGV[3])
94--- 请求时间
95elseif method == 'currentTimeMillis' then
96 return currentTimeMillis()
97--- 初始化令牌桶
98elseif method == 'initTokenBucket' then
99 return initTokenBucket(key, ARGV[2], ARGV[3])
100end
评论