上线前没人告诉我的事:Nginx 限流一旦配错,正常流量也会被自己打死
上线前没人告诉我的事:Nginx 限流一旦配错,正常流量也会被自己打死
说实话,我以前一直把 Nginx 限流当成“防流量洪峰”的标准动作。直到有一次上线后,业务方在群里连发三条消息:接口明明没挂,为什么正常用户也开始大量 503 和 429?
后来复盘才发现,问题不是流量真的打爆了服务,而是我把限流规则配得太理想化了。攻击流量没挡干净,正常流量反而先被误伤。这类事故最麻烦的地方在于:它看起来像后端抖了、像网络超时、像应用线程池满了,但根因其实藏在网关层。
这篇我不讲空泛概念,就讲一次真实的流量治理复盘:Nginx 限流为什么会误伤正常请求、怎么快速确认是不是限流配置背锅、以及上线前后该怎么把这类风险压下去。
先说结论:限流不是“开了就安全”,配错了就是自伤
很多人第一次接触 limit_req,直觉都是“给接口加个阈值,超过就挡住”。逻辑没错,但线上流量根本不是匀速流进来的。
真实请求有突发、有重试、有热点用户、有共享出口 IP,还有前端静默刷新。你如果只按平均 QPS 去算阈值,很容易出现一种很尴尬的情况:机器资源其实还够,但请求已经先被网关层丢掉了。
更坑的是,限流配置一旦用了错误的 key,比如直接按 $binary_remote_addr 限流,办公室、学校机房、移动网络出口这种 NAT 场景就会一起中枪。几十个正常用户共用一个出口 IP,请求一多,大家一起被算成“同一个人”。
事故现场是怎么暴露出来的
那次上线后最早看到的不是机器告警,而是接口成功率突然往下掉。监控面板上有两个现象特别反常:
- upstream 响应时间没有明显变慢;
- 业务容器 CPU、内存、线程数也没到危险线。
但 499、503、429 的比例在短时间内明显抬头。
这时候如果你只盯应用日志,很容易误判成“调用链偶发超时”或者“后端连接池抖动”。我当时也是先查 Java 服务,再查数据库,绕了一圈才回到 Nginx access log 上。
真正让我警觉的是下面这条日志格式里打出来的限流状态:
log_format main '$remote_addr - $request '
'status=$status rt=$request_time '
'urt=$upstream_response_time '
'limit=$limit_req_status';
把 limit_req_status 打进日志后,问题就清楚了:大量失败请求在进入 upstream 之前就被 Nginx 拦掉了。
如果你线上还没把这个字段打出来,我建议先补上。很多限流事故之所以定位慢,不是因为问题难,而是因为证据没落盘。
最容易误伤正常流量的 4 个配置坑
1)按 IP 限流,但你的用户根本不是真正“一人一 IP”
这是最常见的坑。配置看起来通常像这样:
limit_req_zone $binary_remote_addr zone=api_rate_limit:10m rate=20r/s;
server {
location /api/ {
limit_req zone=api_rate_limit burst=40 nodelay;
proxy_pass http://backend_api;
}
}
在测试环境里,这种配置经常“看起来没问题”,因为测试流量来源单一、节奏稳定。但线上用户来源复杂得多,共享出口 IP 的情况非常普遍。
如果你的业务有登录态、租户 ID、API Key,优先考虑按更细粒度的业务标识限流,而不是直接按出口 IP 一刀切。
比如可以这样处理:
map $http_x_api_key $limit_key {
default $binary_remote_addr;
~.+ $http_x_api_key;
}
limit_req_zone $limit_key zone=api_rate_limit:20m rate=30r/s;
这个思路不是绝对完美,但至少比“所有人只看 IP”更接近真实业务模型。
2)只配了 rate,没认真算 burst
rate 决定平均放行速度,burst 决定短时间能容忍多大的突发。很多线上误伤,不是因为平均流量超了,而是因为瞬时突刺没被接住。
尤其是下面这些场景,突发非常常见:
- 前端页面首次加载并发请求多个接口;
- 用户点击后触发轮询或重试;
- 定时任务整点同时发起请求;
- 发布后缓存失效,短时间内回源放大。
如果 burst 太小,Nginx 会很“严格”地把这些正常突发当成异常流量处理掉。
我的经验是,不要只盯理论值,至少要拿最近一段 access log 做分位数统计,看看热点接口在 1 秒窗口、3 秒窗口里的真实峰值长什么样。
下面这个命令就很适合先做一轮粗统计:
awk '{print substr($4,2,20)}' access.log | sort | uniq -c | sort -nr | head -20
如果你想按 URI 再细分,可以再叠一层聚合。
3)误用了 nodelay,把排队缓冲变成了直接拒绝
很多教程里会直接写:
limit_req zone=api_rate_limit burst=20 nodelay;
问题是,nodelay 的语义不是“更高级”,而是“超过瞬时速率但还在 burst 范围内的请求不排队,直接立即处理”。在某些场景它确实有用,但如果 upstream 本身扛不住瞬时并发,它只是把压力更快地打给后端。
反过来,当 burst 用得很小、请求节奏又抖动时,很多人会把 429 激增误以为是 rate 不够,实际上是排队策略和后端承压模型不匹配。
我的做法通常是:
- 低时延、轻计算接口:可以适当用
nodelay; - 重查询、重写入接口:宁可允许短暂排队,也别让突发直接打穿后端。
限流配置不是孤立的,它必须结合接口的 RT、并发度和后端线程/连接池容量一起看。
4)全站一个限流区,结果热点接口拖累普通接口
还有一种配置,一眼看过去就知道后面迟早出事:所有 /api/ 都共用一个 zone。
这种做法省事,但副作用很大。登录接口、查询接口、导出接口、回调接口的流量模型根本不一样,硬塞进一个限流桶里,热点接口一抖,普通接口也会跟着受牵连。
更稳的办法是分层:
limit_req_zone $limit_key zone=login_zone:10m rate=10r/s;
limit_req_zone $limit_key zone=query_zone:20m rate=50r/s;
limit_req_zone $limit_key zone=write_zone:20m rate=15r/s;
location /api/login {
limit_req zone=login_zone burst=20;
proxy_pass http://backend_api;
}
location /api/order/query {
limit_req zone=query_zone burst=80;
proxy_pass http://backend_api;
}
location /api/order/create {
limit_req zone=write_zone burst=20;
proxy_pass http://backend_api;
}
这才是更接近生产现实的限流方式:按接口价值和承压能力做分桶,而不是按目录图省事。
我当时怎么确认“锅就在限流”
如果你也碰到类似情况,我建议按这个顺序查,不容易走弯路。
第一步:把网关层和应用层指标拆开看
重点看下面几组数据:
- Nginx 429/503 占比;
- upstream_response_time 与 request_time 的差值;
- 应用容器 CPU、线程池、连接池;
- 单接口的 QPS 峰值和错误率。
如果 Nginx 已经大量拒绝,但 upstream RT 变化不大,通常就说明请求没真正压到后端。
第二步:检查 access log 里的限流状态
建议临时把这些字段都打出来:
log_format rate_audit '$time_local '
'ip=$remote_addr '
'host=$host '
'uri=$uri '
'status=$status '
'limit=$limit_req_status '
'rt=$request_time '
'urt=$upstream_response_time';
然后直接筛:
grep 'limit=REJECTED' /var/log/nginx/access.log | head -50
如果大量正常业务 URI 都被打上 REJECTED,方向基本就定了。
第三步:按来源和 URI 做聚类
这个步骤特别重要,它能帮你判断是恶意流量、热点用户,还是 NAT 误伤。
grep 'limit=REJECTED' /var/log/nginx/access.log | awk -F 'ip=| host=| uri=| status=' '{print $2" "$4}' | sort | uniq -c | sort -nr | head -30
如果你看到一个出口 IP 对多个普通 URI 都被连续拒绝,而且这些 URI 本身又是正常页面加载链路的一部分,那大概率不是攻击,而是限流 key 设计有问题。
第四步:灰度放宽而不是全量关掉
线上一旦确认误伤,很多人第一反应是直接把限流关掉。我不太建议这么干,尤其是在流量本来就不稳定的时候。
更稳妥的做法是:
- 先调大 burst;
- 再把关键接口拆到独立 zone;
- 必要时只对白名单来源或登录用户切换 key;
- 观察 5~10 分钟错误率、RT、upstream 负载变化,再决定要不要进一步放宽。
这样做的好处是:你是在收敛误伤,而不是把整道闸门直接拆掉。
上线前我现在固定会做的 4 个检查
这次事故之后,我把 Nginx 限流从“顺手一配”改成了“上线项必查”。现在每次改限流规则,我都会固定跑下面这四步。
检查 1:看真实流量分布,不拿平均值自我安慰
不要只看日均 QPS,也不要只看压测报告里那种过于平滑的曲线。上线前最好拿最近一周真实流量做统计,重点看峰值和突发窗口。
检查 2:确认限流 key 是否符合业务身份
能按用户、租户、API Key 分流的,尽量别只按 IP。实在只能按 IP,也要先评估 NAT 场景比例。
检查 3:对热点接口做独立分桶
登录、回调、查询、写入、导出,不要共用一个桶。谁最容易抖、谁最怕误伤,就单独拿出来。
检查 4:把回滚动作写成 Runbook
我现在会把限流变更对应的回滚动作提前写好,至少包括:
- 配置文件位置;
- 当前生效参数;
- 回滚版本;
- reload 命令;
- 验证命令;
- 观察指标面板链接。
例如:
nginx -t && nginx -s reload
curl -s -o /dev/null -w '%{http_code} %{time_total}
' https://api.example.com/health
很多事故真正浪费时间的,不是定位不到,而是知道要改哪里,却没有一套能快速、稳妥落地的回滚步骤。
一个更稳的限流思路:限流、熔断、缓存别各管各的
最后再说一个很容易被忽略的点:限流本身不是最终目标,它只是系统保护的一部分。
如果你的后端本来就没有缓存兜底、没有熔断降级、没有热点隔离,那限流阈值就会变得非常敏感。阈值设低了误伤,设高了挡不住,团队最后只会在“太松”和“太严”之间反复横跳。
真正更稳的做法是把三件事一起考虑:
- 限流:控制入口流量节奏;
- 缓存/本地兜底:减少热点请求直接穿透后端;
- 熔断/降级:后端扛不住时优先保核心链路。
这样一来,Nginx 限流就不用独自扛全部压力,阈值也更容易调到一个既不误伤、又能保系统的平衡点。
写在最后
那次事故给我的最大教训不是“限流参数要调大一点”,而是:网关策略一旦脱离真实业务流量模型,再漂亮的配置也可能变成生产事故的导火索。
Nginx 限流不是不能用,相反,它非常有用。但前提是你得知道自己在保护什么、牺牲什么、以及一旦误伤时怎么快速止血。
如果你最近正准备给接口补限流,我建议先别急着抄配置。先把日志字段补齐,把热点接口拆开,把回滚动作写好。这样真出问题时,你救的不是配置文件,而是整条业务链路。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)