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; // 最大令牌数
const refillRate = 1; // 每秒令牌数
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;
// 根据经过的时间重新填充令牌
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); // 自动清理
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 日志分析器 这样的工具可以帮助你分析 429 响应并识别滥用 IP。
- 沟通限制:在 API 文档中记录速率限制,并包含
X-RateLimit-Limit、X-RateLimit-Remaining等头。
常见问题
限流和节流的区别是什么?
限流阻止超过阈值的请求,而节流则减慢它们(例如,通过排队或延迟)。限流是二元的;节流是渐进的。
我应该使用哪种限流算法?
对于大多数 API,令牌桶提供了良好的平衡:它允许突发,但强制执行平均速率。如果需要严格的平滑,使用漏桶。为了简单,固定窗口适用于低流量 API。
如何处理已认证用户与匿名用户的限流?
对匿名用户(例如,按 IP)应用更严格的限制,对已认证用户(例如,按 API 密钥)应用更宽松的限制。你还可以根据订阅计划实现分层限制。
准备好分析你的 API 流量了吗?使用我们的 Nginx 日志分析器 来解析日志,发现限流违规,并优化你的阈值。