速率限制模式:高效管理受限服務

  1. 情境與問題
  2. 解決方案
  3. 問題與考量
  4. 何時使用此模式
  5. 範例架構
  6. 相關模式
  7. 參考資料

許多服務使用節流來控制資源消耗,對應用程式存取它們的速率施加限制。速率限制模式幫助你避免節流錯誤並準確預測吞吐量,特別是對於大規模重複性自動化任務(如批次處理)。

情境與問題

對受節流服務執行大量操作可能導致流量增加和效率降低。你需要追蹤被拒絕的請求並重試操作,可能需要多次傳遞才能完成工作。

考慮這個將資料匯入資料庫的範例:

  • 你的應用程式需要匯入 10,000 筆記錄。每筆記錄需要 10 個請求單位(RUs),總共需要 100,000 RUs。
  • 你的資料庫實例有 20,000 RUs 的佈建容量。
  • 你傳送所有 10,000 筆記錄。2,000 筆成功,8,000 筆被拒絕。
  • 你重試 8,000 筆記錄。2,000 筆成功,6,000 筆被拒絕。
  • 你重試 6,000 筆記錄。2,000 筆成功,4,000 筆被拒絕。
  • 你重試 4,000 筆記錄。2,000 筆成功,2,000 筆被拒絕。
  • 你重試 2,000 筆記錄。全部成功。

工作完成了,但只是在傳送了 30,000 筆記錄之後——是實際資料集大小的三倍。

這種天真方法的額外問題:

  • 錯誤處理開銷:20,000 個錯誤需要記錄和處理,消耗記憶體和儲存空間。
  • 無法預測的完成時間:不知道節流限制,你無法估計處理需要多長時間。

解決方案

速率限制透過控制在一段時間內傳送到服務的記錄數量來減少流量並提升吞吐量。

服務基於不同指標進行節流:

  • 操作數量(例如,每秒 20 個請求)
  • 資料量(例如,每分鐘 2 GiB)
  • 操作的相對成本(例如,每秒 20,000 RUs)

你的速率限制實作必須控制傳送到服務的操作,在不超過容量的情況下優化使用。

使用持久化訊息系統

當你的 API 可以比受節流服務允許的速度更快地處理請求時,你需要管理匯入速度。簡單地緩衝請求是有風險的——如果你的應用程式崩潰,你會失去緩衝的資料。

相反,將記錄傳送到可以處理你完整匯入速率的持久化訊息系統。使用作業處理器以受節流服務限制內的受控速率讀取記錄。

持久化訊息選項包括:

  • 訊息佇列(例如,RabbitMQ、ActiveMQ)
  • 事件串流平台(例如,Apache Kafka)
  • 雲端佇列服務
