2408 字
约 8 分钟
18
S17-A25 · 限流维度的选择:按什么限,决定了谁被限

一个开放接口按 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. 自测

  1. 五种维度里,哪两种最容易误伤?为什么"IP 是网络拓扑的副产品"会导致两个方向的错误?
  2. 为什么只有"接入方总量"一层时后端仍可能被打满?两层并联时以哪一层为准?
  3. 阈值四种依据如何取?为什么单用户阈值要看 P99 而不是平均值?
  4. 429 为什么必须带 Retry-After?缺它会引发什么连锁反应?
  5. 分布式计数的三个问题里,哪一个会让限流组件本身成为瓶颈?固定窗口的"双倍突发"如何产生?
  6. 判断"限流保护的是后端还是公平"的那个问题是什么?
S17-A25 · 限流维度的选择:按什么限,决定了谁被限
https://www.clxhxhhr.top/posts/6465/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。