API 速率限制:演算法與實作
您的 API 正遭受攻擊。不是來自精密的駭客,而是一個簡單的腳本,每秒鐘數千次地猛擊您的端點,消耗資源並降低合法用戶的服務品質。如果沒有速率限制,一個行為不當的客戶端就可能拖垮整個應用程式。本指南說明核心演算法,並展示在 Node.js 和 Nginx 中的實用實作,以保護您的 API。
為何速率限制至關重要
速率限制控制客戶端在給定時間窗口內可以發出的請求數量。它是您抵禦以下威脅的第一道防線:
- 暴力破解攻擊:針對登入或 API 金鑰端點
- 拒絕服務(DoS):來自過量請求
- 資源耗盡:來自資料庫查詢或檔案處理等昂貴操作
- 濫用免費方案:由爬蟲或機器人發動
除了安全性之外,速率限制可確保公平使用,並協助您執行業務規則,例如分級定價方案。
速率限制演算法
四種演算法主導了 API 速率限制。每種在準確性、記憶體用量和突發處理方面都有取捨。
1. 固定窗口
在固定時間間隔內計算請求數(例如每分鐘 100 個請求)。當窗口重置時,計數器也會重置。
- 優點:簡單,記憶體用量低(每個客戶端一個計數器)。
- 缺點:在窗口邊界會出現突發。客戶端可以在 12:00:59 發送 100 個請求,並在 12:01:00 再發送 100 個,相當於兩秒內 200 個請求。
2. 滑動窗口
追蹤每個請求的時間戳,並計算在過去 N 秒內有多少個請求。這可以平滑突發流量。
- 優點:準確,沒有邊界尖峰。
- 缺點:記憶體用量較高(儲存時間戳),或使用加權平均的滑動窗口計數器。
3. 令牌桶
一個桶子裝有令牌。令牌以固定速率添加。每個請求消耗一個令牌。如果桶子空了,請求就會被拒絕。
- 優點:允許突發流量達到桶子大小,然後強制執行平均速率。
- 缺點:需要為每個客戶端儲存令牌數量和上次填充時間。
4. 漏桶
請求進入佇列(桶子),並以恆定速率處理。如果佇列已滿,請求就會被丟棄。
- 優點:將流量平滑到穩定速率,非常適合保護下游服務。
- 缺點:增加延遲;不適合即時 API。
| 演算法 | 突發處理 | 記憶體 | 使用案例 |
|---|---|---|---|
| 固定窗口 | 差 | 低 | 簡單 API |
| 滑動窗口 | 好 | 中 | 通用 |
| 令牌桶 | 極佳 | 中 | 允許突發的 API |
| 漏桶 | 無 | 中 | 平滑流量 |
在 Node.js 中實作速率限制
我們將使用 Express 和 Redis 實作一個令牌桶速率限制器,以實現分散式狀態。當您有多個伺服器執行個體時,Redis 至關重要。
步驟 1:安裝依賴項目
npm install express redis
步驟 2:建立速率限制中介軟體
const redis = require('redis');
const client = redis.createClient();
async function tokenBucketLimiter(req, res, next) {
const key = `rate_limit:${req.ip}`;
const capacity = 10; // max tokens
const refillRate = 1; // tokens per second
const now = Date.now();
const data = await client.hGetAll(key);
let tokens = data.tokens ? parseFloat(data.tokens) : capacity;
let lastRefill = data.lastRefill ? parseInt(data.lastRefill) : now;
// Refill tokens based on elapsed time
const elapsed = (now - lastRefill) / 1000;
tokens = Math.min(capacity, tokens + elapsed * refillRate);
if (tokens < 1) {
return res.status(429).json({ error: 'Too many requests' });
}
tokens -= 1;
await client.hSet(key, {
tokens: tokens.toString(),
lastRefill: now.toString()
});
await client.expire(key, 60); // auto-cleanup
next();
}
app.use(tokenBucketLimiter);
這個中介軟體會原子性地檢查並更新令牌數量。在生產環境中,請使用 Redis 交易或 Lua 指令碼來避免競爭條件。
在 Nginx 中實作速率限制
Nginx 透過 limit_req 模組提供內建的速率限制。它使用漏桶演算法。
步驟 1:定義速率限制區域
在 nginx.conf 的 http 區塊中:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
這會建立一個名為 api 的 10MB 區域,允許每個 IP 每秒 10 個請求。
步驟 2:套用限制
在您的 location 區塊中:
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
burst=20 允許最多 20 個請求的短暫突發。nodelay 會立即處理突發請求,而不是將它們排入佇列。
API 速率限制的最佳實踐
- 傳回適當的狀態碼:使用
429 Too Many Requests並包含Retry-After標頭。 - 正確識別客戶端:盡可能使用 API 金鑰或使用者 ID 而非 IP 位址,因為 IP 可能被共享(NAT)或偽造。
- 分散式狀態:對於多執行個體部署,請使用 Redis 或類似的儲存。
- 記錄與監控:追蹤速率限制觸發次數以偵測攻擊並調整閾值。像是 Nginx Log Analyzer 這類工具可以協助您分析 429 回應並識別濫用 IP。
- 溝通限制:在 API 文件中記錄速率限制,並包含
X-RateLimit-Limit、X-RateLimit-Remaining等標頭。
常見問題
速率限制與節流有何不同?
速率限制會封鎖超過閾值的請求,而節流會減慢它們的速度(例如透過佇列或延遲)。速率限制是二元式的;節流是漸進式的。
我應該使用哪種速率限制演算法?
對於大多數 API,令牌桶提供了良好的平衡:它允許突發,但強制執行平均速率。如果您需要嚴格的平滑處理,請使用漏桶。為了簡單起見,固定窗口適用於低流量 API。
如何為已驗證與匿名使用者處理速率限制?
對匿名使用者(例如按 IP)套用更嚴格的限制,對已驗證使用者(例如按 API 金鑰)套用更寬鬆的限制。您也可以根據訂閱方案實作分層限制。
準備好分析您的 API 流量了嗎?使用我們的 Nginx Log Analyzer 來解析日誌、找出速率限制違規並最佳化您的閾值。