graph LR A["API
(高速率)"] --> B["持久化
訊息佇列"] B --> C["作業處理器 1"] B --> D["作業處理器 2"] B --> E["作業處理器 3"] C --> F["受節流服務
(有限速率)"] D --> F E --> F style A fill:#e1f5ff style B fill:#fff4e1 style F fill:#ffe1e1

細粒度時間間隔

服務通常基於可理解的時間跨度(每秒或每分鐘)進行節流,但電腦處理速度要快得多。與其每秒批次釋放一次,不如更頻繁地傳送較小的數量以:

  • 保持資源消耗(記憶體、CPU、網路)均勻流動
  • 防止突發請求造成的瓶頸

例如,如果服務允許每秒 100 個操作,每 200 毫秒釋放 20 個操作:

管理多個不協調的處理程序

當多個處理程序共享受節流服務時,邏輯分割服務的容量並使用分散式互斥系統來管理這些分割區的鎖定。

範例:

如果受節流系統允許每秒 500 個請求:

  1. 建立 20 個分割區,每個價值每秒 25 個請求
  2. 需要 100 個請求的處理程序請求四個分割區
  3. 系統授予兩個分割區 10 秒
  4. 處理程序速率限制為每秒 50 個請求,在 2 秒內完成,然後釋放鎖定

實作方法:

使用 blob 儲存為每個邏輯分割區建立一個小檔案。應用程式在短時間內(例如,15 秒)獲得這些檔案的獨佔租約。對於授予的每個租約,應用程式可以使用該分割區的容量。

block-beta columns 3 block:processes:3 columns 3 P1["處理程序 1"] P2["處理程序 2"] P3["處理程序 3"] end space:3 block:leases:3 columns 3 L1["租約 1
25 請求/秒"] L2["租約 2
25 請求/秒"] L3["租約 3
25 請求/秒"] end space:3 block:service:3 S["受節流服務
總共 500 請求/秒"] end P1 --> L1 P2 --> L2 P3 --> L3 L1 --> S L2 --> S L3 --> S style processes fill:#e1f5ff style leases fill:#fff4e1 style service fill:#ffe1e1

為了減少延遲,為每個處理程序分配少量獨佔容量。處理程序只在超過其保留容量時才尋求共享容量租約。

租約管理的替代技術包括 Zookeeper、Consul、etcd 和 Redis/Redsync。

問題與考量

💡 關鍵考量

處理節流錯誤:速率限制減少錯誤但不會消除它們。你的應用程式仍必須處理任何發生的節流錯誤。

多個工作流:如果你的應用程式有多個工作流存取相同的受節流服務(例如,批次載入和查詢),將所有工作流整合到你的速率限制策略中,或為每個工作流保留單獨的容量池。

多應用程式使用:當多個應用程式使用相同的受節流服務時,增加的節流錯誤可能表示競爭。考慮暫時降低吞吐量,直到其他應用程式的使用量降低。

!!!

何時使用此模式

使用此模式以:

  • 減少來自受節流限制服務的節流錯誤
  • 與天真的錯誤重試方法相比減少流量
  • 僅在有容量處理記錄時才將記錄出列,從而減少記憶體消耗
  • 提高批次處理完成時間的可預測性

範例架構

考慮一個應用程式,使用者向 API 提交各種類型的記錄。每種記錄類型都有一個獨特的作業處理器,執行驗證、豐富化和資料庫插入。

所有元件(API、作業處理器)都是獨立擴展的獨立處理程序,不直接通訊。

graph TB U1["使用者"] --> API["API"] U2["使用者"] --> API API --> QA["佇列 A
(類型 A 記錄)"] API --> QB["佇列 B
(類型 B 記錄)"] QA --> JPA["作業處理器 A"] QB --> JPB["作業處理器 B"] JPA --> LS["租約儲存
(Blob 0-9)"] JPB --> LS JPA --> DB["資料庫
(1000 請求/秒限制)"] JPB --> DB style API fill:#e1f5ff style QA fill:#fff4e1 style QB fill:#fff4e1 style LS fill:#f0e1ff style DB fill:#ffe1e1

工作流程:

  1. 使用者向 API 提交 10,000 筆類型 A 記錄
  2. API 將記錄加入佇列 A
  3. 使用者向 API 提交 5,000 筆類型 B 記錄
  4. API 將記錄加入佇列 B
  5. 作業處理器 A 嘗試租用 blob 2
  6. 作業處理器 B 嘗試租用 blob 2
  7. 作業處理器 A 失敗;作業處理器 B 獲得 15 秒的租約(100 請求/秒容量)
  8. 作業處理器 B 將 100 筆記錄出列並寫入
  9. 1 秒後,兩個處理器都嘗試額外的租約
  10. 作業處理器 A 獲得 blob 6(100 請求/秒);作業處理器 B 獲得 blob 3(現在總共 200 請求/秒)
  11. 處理器繼續競爭租約並以其授予的速率處理記錄
  12. 當租約到期時(15 秒後),處理器相應地降低其請求速率

相關模式

節流:速率限制通常是為了回應受節流服務而實作的。

重試:當請求導致節流錯誤時,在適當的間隔後重試。

基於佇列的負載平衡:與速率限制類似但更廣泛。主要差異:

  • 速率限制不一定需要佇列,但需要持久化訊息
  • 速率限制引入分散式互斥在分割區上,允許管理與相同受節流服務通訊的多個不協調處理程序的容量
  • 基於佇列的負載平衡適用於服務之間的任何效能不匹配;速率限制專門針對受節流服務

參考資料

分享到