Web 服务

Nginx 限流实战:限连接与限请求的组合

限流配错比不配更危险:没定义 zone 会退化成按 worker 各算一份限流。本文讲清 limit_req 与 limit_conn 的语义差异、burst 队列与 nodelay 的取舍、共享内存 zone 的必要性,以及 429 激增时逐层排查的顺序。

作者:巧匠团队·9 分钟阅读·更新于 2026-09-30

限请求与限连接:两种不同的保护

limit_conn 限制并发连接数,一个客户端可以持有多个连接,所以它防止的是「单个来源占住大量连接」,典型场景是慢连接把 worker 或后端连接池耗尽。它按连接计数,请求处理完连接关闭才释放额度。

limit_req 限制请求速率,衡量的是单位时间内的请求个数,与连接是否长无关。API、搜索这类短请求靠它压住瞬时峰值。两者互补:连接限流管住资源占用,请求限流管住流量速率。

配置上两者都依赖共享内存 zone。zone 名后面的尺寸决定能存多少个状态条目,1MB 大约可容纳 4000 到 8000 个状态。状态数超限后旧条目会被淘汰,淘汰可能导致限流提前放行,属于精度损失而非故障。

http {
  limit_req_zone $binary_remote_addr zone=api_perip:10m rate=10r/s;
  limit_conn_zone $binary_remote_addr zone=api_conn:10m;

  server {
    location /api/ {
      limit_req zone=api_perip burst=20 nodelay;
      limit_conn api_conn 20;
    }
  }
}

burst 与 nodelay 的取舍

burst 定义突发队列长度:允许超出 rate 的请求先排队等待,而不是立刻 429。不写 burst 时请求超出速率就直接被拒,尖峰流量很容易把正常用户挡在门外。

默认行为下队列里的请求按原速率排队等待,客户端会感到延迟逐步升高。加上 nodelay 后,已通过限流但仍在 burst 队列内的请求会立刻被放行,不排队等待,代价是瞬时并发可能超过 rate 所预期的值。

经验做法是短周期接口用 nodelay 让突发快速通过,长任务接口保留排队以保护后端。burst 大小按「允许的突发倍数」估算,例如 rate 为 10r/s 且 burst 为 20,就是允许短时间 2 倍速率,超出 30 的部分直接 429。

# 突发 2 倍,立即放行:适合短请求 API
limit_req zone=api_perip burst=20 nodelay;

# 突发排队等待:适合慢接口/长任务,保护后端
limit_req zone=slow_perip burst=10;

# 按接口 key 分别计数,而不是只按 IP
limit_req_zone $server_name$request_uri zone=api_route:10m rate=5r/s;

为什么 zone 写错就等于没限流

limit_req_zone 和 limit_conn_zone 必须定义在 http 块,因为它们维护的是跨 location、跨 worker 共享的状态。如果在 server 或 location 块里定义,Nginx 启动会直接报错,所以这一步通常不会错。

真正的坑是反向:location 里的 limit_req zone 名字拼错,或者漏掉 zone 参数。旧版本允许不写 zone,那时状态只在单个 worker 内有效,`worker_processes` 大于 1 时每个 worker 各算一份,实际限流阈值被放大到 worker 数的倍数。

现代 Nginx(1.17.6 之后)在未指定共享 zone 时已不再支持这种写法。排障时先确认生效的 worker 数量,再用 $limit_req_status 变量观察判定结果:PASSED 表示通过,REJECTED 表示被限流,DELAYED 表示在队列中等待。

log_format limits "$remote_addr req=$request_uri status=$status "
                  "limit=$limit_req_status conn=$limit_conn_status rt=$request_time";
access_log /var/log/nginx/limits.log limits;

worker_processes 4;
nginx -t && nginx -s reload

429 激增时怎么排查

先分清是谁在拒绝。可能是 CDN 或上游 WAF 先返回 429,也可能是 Nginx 自己的限流。用响应体和 `log_format` 里的 $limit_req_status 确认:只有 $limit_req_status 为 REJECTED 才是 Nginx 限流。

然后判断是配置问题还是流量真涨。用 $limit_req_status 与 $status 交叉统计:如果 REJECTED 集中在少数 $remote_addr,多半是爬虫或单一客户端,可以换成按 API key 或 $binary_remote_addr 加 key 的方式区分配额;如果整体均匀被拒,说明 rate 确实偏紧。

最后确认放行策略是否合理。常见改进是把登录、搜索这类接口单独拆出更宽松的 zone,把静态资源放进不限流的 location,并用 limit_req_status 与 limit_conn_status 显式指定状态码,保证客户端拿到的是明确的 429 而不是默认行为带来的意外。

limit_req_status 429;
limit_conn_status 429;

location /static/ {          # 静态资源不限流
  limit_req off;
  limit_conn off;
}

location /login/ {          # 登录接口单独放宽
  limit_req zone=login_perip burst=10 nodelay;
}

反代场景下的限流

在 Nginx 做反向代理时,$remote_addr 拿到的是上游负载均衡器或 CDN 的 IP,所有客户端会被算成同一个来源,整站共享一份额度。这是代理后面上流「不生效或误伤」的头号原因。

解决办法是先用 realip 模块还原真实地址:set_real_ip_from 信任内网网段,real_ip_header CF-Connecting-IP 或 X-Forwarded-For,real_ip_recursive on 逐级解析可信来源。信任范围务必收窄到已知代理的网段,写成 0.0.0.0/0 会让任何人伪造来源绕开限流。

改完用 `nginx -t` 验证并 reload,然后在日志里打印 $remote_addr 与 $http_x_forwarded_for 对照,确认拿到的是真实客户端地址。如果前面还有 CDN,别忘了 CDN 自身的限速是独立的一层,两层限流叠加时通常由 CDN 承担大部分粗粒度拦截。

http {
  set_real_ip_from 10.0.0.0/8;
  set_real_ip_from 172.16.0.0/12;
  real_ip_header X-Forwarded-For;
  real_ip_recursive on;

  limit_req_zone $binary_remote_addr zone=api_perip:10m rate=10r/s;
}

log_format realip "$remote_addr fwd=$http_x_forwarded_for rl=$limit_req_status";

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。