速率限制是任何生產 API 的關鍵組成部分。它可以保護後端服務免遭濫用,確保公平的資源分配,保持負載下的效能,並減輕拒絕服務攻擊。本文研究了主要的速率限制演算法、它們的權衡以及設計穩健系統的最佳實踐。
令牌桶演算法
令牌桶是應用最廣泛的限速演算法之一。一個桶保存著以穩定速率重新填充的令牌,每個請求消耗一個令牌。如果桶子是空的,則請求被拒絕。這種方法允許突發達到桶容量,同時隨著時間的推移平滑流量。
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRate;
this.lastRefill = Date.now();
}
tryConsume() {
this._refill();
if (this.tokens > 0) { this.tokens--; return true; }
return false;
}
_refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
}
}
此實作簡單、記憶體效率高,每個客戶端只有一個計數器,非常適合需要突發容忍的通用速率限制。
漏桶演算法
漏桶強制執行嚴格的流出率。請求進入隊列並以固定速率處理,完全防止突發。這使其成為保護無法處理突發流量峰值的下游系統的理想選擇。代價是由於佇列和無法處理任何類型的突發而導致更高的記憶體使用量。
滑動視窗演算法
滑動視窗方法比固定視窗計數器提供更準確的速率限制。滑動視窗日誌儲存目前視窗內所有請求的時間戳記。對於每個請求,視窗外的條目都會被刪除,並檢查剩餘的計數。這種方法是基於視窗的方法中最準確的,但會佔用大量內存,因此適合低流量場景或精度至關重要的情況。
滑動視窗計數器透過使用加權公式將先前的視窗計數與當前視窗部分計數相結合,提供了更實用的平衡:
weightedCount = previousWindowCount * overlapRatio + currentWindowCount
此方法每個客戶端僅使用兩個計數器,為大多數用例提供良好的準確性,並且是 Stripe 和 GitHub 使用的行業標準。
基於Redis的實現
Redis 是分散式速率限制最常見的後備儲存。其原子操作(例如 INCR 和 EXPIRE)提供了線程安全性,內建 TTL 可以實現自動計數器清理,Lua 腳本支援複雜的演算法實現。
const { SlidingWindowRateLimiter } = require("redis-rate-limiter");
const limiter = new SlidingWindowRateLimiter({
client: redisClient,
window: 60,
limit: 100,
});
Redis 也為水平擴展和亞毫秒級操作延遲提供叢集支援。對於生產部署,用於原子速率限制、處理 Redis 故障轉移和效能調整的 Lua 腳本模式是重要的考慮因素。
HTTP 速率限制標頭
標準化標頭允許伺服器通知客戶端其速率限制狀態。 RateLimit-Limit 標頭表示視窗中允許的最大請求數,RateLimit-Remaining 顯示剩餘請求數,RateLimit-Reset 提供視窗重設時間,Retry-After 告訴客戶端在重試之前需要等待多長時間。
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Reset: 1712345678
IETF 標準草案正式化了這些標頭,儘管許多 API 仍然使用攤販特定的 X-RateLimit 前綴。具有資訊正文內容的正確 429 Too Many Requests 回應格式對於客戶端互通性同樣重要。
分散式限速
在分散式系統中,速率限制必須跨實例協調。使用 Redis 的集中式方法提供了具有一致限制的單一事實來源,但引入了單點故障風險。 本地計數器避免了協調開銷,但在負載不平衡的情況下會產生不一致的限制。混合策略使用本地計數器和定期 Redis 同步來平衡權衡。
| 方法 | 一致性 | 可用性 | 複雜性 |
|---|---|---|---|
| 集中式Redis | 準確 | 降低 | 低 |
| 本地專櫃 | 約 | 高 | 無 |
| 混合動力 | 近乎精確 | 高 | 中 |
客戶端 IP 與經過驗證的使用者速率限制是另一個關鍵設計決策,對匿名、經過驗證和進階使用者群組實施分層限制也是如此。
客戶端重試策略
客戶端應實現智慧重試行為以優雅地處理速率限制回應。帶有抖動的指數退避可以防止雷群問題,並且遵守 Retry-After 標頭可確保與伺服器期望保持一致。
async function fetchWithRetry(url, options = {}) {
const maxRetries = options.maxRetries || 3;
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url);
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After") || 1;
await sleep(retryAfter * 1000);
continue;
}
return response;
}
throw new Error("Max retries exceeded");
}
冪等性金鑰允許安全地重試變異請求,確保重試的請求不會產生重複的副作用。
演算法選擇指南
| 演算法 | 爆發支援 | 記憶體 | 準確度 | 最佳用例 |
|---|---|---|---|---|
| 令牌桶 | 是的 | 低 | 好 | 通用 |
| 漏桶 | 沒有 | 中 | 好 | 吞吐量穩定 |
| 固定窗 | 是的 | 低 | 可憐 | 簡單的速率限制 |
| 滑動視窗日誌 | 是的 | 高 | 優秀 | 臨界精度 |
| 推拉窗櫃檯 | 是的 | 低 | 非常好 | 生產 API |
結論
選擇正確的速率限制演算法取決於您對突發容忍度、準確性和記憶體效率的要求。滑動視窗計數器為大多數生產 API 提供了最佳平衡,而令牌桶則擅長流量整形。精心設計的速率限制系統將伺服器端執行與資訊標頭和協作客戶端行為結合,創建一個有彈性的 API 生態系統。

