Ramp 會呼叫數十個第三方 API(Finicity、Teller、Zendesk、Salesforce),而這些 API 各自設有配額;Ramp 也會對全體使用者執行週期性的 Celery 任務,並希望依通過驗證的使用者(而非依 IP)對 inbound 流量進行限流。這三項需求都指向同一個缺口:一個在每個 worker 與每台伺服器之間都準確、而非只在單一 worker 內準確的限流器。

Ramp 原本的做法依賴 Celery 內建的依任務限流。該限制是依各 worker 分別套用,而非全域套用,因此若要落實真正的全系統速率,工程師必須知道該佇列的 worker 數量與自動擴縮行為,再手動把期望的限制分攤到各 worker——而這個數字在 worker 數量一有變動時就立刻過時。在某些 Celery prefetch 設定下,受速率限制的任務甚至可能讓 worker 無法接下新工作,導致整個佇列停滯。

Ramp 評估了固定視窗、滑動視窗與漏桶等演算法,最後選擇通用信元速率演算法(Generic Cell Rate Algorithm,GCRA)——這是漏桶演算法的一種變體,每個鍵只需儲存一個計算出的時間戳,而不必保存完整的請求紀錄。他們以 Redis 實作此演算法,加入分散式鎖以補上參考實作中的競態條件缺口,並改用 Redis 的伺服器時間而非各應用伺服器的時鐘,以避免機器之間的時鐘漂移。

最終成果是一套單一的限流框架,Ramp 可將其套用於全部三種用途——第三方 API 呼叫、全域 Celery 任務節流,以及依使用者的 inbound API 限制——工程師完全不需要考量 worker 數量。

一個以 Redis 為後端的全域 GCRA 限流器

每個鍵只儲存一個 TAT 值,遠比滑動視窗保存完整請求紀錄來得便宜,因此即使面對 Ramp 的請求量,限流器仍能保持快速。

從 Redis 本身而非從處理請求的應用伺服器讀取目前時間,可消除時鐘漂移這個造成限流執行不一致的來源。

在檢查並設定(check-and-set)的過程外加一把 Redis 鎖,可補上兩個並行請求都讀到過期 TAT 而雙雙獲准的競態條件。

打造一套通用框架而非一次性的限流器,意味著同一份程式碼就能服務第三方配額、Celery 任務節流與依使用者的 API 限制,而不必重複邏輯。

只在各行程內執行的速率限制,僅是對全域限制的近似;把計數器集中到一個共享且可上鎖的儲存體,才能讓這個數字真正名副其實。

Ramp 將這套建構於 Redis 上的 GCRA 限流器推行到文中所述的三種用途——全域第三方 API 配額、Celery 任務節流,以及 inbound 的依使用者 API 限流——並搭配單元測試與手動驗證;Ramp 也表示,隨著日後出現更多用途,計畫把同一套框架擴展到新的限流演算法。

追溯依據

參考來源

  1. Rate limiting with Redis Ramp Engineering (Builders Blog) · builders.ramp.com ·