這篇來記錄我在做 Rate Limit 的思考過程,也有稍微提一下我測試時使用的工具是什麼

首先簡單講 Rate Limit 就是「限速」或「限流」,是一種保護系統不被流量衝爆的機制
它會限制某個使用者(或 IP、店家、API Key 之類的 key)在一段時間內能發出多少次請求
舉幾個生活中常見的例子

  • 你用某個 API 每分鐘最多只能 call 100 次,超過就回 429 Too Many Requests
  • 購物網站的結帳頁面,每秒最多讓 5 個人同時結帳,防機器人搶單或 DDoS
  • 社群平台發文或按讚,每小時上限 300 次,防止刷讚或垃圾重複留言

實務上常見的 Rate Limit 演算法大致有三種,差異主要在於流量邊界的平滑程度與實作複雜度

  • Token Bucket
    • 系統以固定速率補充 Token,每次請求消耗一個 Token,因此可以容許短時間超量,同時維持長期流量受控
  • Sliding Window
    • 以當下往前推的一段時間來計算請求數,相較 Fixed Window 邊界行為更平滑,但實作與計算成本較高
  • Fixed Window (本次的受測對象)
    • 在固定時間區間內累積請求數量,超過上限就拒絕,但在時間窗切換瞬間可能出現流量突波

Distributed Rate Limiting Strategies
圖片 resource: https://systemdr.substack.com/p/distributed-rate-limiting-strategies

跟壓力測試的差異在於壓力測試主要都是為了量測系統是否能夠在 RPS/RPM(Request Per Second/Request Per Minute) 下正常運作(可能也會考慮到 data 量,看量測的是什麼)
也會確保把機器打爆之後,將 RPS/RPM 歸零或設為小流量,確認機器是否能夠恢復正常

但是 Rate Limit 的測試就有一些的不同,測試的重點在於規則落實的精準度
主要是驗證當流量精準踩在邊界時

  • 攔截是否發生得毫秒不差
  • 當配置在 Redis 更新後,新規則是否即時生效
  • 以及當一定的時間(1秒/60秒)過去後,系統是否能如期恢復

所以本質上針對業務邏輯的功能測試,只是會需要用到壓力測試的工具來模擬高頻率的動作

有些人的想法或許會單純的認為
「啊就把 RPS/RPM 直接壓爛,看有沒有回 429 就好啦~」

王世堅-就是這麼簡單

但實際上還是有蠻多的測試點可以做思考

來簡單介紹這一次的 Rate Limit 功能

  1. 未設定 RPS/RPM 的店家,都會走全域的 RPS/RPM,全域 Config 是以店家為 key 各自計數 (意思就是 A 的 counter 不會吃到 B)
  2. 可以針對店家設定屬於該店家的 RPS/RPM
  3. 設定的地方在 Redis,所以預期改動設定會立即套用
  4. 當觸發 RPS/RPM 時,會開始回應 429

另外,這次文章中不會提到實際我使用壓測工具的實作內容,但我可以說一下我當時使用的工具跟一些簡單的細節

我那時候是選擇使用 k6,特別是它的 Constant Arrival Rate,簡單的操作跟設定,而且,潮!
雖然它底層依然需要配置 VU (Virtual Users) 作為執行資源,但它的邏輯優點在於你不需要再去算幾個 VU 能跑出多少 RPS,而是直接宣告你想要的目標頻率
這對 Rate Limit 測試是很重要的,因為你必須確保流量發送頻率完全不受受測對象的回應延遲干擾,才能精準地觸及你設定的邊界值

針對功能進行測試的發想

測試前提

在發想前,為了避免過於發散,所以會先設一下 outline

  1. 全域設定是必然存在的
  2. 不需要「順便」測試系統負載,以 Rate Limit 的驗證為優先
  3. 假設 Redis 連線正常,不測試 Redis 故障情況,聚焦在 Rate Limit 觸發邏輯本身

不過實務上還是會針對範圍以外的內容進行風險確認,畢竟 Redis 壞掉或者 Config 遺失的風險也還是可能存在的
所以仍然需要確認這種狀態底下,是否會發生哪些問題

接著理解一下整個 Rate Limit 在系統運作的流程,畫出來是這樣

Rate Limit Flow

開始發想

先簡單暴力的確認全域的 RPS/RPM 可不可以正常運作,這樣做會引發一個思考

如何確保被觸發的是 RPS 或 RPM?

自我解答:就是把其中一個數字設定得很大(控制變相),實際模擬 RPS/RPM 時,確保不會觸發到即可
但又冒出了一個問題

如何在「還不確定 Config 是否正常運作」的情況下,驗證 RPS/RPM 設定確實有效?

再次自我解答:那就是先把兩個數字都設得很大,確保小流量不會回應 429,至少我能確認小流量的情況下系統正常運作了
確保 RPS/RPM 皆正常運作之後,再把其中一個數字設定為 1,這樣我手動就能觸發 429 確保 Rate Limit 機制正常運作了

以上思考與自我解答的時間差大約為 0.01 秒:
咒術迴戰 S1-20 集,東堂迴避花御的咒力種子思考時間

經過上面的思考之後,我們可以得到以下測試想法

A. 全域設定驗證
  1. 全域 RPS 限制是否觸發限制
  2. 全域 RPS 限制是否未觸發限制
  3. 全域 RPM 限制是否觸發限制
  4. 全域 RPM 限制是否未觸發限制
B. 店家設定驗證
  1. 店家專屬 RPS 限制是否觸發限制
  2. 店家專屬 RPS 限制是否未觸發限制
  3. 店家專屬 RPM 限制是否觸發限制
  4. 店家專屬 RPM 限制是否未觸發限制
