一个开放接口按 IP 限流,每分钟 60 次。上线后一家合作方接入,他们的服务部署在三个出口 IP 上,每个 IP 每分钟发 40 次——总量 120 次,看起来合规,实际业务量已经超过后端承载。而另一边,一个网吧里的 30 个用户共用一个出口 IP,第 31 个用户被挡在门外。
限流的维度选择比阈值更难:按 IP 限,惩罚的是"共享出口";按用户限,惩罚的是"一个坏账号";按全局限,惩罚的是所有人。
维度选择还有一个"可伪造性"问题:IP 可以换,用户 ID 可以注册,只有接入方身份需要资质。所以反滥用强度与维度成本成正比——这就解释了为什么开放平台总是先做接入方认证,再做限流。
选错维度,规则越精确,误伤越具体。
1. 五种限流维度
| 维度 | 限的是谁 |
|---|---|
| IP / 网段 | 出口地址 |
| 用户 / 账号 | 身份 |
| 应用(client_id) | 接入方 |
| 接口(endpoint) | 资源 |
| 全局 | 全部流量 |
前两种最常见,也最容易误伤:IP 是网络拓扑的副产品,不是用户身份。它会在 NAT、CDN、移动网关下把大量用户合并成一个,也会让"换 IP 即绕过"变得极其容易。
第二项(用户维度)有个成本问题:它要求每个请求都能拿到已认证的用户身份。匿名接口与内部调用拿不到,于是实践中常常退化成"IP + 用户"混用——两套规则的阈值要分别设,不能简单叠加。
第三种是开放平台的正解:按接入方限流,责任与配额都落到签约主体上。它需要身份体系,但一旦有了,配额、计费、风控都能复用同一个键。
2. 组合维度:两层限流
| 层 | 作用 |
|---|---|
| 第一层:接入方总量 | 保护后端 |
| 第二层:接入方 × 接口 | 防止单一接口占满 |
| 第三层:用户级 | 防止单账号滥用 |
只有第一层时,一个接入方可以用全部配额打同一个最贵的接口;只有第二层时,总量会失控。所以生产上通常是两层并联,取"最先触发"的那一层。
// 两层限流:总量 + 单接口,各自独立计数
rate_limit:
- key: "client:{client_id}"
rate: 1000/min # 接入方总量
action: reject
- key: "client:{client_id}:{endpoint}"
rate: 100/min # 单接口
action: reject
- key: "user:{user_id}"
rate: 20/min # 单账号(防脚本)
action: throttle
第三层用 throttle(延迟放行)而不是 reject,因为单用户的突发是正常行为,直接拒绝的体验很差。
3. 阈值怎么定
| 依据 | 说明 |
|---|---|
| 后端容量 / 安全系数 | 保护性上限 |
| 历史 P99 流量 × 2 | 常规上限 |
| 签约配额 | 商业约束 |
| 单用户行为分布 | 反滥用阈值 |
第一项给出"绝对不能超过",第二项给出"正常情况下不会触发"。两者取小,因为保护后端优先。
第三项(签约配额)不是技术判断而是商业判断:配额是承诺,技术实现必须先能兑现它。常见错误是先卖出配额,再发现单接口限流让承诺不可达。
第四项需要看分布而不是平均值:单个用户正常每分钟 3 次,但 P99 用户是 40 次(批量操作)。阈值若按平均值定,会把正常的大客户挡掉。
// 阈值定法:三种上限取小
保护上限:后端承载 3000 QPS × 0.6 = 1800/min
常规上限:历史峰值 900/min × 2 = 1800/min
配额上限:签约 2000/min
→ 取 1800/min;再按接口拆分,单接口不超过 300/min
4. 拒绝的形态
| 形态 | 适用 |
|---|---|
| 立即拒绝(429 + Retry-After) | 明确超额 |
| 排队(延迟放行) | 短时突发 |
| 降级(返回缓存或简化结果) | 读接口 |
| 采样丢弃 | 埋点类流量 |
第四项(采样丢弃)只对"丢了也无所谓"的流量成立:埋点、心跳、日志上报这类可以丢,业务请求不能丢。混用会导致"关键请求被当成可丢流量"。
第一项必须带 Retry-After,否则调用方只能盲目重试,而盲目重试正是把限流变成雪崩的机制。
第三项在有缓存时最友好:限流触发时返回稍旧的数据,而不是错误,用户几乎无感。
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 1800
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1762777200
Content-Type: application/json
{"code":"RATE_LIMITED","retry_after_ms":12000,"scope":"client"}
上线前还有一步值得做:把"谁在用哪个维度"画成一张图。同一套系统里常有网关限流、应用内限流、数据库层限流三处,各按不同维度计数——用户被拒绝时看到的是最外层的原因,而真实约束可能在里层。
// 三处限流的常见组合与冲突
网关:client_id 1000/min ← 对外承诺
应用:endpoint 100/min ← 实际约束(更严)
数据库:连接池 20 ← 隐性约束(最严,但无 429)
// 结果是:用户看到"接入方超限",实际是连接池排队
5. 分布式计数的三个问题
| 问题 | 后果 |
|---|---|
| 计数存储成为瓶颈 | 限流本身拖慢请求 |
| 多节点计数不一致 | 实际放行超过阈值 |
| 窗口边界抖动 | 双倍突发 |
第一项在 Redis 上很现实:每个请求都要一次 INCR + EXPIRE,QPS 高时这部分就是主要延迟。缓解方式包括本地预聚合(每节点本地计数、定期合并)或用滑动窗口近似。
这三项之外还有一个"记账口径"问题:限流计数是否包含被拒绝的请求。包含的话,一个已经在超额的调用方永远无法恢复;不包含的话,攻击流量可以持续探测。常见折中是"拒绝的请求按 0.1 权重计数"。
第三项是固定窗口的固有缺陷:窗口边界前 100ms 与后 100ms 各发满额度,等于瞬间双倍。滑动窗口或令牌桶能缓解。
-- Redis 令牌桶:一次调用完成取令牌,避免多次往返
-- KEYS[1]=bucket ARGV: rate, burst, now_ms, cost
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(b[1]) or tonumber(ARGV[2])
local ts = tonumber(b[2]) or tonumber(ARGV[3])
local delta = math.max(0, tonumber(ARGV[3]) - ts) / 1000
tokens = math.min(tonumber(ARGV[2]), tokens + delta * tonumber(ARGV[1]))
if tokens < tonumber(ARGV[4]) then return {0, math.floor(tokens)} end
tokens = tokens - tonumber(ARGV[4])
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', ARGV[3])
redis.call('EXPIRE', KEYS[1], 60)
return {1, math.floor(tokens)}
6. 排错:五种"限流上线后问题更多"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 正常用户被挡 | 维度选错(IP/NAT) | 换用户或接入方维度 |
| 调用方疯狂重试 | 429 无 Retry-After | 补齐响应头 |
| 后端仍被打满 | 阈值高于容量 | 按容量重算 |
| 限流组件自己超时 | 计数存储瓶颈 | 本地预聚合 |
| 合作方投诉误伤 | 两层规则叠加过严 | 分层设不同阈值 |
第五项值得说明:总量与单接口两层同时触发时,实际可用额度是两者的最小值,而文档里通常只写了总量。合作方按总量规划,实际被单接口卡住——这是最常见的投诉来源。
7. 边界:什么情况不必精细限流
| 场景 | 为什么 | 做法 |
|---|---|---|
| 内部可信调用 | 有其它约束 | 只做全局兜底 |
| 流量远低于容量 | 无风险 | 只告警不拒绝 |
| 只读、可缓存的接口 | 可降级 | 用降级代替拒绝 |
| 已有网关配额管理 | 平台已提供 | 跟随平台 |
还有一条与"上线方式"有关的边界:限流应当先以"只记录不拒绝"的观察模式跑一周,看清真实分布再开拒绝。直接上线拒绝规则,误伤的第一个投诉往往来自最大的客户。
判断口径一句话:这次限理想保护的是"后端"还是"公平"。保护后端用总量维度;讲公平用身份维度——两者混在一层里,必然有一边失效。
8. 自测
- 五种维度里,哪两种最容易误伤?为什么"IP 是网络拓扑的副产品"会导致两个方向的错误?
- 为什么只有"接入方总量"一层时后端仍可能被打满?两层并联时以哪一层为准?
- 阈值四种依据如何取?为什么单用户阈值要看 P99 而不是平均值?
- 429 为什么必须带
Retry-After?缺它会引发什么连锁反应? - 分布式计数的三个问题里,哪一个会让限流组件本身成为瓶颈?固定窗口的"双倍突发"如何产生?
- 判断"限流保护的是后端还是公平"的那个问题是什么?