前段時間,我使用了 jwt 來實現郵箱驗證碼的校驗與使用者認證與登入,還特別寫了一篇文章作為總結。
在那篇文章中,提到了一個點,如何限速。
在簡訊驗證碼和郵箱驗證碼,如果不限速,被惡意攻擊造成大量的 QPS,不僅拖垮了服務,也會心疼如水的資費。鑑於君子固窮的原則,在我的郵箱服務里加上限速。
關於如何限速,有兩個比較出名的演算法,漏桶演算法與令牌桶演算法,這裡對其簡單介紹一下,最後再實踐在我發郵件的API中
以下是傳送郵件的 API,已限制為一分鐘兩次,你可以通過修改 email
進行試驗。你也可以在我的站點直接試驗
curl 'https://graphql.xiange.tech/graphql' -H 'Content-Type: application/json' --data-binary '{"query":"mutation SEND($email: String!) {\n sendEmailVerifyCode (email: $email)\n}","variables":{"email":"xxxxxx@qq.com"}}'
複製程式碼
以下是我關於登入實踐的系列文章
本文地址:shanyue.tech/post/rate-l…
Leaky Bucket (漏桶演算法)
漏桶演算法表示水滴(請求)先進入到漏桶裡,漏桶(bucket)以一定的速度出水,當漏桶中水滿時,無法再加水。
- 維護一個計數器作為 bucket,計數器的上限為 bucket 的大小
- 計數器滿時拒絕請求
- 每隔一段時間清空計數器
用 option
代表在 option.window
的視窗時間內最多可以通過 option.max
次請求
以下是使用 redis 的計數器實現限流的虛擬碼
const option = {
max: 10, // window 時間內限速10個請求
window: 1000 // 1s
}
function access(req) {
// 根據請求生成唯一標誌
const key = identity(req)
// 計數器自增
const counter = redis.incr(key)
if (counter === 1) {
// 如果是當前時間視窗的第一個請求,設定過期時間
redis.expire(key, window)
}
if (counter > option.window) {
return false
}
return true
}
複製程式碼
這裡有 Redis 官方使用 INCR 實現限流的文件 redis.io/commands/IN…
此時有一個不算問題的問題,就是它的時間視窗並不是滑動視窗那樣在桶裡出去一個球,就可以再進來一個球。而更像是一個固定時間視窗,從桶裡出去一群球,再開始進球。正因為如此,它可能在固定視窗的後一半時間收到 max-1
次請求,又在下一個固定視窗內打來 max
次請求,此時在一個隨機的視窗時間內最多會有 2 * max - 1
次請求。
另外還有一個redis的 INCR
與 EXPIRE
的原子性問題,容易造成 Race Condition
,可以通過 SETNX
來解決
redis.set(key, 0, 'EX', option.window, 'NX')
複製程式碼
另外也可以通過一個 LUA
指令碼來搞定,顯然還是 SETNX
簡單些
local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
redis.call("expire",KEYS[1],1)
end
複製程式碼
為了解決 2N 的問題,可以由維護一個計數器,更改為維護一個佇列。代價是記憶體佔用空間過高,且更難解決 Race Condition
以下是使用 redis 的 set/get string 實現的限流
const option = {
max: 10, // window 時間內限速10個請求
window: 1000 // 1s
}
function access(req) {
// 根據請求生成唯一標誌
const key = identity(req)
const current = Date.now()
// cache 視為快取物件
// 篩選出當前時間視窗的請求個數,每個請求標誌為時間戳的格式
// 為了簡單這裡不做 json 的序列化和反序列化了...
const timestamps = [current].concat(redis.get('timestamps')).filter(ts => ts + option.window > current)
if (timestamps.length > option.max) {
return false
}
// 此時讀寫不同步,會有 Race Condition 問題
redis.set('timestamps', timestamps, 'EX', option.window)
return true
}
複製程式碼
這裡再使用一個 LUA 指令碼解決 Race Condition
的問題
TODO
Token Bucket (令牌桶演算法)
由圖先看一看令牌桶與漏桶的不同
- 令牌桶初始狀態 bucket 是滿的,漏桶初始狀態 bucket 是空的
- 令牌桶在 bucket 空的時候拒絕新的請求,漏桶在 bucket 滿的時候拒絕新的請求
- 當一個請求來臨時,假設一個請求消耗一個token,令牌桶的 bucket 減少一個 token,漏桶增加一個 token
以下使用 redis 實現令牌桶
TODO