C. 設定優先權驗證
  1. 當店家有專屬設定時,是否優先使用店家設定而非全域設定
  2. 當店家無專屬設定時,是否正確 fallback 到全域設定
D. 即時更新驗證
  1. 修改 Redis 設定後,是否立即生效(不需重啟服務)
  2. 當實際 call 的 RPS 降低後,系統是否能夠恢復正常
E. 其他
  1. 確保店家專屬設定不會套用在其他店家
  2. 確保全域設定在不同店家是獨立的

建立測試矩陣

根據上列的測試想法,就可以開始建立測試矩陣了,驗證重點的代碼對應上方清單

序號 全域設定 (RPS/RPM) 店家 A 設定 (RPS/RPM) 測試流量 (對 A) 測試流量 (對 B) 預期結果 驗證重點
01 20 / 3600 19 RPS 0 200 OK A2, A4
02 20 / 3600 21 RPS 0 429 Refuse A1
03 25 / 3600 21 RPS 0 200 OK D1, A2, A4
04 25 / 1250 21 RPS 0 429 Refuse A3, D1 (21*60=1260 RPM)
05 25 / 3600 22 / 1500 21 RPS 0 200 OK B2, B4, A2, A4, D1
06 25 / 3600 22 / 1500 23 RPS 0 429 Refuse C1, B1, B4
07 25 / 3600 25 / 1500 23 RPS 0 200 OK B4, B2, D1
08 25 / 3600 25 / 1080 23 RPS 0 429 Refuse B3, D1
09 25 / 3600 22 / 1080 15 RPS 0 200 OK D2, A2, A4, B2, B4
10 25 / 3600 22 / 1080 0 23 RPS 200 OK E1, A2, A4
11 25 / 3600 22 / 1080 15 RPS 23 RPS 200 OK E2, A2, A4, B2, B4
12 25 / 3600 26 RPS 0 429 Refuse C2, A1
13 25 / 3600 23 RPS 0 200 OK C2, A2

列到這裡大致上的「測試想法」都涵蓋到了
在列這個測試矩陣的時候很容易腦子裡的邏輯會打架,在列舉的同時會衍伸很多想法,例如

  1. 如果觸發了限制,是觸發 global config 還是 店家專屬 config,如何控制 Config 達成測試目的
  2. 每一個 測試想法 是否有 組合性,例如 C2 是明顯有組合性的,因為 fallback 回去也要確認 A1, A2 是否運作正常
  3. 原本 A1~A4 和 B1~B4 只想寫 RPS/RPM 是否觸發,但在閱讀上會很難判斷究竟驗證了什麼樣的東西,所以才拆成現狀
  4. 要控制的變因很有多元性,你可以控制你 call 的頻率、全域 Config、店家 Config、call 哪個店家,這很容易讓腦子混亂

結語

這一個 Rate Limit 測試紀錄其實還有很多沒寫上來的,但其實也「可以是」測試者的防守範圍

例如,測試時我會去問為什麼選擇 Fixed Window?是否能接受它在窗口切換瞬間可能造成的流量突波?因為這些選擇,實際上是在決定系統在流量邊界下的穩定性表現

在這次的測試發想過程中,最難的不只是要理解系統的內部機制,還包含要區分那些看起來重疊,但在執行語意上其實不同的測試 Case(有時候確實會用等價分析來限縮)
另外,如果不理解 Config 是存放在 Redis、不理解系統如何從全域 Fallback 到店家專屬設定,我們就無法列出像 Case 12, 13 這種變因重疊的測試場景

另外,還有一些在這裡沒有特別展開的,是關於隱性邊界的測試,例如在分散式環境下,計數器同步是否可能因為 Race Condition 導致攔截不準
這題就很吃測試者對於受測環境的掌控度

所以測試 Rate limit 的本質就是在驗證系統在流量超過設計上限時,是否能用可預期、可控制的方式拒絕請求,同時維持整體服務穩定與公平性的一個「功能測試」
也就是說,一個好的 Rate Limit 測試,就不會只看他有沒有回 429
因為回 429 只能證明限制存在,但不能證明限制是正確的

Rate Limit 測試要達成的是觸發到的邊界是否準確、規則切換是否即時、多節點下結果是否一致,且整個過程能被觀測與重現
這些都成立,才代表你的限流是可靠的

碎念

其實這個 Rate Limit 的測試已經是快要兩年多前做的了
拖到現在才寫是因為要描述測試這件事情的過程是很費工的事情
對我來說,如何妥善地把測試的思考步驟描述出來並且將原本的 Rate Limit 功能簡化成可以把它寫下來的版本,是這一篇最難的地方

原本想寫的架構可能是直接實做一個有限流器的 backend API,用 AI 很快就能做出來,只是覺得就算做出來也不是我想表達的重點
因為我想表達的是「人類在測試的過程中思考是如何運作的」,也可以讓一些可能會測試到 Rate Limit 的人有一些參考

雖然現在 AI 的世界中,多少會把自己對於需求的理解寫下來
搭配 AI 一起生成待測試項目,懶惰一點的可能就直接把 spec 丟給 AI 就產生了

但不管是勤勞還是懶惰
你最終都得拿著那份「不是你產生的待測項目」執行一次
而你永遠都可以在這份待測項目產生新的測試想法
而這種新產生的測試想法也很常是非常具有價值的
因為它很常出現在規格描述的範圍之外,他可能是某個功能跟這個待測項目產生的隱形邊界,或者根本沒被描述在規格之中的情境
這個額外產生的新想法就是測試者本身的隱性知識所帶來的,也是測試者在測試工作上所能產生的最大價值且難以被取代的能力之一

總之,上面這些就是這次簡單聊我是怎麼測試 Rate Limit 的內容
如果你也測試過 Rate Limit,歡迎分享你踩過什麼雷~