一、问题背景
我们的主站 www.example.com 使用 Nginx 1.24 作为七层接入网关,前面是 CDN,后面挂 8 个应用节点。由于此前遭遇过一次恶性 CC 攻击,安全团队要求对核心接口加限流。目标是:拦住异常高频的刷接口行为,但不能影响正常用户的浏览和下单。
限流方案采用 Nginx 原生的 limit_req 模块(基于漏桶算法),按客户端 IP 做粒度。上线时间是周一下午,由运维在网关配置中新增了一段 limit_req_zone + limit_req 规则,并 nginx -s reload 平滑生效。本以为只是"加一道保险",没想到立刻引发客诉。
二、故障现象
reload 后大约 10 分钟,客服群开始炸:大量用户反馈"页面打开就提示 503 Service Temporarily Unavailable",且不是刷子,是普通用户,包括公司内网同事自己访问也中招。
监控侧:
- Nginx
503返回码占比从 0.1% 飙到 18%; limit_req拒绝计数($limit_req_status)持续高位:
|
|
- 被拒的 IP 高度分散,并非集中在少数异常 IP,说明是正常流量被误杀;
- 应用节点 CPU、连接数都很低,证明后端完全健康,问题 100% 出在网关限流层。
这与"防 CC"的目标完全背离:攻击没拦住多少,正常用户先被砍了一半。
三、排查过程
第一步,定位是哪条 limit_req 规则在拒绝。打开 Nginx 的 limit_req_status 日志字段,确认返回 503 的请求都带 limit_req_status=503,且命中的 zone 是 perip。
第二步,看配置。问题一目了然:
|
|
三个致命点:
rate=2r/s太小:每个 IP 平均每秒只允许 2 个请求。现代网页一次打开就并发数十个请求(html + css + js + 图片 + api),瞬时远超 2/s。burst=3太小:只缓冲 3 个超出请求就拒绝,正常用户的突发并发根本装不下。nodelay误用:nodelay让 burst 内的请求不排队立即处理,等于把限流瞬时阈值直接抬到rate+burst=5,超出立刻 503,没有平滑过渡,正常用户的并发瞬间被打挂。
第三步,验证判断。用正常浏览器行为模拟单 IP 的首屏请求数:
|
|
单 IP 并发 20 个请求,17 个被 503——完美复现用户"打开就报错"。这正是 rate+burst=5 在 nodelay 下的直接后果。
第四步,确认 CDN 是否放大问题。由于前面是 CDN,回源到 Nginx 时客户端 IP 已被 X-Forwarded-For 携带,但 limit_req_zone 用的是 $binary_remote_addr(即 CDN 边缘节点 IP)。这意味着所有经同一 CDN 节点回源的用户被当成同一个 IP 限流,进一步把正常流量成片误杀。这是第二个隐藏坑。
四、解决方案
应急先回退,再给出正确配置:
-
立即
nginx -s reload回退到无 limit_req 的版本,503 比例 1 分钟内归零,客诉停止。 -
修正限流参数,按"正常用户首屏并发约 20~30 个请求、且允许一定突发"来设计:
|
|
要点:
rate=20r/s容纳正常浏览;burst=40给足突发缓冲;- 去掉
nodelay,改用delay=20:前 20 个超出的请求不延迟,其余进队列平滑处理,既保体验又防刷; - 限流拒绝码改为
429(语义更准确),并基于X-Forwarded-For取真实客户端 IP,避免 CDN 合并误杀。
- 对静态资源(/assets/、图片)不限流,只对 API 和登录等敏感路径限流,进一步降低误杀面。
灰度上线后观察 30 分钟:503/429 比例稳定在 0.3%,且被拒 IP 确为少数高频异常来源,正常用户零投诉。
五、根因分析
根因是 limit_req 阈值设计与实际流量模型严重错配,叠加两个误用:
rate/burst按"单请求流"想象设置(2r/s、burst 3),但浏览器首屏天然并发几十请求,正常行为就被判为"超限";nodelay把漏桶本该的"平滑排队"变成"瞬间砍杀",剥夺了缓冲;- CDN 场景下用
$binary_remote_addr取到的是 CDN 节点 IP,等于把整片用户合并成一个被限流对象,放大误杀。
本质上,限流是保护手段,但阈值必须建立在对正常用户行为基线的量化之上,不能拍脑袋。
六、预防措施
-
先度量,再限流:上线前用日志统计真实用户 P95/P99 的每 IP 请求速率与并发,阈值取正常峰值的 1.5~2 倍。
-
慎用
nodelay:除非明确要"超 burst 立即拒绝",否则用delay=让超出请求排队,保住体验。 -
CDN 后取真实 IP:务必
real_ip/X-Forwarded-For解析真实客户端地址,避免按边缘节点聚合限流。 -
分级限流:静态资源不限,敏感/写接口严限,做到"该拦的拦、该放的放"。
-
灰度 + 监控:新限流规则先 1 台网关灰度,盯
limit_req_status与 4xx 比例,确认无误再全量;拒绝码用 429 便于区分。 -
返回友好提示:对限流返回 429 +
Retry-After头,前端做退避重试,避免用户看到生硬 503。
七、总结
这次故障是典型的"安全策略反噬业务"。limit_req 本身没错,错在阈值是凭直觉设的、且 nodelay 用错了地方。排查关键三步:limit_req_status 日志确认拒绝来源 → 看 rate/burst/nodelay 组合 → 用真实并发复现 503。记住三条铁律:限流阈值必须基于真实流量基线;nodelay 不是默认选项;CDN 后面一定要还原真实客户端 IP。限流的目标是"拦坏人",不是"误伤好人",上线前做一次灰度观测,能省掉一次客诉危机。