Skip to main content

Flask rate limiter耐人尋味的地方

· 4 min read
Blog owner

最近工作上的需要,研究了一下Flask Limiter,裡面提供三種限流的演算法,不過在文件裡面稱為strategy

因為文件沒有講得很詳細,所以有稍微看一下source code,不過主要也是看以Redisstorage的實做方式。

Fixed WindowMoving Window的實作方式跟猜測的沒有差太多,Fixed WindowRedis裡面用int來記錄count,並且呼叫incrby來增加count,並且根據設定的rate limit來決定TTL

Moving WindowRedis用一個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,所以每次keyexpire都會被重設,重設得值根據設定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到天長地久......