IP 限流方案
常用的限流分类是:基于资源的限流和基于时间的限流。
限流的本质和目的
限流的本质是系统的一种保护机制,目的是在资源有限的情况下,通过控制请求的流量,避免资源的过载和服务的崩溃,实现流量的可控性和服务的高可用性。
我们从标题可以看出,我们是针对 IP 进行限流,所以我们应该要做的三个事情是:
- 识别:如何统计 IP 的请求量
- 判断:如何判断 IP 是否超过限制
- 处理:如何处理超过限制的 IP
一、如何统计 IP 的请求量
1.1 识别 IP
在统计 IP 请求量之前,首先要确保能够正确地识别请求方的 IP 地址。
方法:
直接从 HTTP 请求中获取:
- 通过服务器的网络接口直接获取客户端 IP 地址。
- 示例(Express.js):javascript
const ip = req.ip; // 或 req.connection.remoteAddress
通过
X-Forwarded-For
标头识别:- 在反向代理或负载均衡环境下,请求的真实 IP 通常存储在
X-Forwarded-For
头中。 - 示例(Express.js):javascript
const ip = req.headers["x-forwarded-for"] || req.ip;
- 在反向代理或负载均衡环境下,请求的真实 IP 通常存储在
处理多级代理的情况:
X-Forwarded-For
可能包含多个 IP(多个代理经过的路径),真实 IP 通常是最左边的 IP。- 示例:javascript
const ip = (req.headers["x-forwarded-for"] || req.ip).split(",")[0].trim();
特殊情况处理:
- 若有 VPN 或代理环境,可能需要结合其他用户标识(如
user-agent
)进行辅助识别。
- 若有 VPN 或代理环境,可能需要结合其他用户标识(如
1.2 统计 IP 请求量
识别 IP 后,需要在短时间内统计每个 IP 的请求数量。以下是常见统计方法:
1. 基于内存的统计
适用场景:单实例服务器,小规模流量。
实现方式:使用哈希表记录每个 IP 的请求量。
示例(Node.js):
javascriptconst ipRequests = {}; function logRequest(ip) { if (!ipRequests[ip]) { ipRequests[ip] = 1; } else { ipRequests[ip]++; } }
优点:简单高效,低延迟。
缺点:不适合分布式系统或长时间统计。
2. 基于 Redis 的分布式统计
适用场景:分布式环境,大规模流量。
实现方式:使用 Redis 的原子操作和过期时间统计请求量。
示例(使用 Redis 和 Node.js):
javascriptconst redis = require("ioredis"); const client = new redis(); async function logRequest(ip) { const key = `rate_limit:${ip}`; const count = await client.incr(key); if (count === 1) { await client.expire(key, 60); // 设置 60 秒过期 } return count; }
优点:支持分布式,过期时间可控。
缺点:依赖 Redis,增加了外部依赖。
3. 使用滑动窗口计数器
适用场景:需要更平滑的限流效果。
实现方式:
将时间划分为多个小窗口,每个窗口分别统计请求数。
滑动窗口通过加权计算准确统计当前时间段的请求总数。
实现示例:
javascriptconst redis = require("ioredis"); const client = new redis(); async function logRequest(ip) { const key = `sliding_window:${ip}`; const currentTime = Date.now(); const windowSize = 60 * 1000; // 60 秒 // 删除过期请求 await client.zremrangebyscore(key, 0, currentTime - windowSize); // 添加当前请求 await client.zadd(key, currentTime, currentTime); // 获取当前窗口内的请求数量 const count = await client.zcard(key); return count; }
优点:限流更平滑,无突刺效应。
缺点:实现复杂,性能略低。
4. 使用令牌桶算法统计
适用场景:需要严格控制请求速率的场景。
实现方式:通过动态生成令牌来限制请求速率。
实现示例:
javascriptconst redis = require("ioredis"); const client = new redis(); async function logRequest(ip) { const key = `token_bucket:${ip}`; const maxTokens = 100; // 最大令牌数 const refillRate = 1; // 每秒生成令牌数 // 获取当前令牌数 const tokens = await client.get(key); const currentTokens = Math.min( maxTokens, (tokens || maxTokens) + refillRate ); if (currentTokens <= 0) { return false; // 请求被拒绝 } await client.set(key, currentTokens - 1, "EX", 60); // 更新令牌数并设置过期时间 return true; // 请求允许 }
优点:动态控制请求速率,性能稳定。
缺点:实现复杂,需要精确时间同步。
小结
- 小型系统:可以用内存统计方式(如简单计数器)。
- 分布式系统:推荐使用 Redis 进行分布式统计。
- 严格限流:选择滑动窗口或令牌桶算法。
二、如何判断 IP 是否超过限制
判断 IP 是否超过限制一般会通过以下几个方法:
请求计数器:
- 为每个 IP 分配一个计数器,记录每个 IP 在单位时间内发起的请求次数。
- 如果请求次数超过设定的阈值,就判断该 IP 超出了限制。
存储结构:
- 使用内存存储(如 Redis、Memcached 等)来存储每个 IP 在某段时间内的请求次数。
- 设置有效时间(如 1 分钟)来限制计数器的生命周期,防止内存被大量请求数据占用。
时间窗口:
- 每当一个请求到达时,检查该请求时间是否在设定的时间窗口内。如果在窗口内,增加该 IP 的请求计数;如果超过了窗口的有效期,重置计数器。
判断条件:
- 判断每个 IP 的请求次数是否超出限制,比如:
- 如果每分钟请求次数大于某个预设值(如 100 次),则判定该 IP 超限。
- 对于更精细的判断,可以通过滑动窗口或令牌桶算法来处理请求分布。
- 判断每个 IP 的请求次数是否超出限制,比如:
三、如何处理超过限制的 IP
当某个 IP 的请求超过限制时,需要对其进行处理,避免该 IP 继续发起请求。以下是一些常见的处理策略:
返回 429 状态码:
- 如果某个 IP 超过限制,直接返回 HTTP 429 Too Many Requests 状态码,提示用户请求过于频繁。
封禁或限制请求:
- 临时封禁:将超过限制的 IP 加入黑名单,禁止访问一定时间(如 10 分钟、1 小时等),然后恢复。
- 阻止进一步请求:直接拒绝该 IP 的请求,不再响应。
降级服务:
- 对超过限制的 IP,可以降低服务质量,例如返回更低频次的数据或简化的结果,防止过度请求消耗系统资源。
限流重试:
- 提供一个重试的机制,超出限制的 IP 可以等待一段时间(例如 10 秒、1 分钟),然后再重新请求。如果请求次数继续超过限制,则延长等待时间或禁止访问。
日志记录:
- 将超过限制的 IP 进行日志记录,帮助管理员了解哪些 IP 在恶意请求服务,以便做进一步的分析和防御措施。
通知用户:
- 可以通过邮件、短信等方式通知用户,其 IP 已被限流,提示其行为异常并要求改善。
限流方案总结:
- 限流策略选择:根据业务需求选择适合的限流算法(如固定窗口、滑动窗口、漏桶、令牌桶等)。
- 判断超限:使用请求计数器和时间窗口检测 IP 的请求频率,判断是否超过限制。
- 处理超限:对超限的 IP 进行处理,可以返回错误状态码、临时封禁、限速等。