🔍搜索⌘K

给接口网关做限流,我差点被自己骗了

之前给一个小程序接口网关做压测,发现个挺离谱的事:并发一高,请求不是报错,是全部慢到转圈。目标 800 请求/秒的时候,延迟中位数从 90ms 一路爬到 13s,CPU 直接贴满 100%。

我就在前置的 nginx 上加了一道限流,想把超负荷的流量挡在门外。结果这事儿从”限多少”到”怎么确认它真生效了”,每一步都踩坑。下面按”问题 — 现象 — 怎么修 — 结论”捋一遍。

问题

不加限流,过量流量会把 CPU 打满,大家一起排队等死。但限流本身要回答两件事:

  1. 用哪种限流?nginx 有 limit_req(限制速率)和 limit_conn(限制并发)两种,选错等于没限。
  2. 限多少?拍个数早晚出事。

我一开始直觉是用 limit_conn 限并发,后来发现不对——并发是结果不是原因,并发数 = 到达速率 × 处理耗时,这个值系统自己决定,我根本没法拍一个固定数。所以最后选了 limit_req,按速率限。

现象

我先用 limit_req 设了速率,开 dry_run(只记账、不真拦)去量”这个速率下会被拒多少”。跑了 800 请求/秒、3 分钟,结果 429 比例是 0.00%。

这下我懵了:要么 dry_run 真生效了(确实只记账),要么配置根本没进容器镜像(那就是白忙活)。光看 429=0 分不出来。我去 error 日志里搜 limiting requests,结果一条都没有——配置像是没生效。

后来查到真正的原因:我之前把 limit_req_log_level 设成了 warn,但 nginx 的 error_log 默认只记 error 级以上的日志,warn 比 error 轻,被悄悄丢掉了。改成 error 之后重跑,日志里哗哗出来 3 万多条 limiting requests, dry run,这才确认配置进了镜像、dry_run 也真生效了。

第二个现象更阴。我把 dry_run 关掉正式限流,压 500 请求/秒、跑 10 分钟,全程 429 是 0,CPU 也没到 100%,看着很安全。但我去看了下队列里积压的请求数,它每分钟稳定往上涨约 10 个,10 分钟爬到 157(上限是 166)。也就是说再约 1 分钟就要开始 429 了——只是我测的时间不够长,没撞上。短测的”0 个 429”是个假象。

怎么解决

数值怎么定。 墙是 CPU,每个请求吃掉的 CPU 接近固定(约 20ms)。那公式就是:

单台允许速率 = 目标利用率 × (vCPU ÷ 每请求CPU)
            = 0.85 × (2 ÷ 0.0201)
            ≈ 84.6 请求/秒

向下取整取 83。6 台机器合起来约 498 请求/秒。这个数不是蒙的——之前唯一一次全绿实测是 500 请求/秒 ÷ 6 台 = 83.3/台,当时 CPU 83.8%,跟算出来的对得上。

放哪个位置。 limit_req 在 nginx 收到请求后最早的一步就执行,比健康检查接口的 return 200 还早。如果把它配在 server 块里,健康探针也会被限成 429,负载均衡器一看全 429 就把整批机器摘掉——直接雪崩。所以只能配在具体业务请求的 location 里,健康探针那一路放过。

burst 是什么。 burst=166 不是”每秒多放 166 个”,是 nginx 里临时排队的最大名额(个)。这个名额以 83/秒的速度补充。关键是不要加 nodelay——加上会让那 166 个瞬间砸到后端;去掉的话,nginx 自己按 83/秒慢慢排队放行,超出的在 nginx 里等,几乎不吃 CPU。

确认生效。 改完打包、重建容器,正式压 800 请求/秒。通过量稳稳钉在 498.1/秒(= 6×83),429 占 37%,CPU 最高 85.6%,再没到过 100%。这下确认限流是真生效了。

结论

  1. 限流数值得按 CPU 占用算出来、再用实测点核对,别拍脑袋。
  2. 观测工具自己会埋雷(log level 被默认级别吃掉),实打实的证据只能从结构化日志和真实读数拿,不能信”看起来没报错”。
  3. 限流之后的行为要用”长测 + 看队列里积压了多少”判断,短测的 0 错误率是假象。
  4. 限流只是把”全体慢死”换成”多数快、少数立即 429”的体面失败,真要抬天花板得去修下游(缓存、慢查询那些),限流解决不了根本问题。