限流(Rate Limiting)
限流是一种基础的流量控制技术,它限制客户端在特定时间窗口内 可以向 API 或 Web 服务发起的请求数量, 保护基础设施免受过载、拒绝服务攻击(DoS/DDoS)、自动化 滥用、恶意爬取以及对有限计算资源的不当使用。在 现代 API 每秒处理来自移动应用、合作伙伴集成、合法机器人 以及潜在恶意攻击者的数百万次请求的场景下,缺乏 适当的限流可能导致运营成本激增、合法用户的性能 严重下降、易受撞库(credential stuffing)和暴力 破解攻击,甚至导致服务完全不可用。有效实现限流 需要深入理解各种算法(token bucket、leaky bucket、fixed window、 sliding window)、关于分布式架构的考量(多个服务器需要 共享速率计数器)、客户端识别策略(IP、API key、JWT、 session)、按用户层级(free、premium、enterprise)的差异化策略,以及 通过标准化 HTTP headers 进行清晰沟通的机制,以告知客户端限制、 当前消耗量和重置时间。本文探讨用于构建健壮限流系统的算法、实现模式、 工具和最佳实践(best practices)。
限流算法
1. Token Bucket(最常用)
# 概念:具有最大 token 容量的 bucket
# Token 以恒定速率添加
# 每个 request 消耗 1 个 token
# 如果 bucket 为空,request 被拒绝
容量:100 tokens
补充速率:10 tokens/秒
优点:
- 允许受控的 bursts(bucket 已满)
- 实现简单
- 对不同流量模式具有灵活性
缺点:
- 可能允许使系统过载的 bursts
- 需要跟踪上次补充
实现:
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRate;
this.lastRefill = Date.now();
}
consume(count = 1) {
this.refill();
if (this.tokens >= count) {
this.tokens -= count;
return true;
}
return false;
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
2. Leaky Bucket
# 概念:以恒定速率漏出的 queue
# Requests 进入 bucket(queue)
# 以恒定速率处理(leak)
# 如果 queue 已满,requests 被拒绝
优点:
- 平滑 bursts(traffic shaping)
- Output rate 恒定且可预测
- 保护 backend 免受 spikes 影响
缺点:
- 可能增加延迟(queueing)
- 实现复杂
用途:
- Traffic shaping
- Network gateways
- 当恒定的 output rate 至关重要时
3. Fixed Window Counter
# 概念:按固定时间窗口的计数器
# 示例:每分钟 100 个 requests
# 在每分钟开始时重置(XX:00, XX:01, XX:02...)
优点:
- 极其简单
- Memory efficient
- 易于理解
缺点:
- Edge case:1 秒内 200 个 requests
(第 1 分钟末尾 100 个,第 2 分钟开头 100 个)
- 在窗口边界允许 bursts
Redis 实现:
INCR user:123:2024-01-15:14:30
EXPIRE user:123:2024-01-15:14:30 60
GET user:123:2024-01-15:14:30 # 如果 > 100,reject
4. Sliding Window Log
# 概念:requests 时间戳的 log
# 移除窗口外的 requests
# 统计滑动窗口内的 requests
优点:
- 完美的精确度
- 没有 fixed window 的 edge cases
- 均匀分布速率
缺点:
- Memory intensive(存储所有 timestamps)
- 在 high traffic 下性能下降
Redis 实现(Sorted Set):
ZADD user:123 <timestamp> <request-id>
ZREMRANGEBYSCORE user:123 0 <timestamp-60s> # 移除旧的
ZCARD user:123 # Count requests
如果 > 100,reject
5. Sliding Window Counter(混合)
# 概念:将 fixed window 与 sliding 结合
# 使用前几个窗口的计数器并加权
# 估算滑动窗口中的速率
示例:限制 100/分钟
当前窗口(14:30):70 个 requests
前一窗口(14:29):90 个 requests
当前窗口中已过去:40s(66.7%)
估算:90 * (1 - 0.667) + 70 = 30 + 70 = 100
优点:
- 精确度接近 sliding log
- Memory efficient(仅 2 个计数器)
- 平滑 bursts
缺点:
- 估算(不精确)
- 比 fixed window 更复杂
分布式限流(Distributed Rate Limiting)
# 问题:多个服务器需要共享 state
# 解决方案:
1. 集中式 Redis(最常用)
const redis = require('redis');
const client = redis.createClient();
async function checkRateLimit(userId) {
const key = \`rate:\$:\${getCurrentWindow()}\`;
const count = await client.incr(key);
if (count === 1) {
await client.expire(key, 60); // 60 seconds
}
return count <= 100; // Limit: 100/min
}
2. Redis Lua Script(Atomic)
const luaScript = \`
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = redis.call('incr', key)
if current == 1 then
redis.call('expire', key, ARGV[2])
end
if current > limit then
return 0
end
return 1
\`;
3. Sticky Sessions + Local Counters
- 始终将用户路由到同一 server
- 服务器上的本地 counter
- 问题:与 auto-scaling 配合不佳
4. Gossip Protocol
- 服务器通过 gossip 共享 state
- Eventual consistency
- 更复杂,用于 high scale
标准 HTTP Headers
# Standards (RFCs)
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000 # Unix timestamp
# 当超出限制时
HTTP/1.1 429 Too Many Requests
Retry-After: 60 # 距离 retry 的秒数
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640000060
{
"error": "Rate limit exceeded",
"retryAfter": 60,
"limit": 100
}
# GitHub 风格(更具信息量)
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1372700873
X-RateLimit-Used: 1
X-RateLimit-Resource: core
使用 Express.js 实现
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const client = redis.createClient();
// Basic rate limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
message: 'Too many requests, please try again later.'
});
app.use('/api/', limiter);
// Redis-based distributed rate limiting
const distributedLimiter = rateLimit({
store: new RedisStore({
client: client,
prefix: 'rate-limit:',
}),
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
});
// Different limits per route
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Stricter for auth endpoints
skipSuccessfulRequests: true, // Don't count successful logins
});
app.post('/api/login', authLimiter, loginHandler);
// Custom key function (rate limit by user ID instead of IP)
const userLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
keyGenerator: (req) => req.user.id, // Requires auth middleware
});
分层限流(Tiered Rate Limiting)
// Different limits based on user tier
function getRateLimit(user) {
const tiers = {
free: { windowMs: 3600000, max: 100 }, // 100/hour
basic: { windowMs: 3600000, max: 1000 }, // 1000/hour
premium: { windowMs: 3600000, max: 10000 }, // 10k/hour
enterprise: { windowMs: 3600000, max: 100000 } // 100k/hour
};
return tiers[user.tier] || tiers.free;
}
app.use(async (req, res, next) => {
const user = await getUserFromToken(req);
const limits = getRateLimit(user);
const limiter = rateLimit({
...limits,
keyGenerator: () => user.id,
});
limiter(req, res, next);
});
工具与服务
- Redis:分布式计数器,自动 expire
- Kong:带有限流 plugin 的 API Gateway
- Nginx rate limiting:limit_req_zone, limit_conn_zone
- Cloudflare:CDN 级别的限流
- AWS API Gateway:内置 throttling
- express-rate-limit:Express 中间件
- Tyk:开源 API gateway
最佳实践(Best Practices)
- 选择合适的算法:通用 API 使用 token bucket,需要精确度时使用 sliding window
- 差异化限制:Auth endpoints 更严格,read-only 更宽松
- 清晰沟通:信息丰富的 headers,有用的错误消息
- Whitelist:可信合作伙伴的 IP、health checks
- Monitoring:当用户频繁达到限制时发出警报
- Graceful degradation:尽可能返回 cached data
- Distributed state:对多服务器部署使用 Redis
- Cost-based limiting:昂贵的操作消耗更多 tokens
建议
对于现代 API,请实现 带 Redis 的 token bucket 以进行分布式限流。 当你需要精确度但不想要内存开销时,使用 sliding window counter。 配置差异化限制:login 为 5 req/min,读取为 100 req/min, 写操作为 10 req/min。始终返回信息丰富的 headers,并在客户端实现 带 exponential backoff 的 retry logic。
