設計限流器(Rate Limiter)
問題定義
設計一個 Rate Limiter,控制用戶或服務在時間窗口內的請求數量,保護後端服務不被過量流量壓垮。
ℹ️限流的位置
Rate Limiter 通常放在 API Gateway 或 Middleware 層,在請求到達業務邏輯之前就做判斷。返回 HTTP 429 Too Many Requests 表示被限流。
需求分析
- 功能性需求:按用戶/IP/API key 限流、可配置限流規則(每秒/每分鐘 N 次)、超過限制返回 HTTP 429 Too Many Requests 並附上
Retry-AfterHeader 告知用戶何時可以重試 - 非功能性需求:低延遲(限流判斷 < 1ms,不能因為限流反而拖慢正常請求)、高可用(限流器掛了要 fail-open 還是 fail-close?)、分散式環境下準確
- 限流粒度:用戶級(每個登入用戶 100 req/min)、IP 級(防爬蟲,每個 IP 1,000 req/min)、API 級(某個耗資源的 API 全域 500 req/s)、全局級(系統總流量上限 50,000 req/s)。可以疊加多層,如「每個用戶每分鐘 100 次 + 全域每秒 10,000 次」
- 軟限流 vs 硬限流:軟限流允許短暫超額(如允許超過 10%,超過後降級回應而非拒絕),適合用戶體驗優先的場景。硬限流嚴格拒絕超額請求,適合保護後端不被壓垮或防止 API 濫用
- fail-open vs fail-close:限流器本身出問題時,fail-open = 放行所有請求(保可用性),fail-close = 拒絕所有請求(保安全性)。大多數場景選 fail-open,因為限流器故障時寧可暫時不限流也不要整個服務停擺
注意事項
⚠️分散式環境的挑戰
單機限流很簡單(記憶體計數器就好),但多台伺服器時,每台的計數器是獨立的。用戶的請求被分散到不同伺服器,每台都覺得沒超限,實際上總量早就超了。必須用 Redis 等集中式儲存來做分散式限流。
設計流程
選擇限流演算法
Token Bucket、Leaky Bucket、Fixed Window、Sliding Window
決定限流粒度
按用戶 ID、IP、API Key 或全局
設定限流規則
定義每個粒度的速率限制(如每秒 10 次)
選擇儲存方案
單機用記憶體,分散式用 Redis
整合到 API Gateway
作為 Middleware 在請求處理前攔截
回應 Header 設計
返回 X-RateLimit-Remaining 等資訊
架構設計
Token Bucket 演算法(C#)
public class TokenBucket
{
private readonly int _capacity; // 桶的最大容量
private readonly double _refillRate; // 每秒填充幾個 token
private double _tokens;
private DateTime _lastRefill;
private readonly object _lock = new();
public TokenBucket(int capacity, double refillRate)
{
_capacity = capacity;
_refillRate = refillRate;
_tokens = capacity;
_lastRefill = DateTime.UtcNow;
}
public bool TryConsume(int tokens = 1)
{
lock (_lock)
{
Refill();
if (_tokens < tokens) return false;
_tokens -= tokens;
return true;
}
}
private void Refill()
{
var now = DateTime.UtcNow;
var elapsed = (now - _lastRefill).TotalSeconds;
_tokens = Math.Min(_capacity, _tokens + elapsed * _refillRate);
_lastRefill = now;
}
}分散式 Sliding Window(Redis + TypeScript)
async function isAllowed(
userId: string,
windowMs: number,
maxRequests: number
): Promise<boolean> {
const now = Date.now();
const key = `ratelimit:${userId}`;
// Redis pipeline: 原子操作
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, now - windowMs); // 移除過期紀錄
pipeline.zadd(key, now, `${now}:${Math.random()}`); // 加入當前請求
pipeline.zcard(key); // 計算窗口內請求數
pipeline.expire(key, Math.ceil(windowMs / 1000)); // 設定 key 過期
const results = await pipeline.exec();
const count = results![2][1] as number;
return count <= maxRequests;
}
// Middleware
app.use(async (req, res, next) => {
const allowed = await isAllowed(req.userId, 60_000, 100); // 每分鐘 100 次
if (!allowed) {
res.set("Retry-After", "60");
res.status(429).json({ error: "Rate limit exceeded" });
return;
}
next();
});架構圖
架構圖解讀:Client 請求先到 API Gateway,Gateway 內嵌的 Rate Limiter Middleware 攔截請求。Middleware 從 Rules Config 載入限流規則(如「用戶級每分鐘 100 次」),然後向 Redis 讀取/更新該用戶的計數器。如果未超限,請求被放行到 Backend API;如果超限,直接回傳 429 給 Client,請求不會到達後端。
實戰補充
💡演算法選擇指南
如何選擇限流演算法?根據你的場景需求:
| 演算法 | 允許突發 | 記憶體用量 | 精確度 | 實作複雜度 | 適合場景 | |--------|---------|-----------|--------|-----------|---------| | Token Bucket | 是(桶滿時可瞬間消耗) | 低(2 個變數) | 中 | 低 | API Gateway 通用限流(最推薦) | | Leaky Bucket | 否(固定速率流出) | 低(佇列) | 中 | 低 | 需要平滑流量的場景(如資料庫寫入) | | Fixed Window | 否 | 極低(1 個計數器) | 低(邊界問題) | 極低 | 粗略限流,對精確度要求不高 | | Sliding Window Log | 否 | 高(存每個請求時間戳) | 最高 | 中 | 需要精確計數且流量不大 | | Sliding Window Counter | 部分 | 低 | 高 | 中 | Fixed Window 精度不夠但 Log 記憶體太大時的折衷 |
經驗法則:80% 的場景用 Token Bucket 就夠了。需要精確計數(如計費 API)用 Sliding Window Counter。需要絕對平滑的輸出速率用 Leaky Bucket。
面試常見問題與參考答案:
Q:如果有人用大量不同 IP 發動 DDoS,IP 級限流還有用嗎? A:單獨的 IP 限流不夠,需要多層防禦:IP 限流 + 全局限流 + 行為分析(如正常用戶不會每秒請求同一個 API 100 次)。更上層可以用 WAF(如 Cloudflare)在網路邊緣就擋掉。
Q:限流規則怎麼配置和更新? A:用 Config Service(如 etcd、Consul)集中管理,限流器定期拉取最新規則(如每 30 秒)。這樣不需要重啟服務就能動態調整限流閾值。
理解測驗
🤔 為什麼 Fixed Window Counter 有「邊界問題」?
🤔 在分散式環境中,為什麼不能只用每台伺服器的本地記憶體做限流?
🤔 Token Bucket 演算法中,「桶的容量」代表什麼意義?
重點整理
💡一句話記住
Gateway 攔截、Redis 計數、429 拒絕。 記憶口訣:「攔數拒」— 在 Gateway 攔截、用 Redis 計數、超限就 429 拒絕。演算法八成選 Token Bucket。
| 概念 | 說明 | |------|------| | Token Bucket | 允許突發流量,最常用的演算法 | | Sliding Window | 精確計數,沒有邊界問題 | | HTTP 429 | Rate Limit 超限的標準回應碼 | | Redis | 分散式限流的集中式計數器 | | API Gateway | Rate Limiter 的最佳部署位置 |