3240 字
约 10 分钟
17
S02-A08 · 限流与背压的位置:放在链路的哪一层

网关上限流配了 1000 QPS,压测也过了。上线后后端照样被打穿,日志里全是 503 和连接池耗尽。查下去发现:被打穿的不是入口流量,而是某个内部服务之间的调用——网关根本看不见那段流量。

这就是限流最典型的失败形态:限流只保护它看得见的流量。位置选错,后面的容量规划、算法选择、阈值调参全部白做。


1. 要保护的是哪种资源

资源 饱和后的表现 瓶颈通常所在 该用什么手段
带宽/入口连接 新建连接超时、TLS 握手慢 边缘、LB、NAT 连接数上限、速率限制
线程/连接池 排队时间线性上涨,随后雪崩 服务进程内 并发数限制、排队 + 背压
后端计算(DB、下游 RPC) 慢查询堆积、连接池耗尽 资源池一侧 客户端侧配额、池大小与隔离
外部配额/成本 402、429、账单飙升 第三方 配额分配 + 降级

一个服务同时受多种资源约束,要保护的是"最先饱和的那一种",而不是流量数字。判断方法:逐步加压看哪个指标先拐——线程池队列先涨,还是 DB 连接先耗尽。


2. 五个可能的位置与各自的能力边界

位置 看得见的维度 能力强项 盲区
客户端 自己的调用节奏 零成本、最先生效 不可信,别人可以不带
CDN/边缘 IP、UA、路径、地域 挡爬虫与刷量,成本最低 对已登录用户与内部调用无效
网关 路由、租户、API key 全局限流、统一策略 看不到服务间调用;不知道下游真实负载
服务自身 接口、方法、租户上下文 能按真实成本限流(这条 SQL 多重) 各自为政,全局阈值会漂移
资源池(连接池/线程池/信号量) 并发占用 最后一道硬闸,永远有效 只能拒绝/排队,无法区分请求价值

结论:网关挡"不该进来的",服务挡"进来太多的",资源池兜底"已经太晚的"。三层不冗余,因为输入信息不同。服务侧不可替代,是因为只有它知道一次请求要花几次 DB 查询——网关按 QPS 计数,服务按成本计数。


3. 为什么单层限流必然漏

任何单层限流器只知道经过它的请求。三类流量天然绕过网关:

1. 内部调用     svc-a → svc-b            不经过入口网关
2. 重试与补偿   gateway 放过 1 次 → svc 内部 retry 3 次 → 实际 3 倍成本
3. 异步/定时    MQ 消费、定时任务、批处理  没有 HTTP 入口
# 本地限流的真实阈值 = 单实例配额 × 实例数;扩缩容即漂移
单实例 100 QPS × 12 实例 = 1200   # 昨天 10 实例,今天 12 实例,网关阈值没动
方案 阈值稳定性 每次请求开销 失效场景
本地(进程内计数器) 随实例数线性漂移 纳秒级 扩缩容、滚动发布时新旧实例并存
全局(中心化计数) 稳定 一次网络往返(亚毫秒到数毫秒) 计数中心抖动 → 全站限流失效
混合(本地令牌 + 周期配额) 近似稳定 分摊后接近本地 配额周期内突发分布不均

滚动发布是最容易忽略的漂移源:新旧版本各 6 个实例,实现或默认值不同,发布窗口里阈值就翻倍。


4. 维度选择与误伤

维度 挡得住 误伤 绕过成本
全局 整体过载 一个租户的大促拖垮所有人 无(本来就不是防单点)
租户/用户 单租户刷量 共享出口 IP 的大客户被连带 注册新账号
IP 爬虫、脚本 NAT 后的整个办公网 换代理
接口/路由 单接口热点 正常业务量的接口被压 换接口
接口 × 租户 单租户刷单接口 维度多 → 每格配额小 → 统计不稳 多租户轮换

误伤分析:先写下"哪些正常用户会被这条规则挡住"。若出现"NAT 后的公司"或"全部新用户",规则就该收窄。误伤不是边缘情况,是常态成本。


5. 算法选择

算法 允许突发 内存/状态 适用
固定窗口计数 窗口交界可双倍通过 1 个计数器 粗粒度配额(日调用量)
滑动窗口 平滑,近似精确 分片计数或时间轮 网关级 QPS
令牌桶 允许按桶容量突发 令牌数 + 时间戳 面向用户的 API 配额
漏桶 不允许突发,匀速放行 队列长度 保护脆弱下游(老 DB)
并发数限制(信号量) 与速率无关 一个计数器 线程/连接池这类资源

并发数限流最被低估:它约束"同时在飞的请求数",天然适配线程池与连接池,对长尾更有效——QPS 相同,慢请求会占满线程。


6. 背压与限流不是一回事

对比项 限流 背压
信号方向 入口 → 拒绝 下游 → 上游减速
依据 预设阈值 实时队列深度/延迟
失败表现 429/503,明确拒绝 生产者阻塞或丢弃,延迟升高
需要什么 计数与阈值 有界队列 + 阻塞或异步反馈
// 背压:有界队列 + 拒绝策略把压力反弹给调用方(而非无限堆积)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    8, 8, 0, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(200),          // 有界:背压的前提
    new ThreadPoolExecutor.AbortPolicy());  // 满了就抛,让调用方感知

