速率限制器
速率限制器
在网络系统中,**速率限制器(Rate Limiter)**用于控制客户端或服务端的请求速率,防止资源滥用、降低系统负载并提高稳定性。例如:
- 限制某 IP 每分钟最多发送 10 篇博客。
- 每用户每天领取优惠券上限为 10 次。
- 短信验证码每 60 秒发送一次。
限流的好处包括防止 DoS 攻击、保护服务端资源、降低运营成本等。本文将深入探讨速率限制器的设计要求、类型、实现方式以及在单机和分布式环境下的差异。
速率限制器的核心要求
一个优秀的速率限制器需满足以下要求:
- 精准性:准确记录和计算请求速率,不漏计任何请求。
- 低延迟:尽量不增加请求响应时间。
- 低资源占用:减少对 CPU、内存等资源的消耗。
- 用户通知:通过 HTTP 状态码(如 429 Too Many Requests)告知客户端限制情况,并提供相关信息(如剩余请求数)。
- 容错性:即使限流器故障,也不应影响系统整体运行。
速率限制器的类型
1. 客户端限流
在客户端实现限流逻辑,开发成本低,但安全性较差:
- 客户端可轻易绕过限制(如修改代码)。
- 多客户端场景下,各客户端独立计数,无法统一管理。
适用场景:对安全性要求低的场景,或作为服务端限流的补充。
实现方案:
- 重写 fetch 和 XMLHttpRequest:通过改写浏览器原生的 fetch 和 XMLHttpRequest 方法,在每次发起 HTTP 请求前插入限流逻辑
- 使用 Service Worker:Service Worker 作为浏览器和网络之间的代理,可拦截所有符合其作用域的网络请求,包括 fetch 请求、页面导航、资源加载等。关于 Service Worker 的更多内容,可以参考我的另一篇文章
2. 服务端限流
服务端限流更安全,常见算法包括:
- 令牌桶(Token Bucket):以固定速率向桶中添加令牌,请求需消耗令牌,适用于突发流量场景。
- 漏桶(Leaky Bucket):请求以固定速率流出,超出部分被丢弃,适合平滑流量。
- 固定窗口计数器:在固定时间窗口内计数,简单但可能导致窗口边界突刺问题。
- 滑动窗口:通过记录请求时间戳实现更细粒度的控制,适合高精度场景。
实现示例(以 Redis 和滑动窗口为例):
-- Redis Lua 脚本:滑动窗口限流
local key = KEYS[1] -- 用户标识
local window = tonumber(ARGV[1]) -- 时间窗口(秒)
local limit = tonumber(ARGV[2]) -- 请求上限
local now = tonumber(ARGV[3]) -- 当前时间戳
-- 删除窗口外的旧请求
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 添加新请求时间戳
redis.call('ZADD', key, now, now)
return 1 -- 允许请求
else
return 0 -- 拒绝请求
end
3. API 网关限流
云服务中的 API 网关(如 AWS API Gateway、Nginx)通常内置限流功能,支持速率限制、SSL 终止、IP 白名单等。配置简单,但灵活性较低,适合需求不复杂的场景。
推荐场景:中小型项目或已有 API 网关的项目。
HTTP 请求的限流配置
当请求被限制时,服务端应返回 429 Too Many Requests 状态码,并在响应头中提供以下信息:
- X-RateLimit-Limit:允许的最大请求数(如 100)。
- X-RateLimit-Remaining:当前剩余请求数(如 10)。
- X-RateLimit-Reset:下次重置的 Unix 时间戳(秒,如 1726339200)。
- X-RateLimit-Retry-After:建议等待时间(秒,如 10)。
单机 vs 分布式限流
单机限流
在单服务器环境中,限流实现较为简单:
- 定义限流规则,存储在磁盘中,工作时,需要从磁盘上读取规则,然后存储在某个缓存中使用。
- 请求到达时,限流器从缓存加载规则,依据算法判断是否允许。
- 若被限制,返回 429 状态码;否则放行至业务逻辑。
优点:实现简单,适合小型系统。
缺点:无法应对分布式场景下的高并发。
分布式限流
分布式环境可以容许更大用户量,但是需解决以下问题:
- 竞争问题:多请求并发修改计数器,可能导致计数不准。
- 解决方案:
- 使用 Redis 的
INCR
命令(原子性操作)。 - 使用 Redis Lua 脚本保证操作原子性。
- 使用 Redis Bitmap 记录请求时间戳,适合高精度场景。
- 使用 Redis 的
- 解决方案:
- 同步问题:多个限流器独立计数,无法统一管理。
- 解决方案:
- 集中存储:使用 Redis 或 ZooKeeper 存储计数数据。
- 一致性哈希:将同一客户端请求分配到固定节点,减少同步开销。
- 分布式锁:通过 Redis 或 ZooKeeper 实现全局锁,但需权衡性能。
- 解决方案:
总结
速率限制器是构建高可用系统的关键组件。单机限流实现简单,适合小型应用;分布式限流需解决竞争和同步问题,推荐使用 Redis 等集中存储方案。选择合适的限流算法(如令牌桶、滑动窗口)并结合 API 网关或自定义实现,可以满足不同场景的需求。