限请求与限连接:两种不同的保护
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 reload429 激增时怎么排查
先分清是谁在拒绝。可能是 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";