限流是开环的:阈值写死,不管下游死活。背压是闭环的:下游慢了上游自动减速。只有限流没有背压,阈值调大一点就重新打穿;只有背压没有限流,压力会一路顶到入口变成超时。


7. 队列的三种处置与延迟预算

处置 用户看到 成本 适用
排队(有界) 变慢、可能超时 延迟预算 下游短时可恢复,请求可等待
拒绝(429/503) 立刻失败 一次错误响应 请求不可等待,或过载会传染
降级(返回旧值/默认) 结果不新鲜但可用 一致性 读路径、缓存可兜底

延迟预算决定排队深度:入口 P99 目标 500 ms、单请求 200 ms,队列只容得下 2–3 个等待周期。队列长度 = 可接受延迟 ÷ 单请求处理时间,比"队列设 1000"有依据。


8. 限流与重试的乘法关系

客户端重试 3 次、指数退避缺失 → 被拒请求立刻变成 3 倍入流量
1000 QPS 上限被触发 → 1000 × 3 = 3000 打在同一个入口 → 重新过载
// 重试只对幂等且可恢复的错误生效;429 必须遵守 Retry-After
if (resp.status() == 429) {
    sleep(parseRetryAfter(resp));   // 服务端给的节流指令,客户端要照做
    return retryOnce();             // 且只重试一次,不再放大
}

限流触发重试会形成正反馈:拒绝 → 重试 → 更多拒绝。两个约束:被限流的响应必须带 Retry-After;重试预算计入总配额。


9. 阈值怎么定,而不是怎么猜

# 1) 单实例容量测试:固定并发,逐步加压,记录拐点
for c in 50 100 150 200 250; do
  wrk -t4 -c$c -d60s --latency http://TARGET/ENDPOINT > out-$c.txt
done
# 2) 找 P99 开始抬升但错误率仍为 0 的档位,记为单实例安全容量 C
# 3) 集群阈值 = C × 实例数 × 安全系数(0.6–0.8);预留余量给发布与抖动

按版本核对的三件事:限流器是否共享状态(本地/全局)、Retry-After 是否被网关透传、熔断与限流是否串联(顺序不同,被拒请求的统计口径就不同)。


10. 分布式限流的成本与近似替代

方案 每请求开销 精度 失效后果
每次请求查 Redis 一次往返 高 Redis 抖动 → 全站 5xx 或全放行
本地预取配额(每 100 ms 取一批) 分摊到 1/100 近似 周期内分布不均,跨实例有小误差
一致性哈希分片计数 一次往返到固定节点 中 热点租户集中在一个分片
只做本地 + 事后对账 零 低 发布/扩缩容窗口漂移

判据不是精度,而是故障时要哪种失败:Redis 不可用时"全放行"还是"全拒绝"。必须显式写下来,否则默认谁先写谁赢。


11. 可观测:被拒之后发生了什么

指标 为什么要看 异常读法
限流命中率 阈值是否贴近真实容量 长期为 0 = 阈值虚高,等于没限流
被拒请求的后续行为 是重试、放弃还是换接口 重试占比高 = 第 8 节的放大在发生
拒绝与下游饱和的时间关系 拒绝是否真的保护了后端 拒绝很多但 DB 仍饱和 = 位置选错
单实例配额分布 是否均匀分摊 分布倾斜 = 哈希或路由不均

12. 排错

现象 最可能的原因 处理
网关限流生效,后端仍被打穿 内部调用/重试/异步流量绕过网关 在服务侧补一层按成本的限流
扩缩容后限流过松或过紧 本地限流阈值 × 实例数漂移 改全局或本地预取配额
上了限流,P99 反而变差 排队深度超出延迟预算 缩短队列、改拒绝或降级
被拒请求量远高于下游实际压力 维度选错(如按 IP 挡了 NAT) 改租户/接口维度并加豁免
限流触发后总流量翻倍 客户端对 429 立即重试 返回 Retry-After,重试纳入预算

13. 边界:这些场景不该用限流

场景 更合适的做法 理由
单实例、低流量内部服务 池大小 + 超时 + 背压 限流器的运维成本高于收益
请求不可拒绝(支付回调、消息投递) 排队 + 削峰 + 幂等 拒绝等于丢数据
下游只是短时抖动 熔断 + 重试预算 限流会把抖动固化成长期拒绝
突发是业务本身(秒杀、开票) 预约/排队/令牌发放 限流只是把失败提前
成本/配额类约束 配额分配与降级 与速率无关,需要按量核算

14. 自测

  1. 网关已限流,后端仍被打穿,最先怀疑哪三类流量?
  2. 为什么服务侧限流无法被网关替代?它多知道什么?
  3. 本地限流在滚动发布窗口里会发生什么?
  4. 并发数限流为什么对长尾请求比 QPS 限流更有效?
  5. 限流与背压各自的开环/闭环性质,决定了它们必须同时存在吗?
  6. 限流器本身故障时,"全放行"和"全拒绝"分别保护了什么?
S02-A08 · 限流与背压的位置:放在链路的哪一层
https://www.clxhxhhr.top/posts/6225/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。