Flask rate limiter耐人尋味的地方
最近工作上的需要,研究了一下Flask Limiter,裡面提供三種限流的演算法,不過在文件裡面稱為strategy。
因為文件沒有講得很詳細,所以有稍微看一下source code,不過主要也是看以Redis當storage
的實做方式。
Fixed Window
和Moving Window
的實作方式跟猜測的沒有差太多,Fixed Window
在Redis裡面用int
來記錄count,並且呼叫incrby
來增加count,並且根據設定的rate limit來決定TTL。
Moving Window
在Redis用一個list紀錄最多N
個請求(N
是設定rate limit),每次request來的時候把list裡面outdated
的node給刪掉,然後檢查list長度有沒有超過N
。
有趣的是Flask Limiter提供的第三個演算法Fixed Window with Elastic Expiry
,我們來看一下官方文件怎麼 解釋
This strategy works almost identically to the Fixed Window strategy with the exception that > each hit results in the extension of the window. This strategy works well for creating large > penalties for breaching a rate limit.
For example, if you specify a 100/minute rate limit on a route and it is being attacked at > the rate of 5 hits per second for 2 minutes - the attacker will be locked out of the > resource >for an extra 60 seconds after the last hit. This strategy helps circumvent bursts.
我就是看不懂它的具體行為,所以才開始翻code,然後就看到有趣的地方
def _incr(
self,
key: str,
expiry: int,
connection: RedisClient,
elastic_expiry: bool = False,
amount: int = 1,
) -> int:
"""
increments the counter for a given rate limit key
:param connection: Redis connection
:param key: the key to increment
:param expiry: amount in seconds for the key to expire in
:param amount: the number to increment by
"""
value = connection.incrby(key, amount)
if elastic_expiry or value == amount:
connection.expire(key, expiry)
return value
當使用Fixed Window with Elastic Expiry
的時候,elastic_expiry
會被設定成True
,所以每次key
的expire
都會被重設,重設得值根據設定rate limit決定,如文件說的這個演算法會懲罰明明超過limit還一直打的client,但因為key
不會expire,所以即使沒違限速,只要一直打也是會打倒定義的上限,然後被鎖起來直到timeout。
我從request開始的地方一路遊覽到response,沒看到任何reset count的地方,覺得很納悶,所以寫了簡單的程式來確認行為,我設定每10秒上限20個(qps=2),然後一秒打一次(qps=1),果然打20秒以後就開始被block,但是用Fixed Window
並不會,所以證明了我的想法。
我估計Flask Limiter想讓開發者有決定懲罰多重的自由度,但沒超過限速也會被block這點真的很微妙,線上使用我一定不敢用這個方法,一個不小心client可能被block到天長地久......