NGINX 的 limit_req 模块是应对突发流量和恶意请求的第一道防线。很多人配置了却仍然被穿透,根本原因在于对“速率”和“突发”两个核心参数的理解存在偏差。limit_req 不是简单的一秒允许几个请求,而是基于漏桶算法(Leaky Bucket)的流量整形机制。漏桶有一个固定速率处理请求,超出速率的请求进入队列等待,队列满了就拒绝。这个队列就是 burst 参数,而 nodelay 决定了队列里的请求是立即处理还是按速率间隔执行。
limit_req 的核心指令与工作原理limit_req_zone 用来定义共享内存区域,存储请求计数。语法是 limit_req_zone key zone=name:size rate=rate。key 是判断请求是否相同的依据,通常用 $binary_remote_addr 限制单个 IP,或者 $server_name 限制单个站点。$binary_remote_addr 比 $remote_addr 节省空间,一个 IPv4 地址只占 4 字节。zone 后面跟区域名称和大小,1MB 大约能存储 16000 个 IP 的状态。rate 是核心速率,单位是 r/s 或 r/m,比如 10r/s 表示每秒处理 10 个请求,也就是每 100 毫秒一个。
limit_req 指令用在 location 或 server 块里,语法是 limit_req zone=name [burst=number] [nodelay | delay=number]。burst 是漏桶的容量,允许瞬间超出 rate 的请求数量。nodelay 让 burst 内的请求立即执行,不按速率间隔排队。delay 是 NGINX 1.15.7 引入的参数,可以更精细地控制,比如 delay=5 表示前 5 个突发请求立即处理,超过的按速率排队。
实际配置场景与参数调优先看一个基础配置,限制单个 IP 每秒 10 个请求,突发 20 个,不加 nodelay:
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20;
proxy_pass http://backend;
}
}
}
这个配置下,如果客户端瞬间发来 30 个请求,前 10 个按每秒 10 个的速率处理,接下来的 20 个进入 burst 队列排队,最后 1 个直接返回 503。队列里的 20 个请求会以每 100 毫秒一个的速度陆续处理,客户端会感受到明显延迟。这种配置适合对延迟不敏感的后台任务接口。
如果加上 nodelay:
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
此时 30 个请求中,前 10 个立即处理,burst 里的 20 个也立即处理,只有超出 30 的才返回 503。这实际上放宽了瞬时限制,但后端会在瞬间承受 burst+rate 的并发量。很多人误以为 nodelay 是“不限速”,其实它只是取消了队列等待,burst 容量仍然是硬上限。
delay 参数提供了折中方案:
location /api/ {
limit_req zone=api_limit burst=20 delay=8;
proxy_pass http://backend;
}
这个配置表示 burst 中的前 8 个请求立即处理,剩余 12 个按速率排队。这样既能快速响应少量突发,又能保护后端不被大量并发冲垮。delay 的值需要根据后端实际承载能力设定,一般取 burst 的 30% 到 50%。
多维度限速与白名单机制单一按 IP 限速无法应对复杂场景。比如某个 IP 在大量请求不同 URL,或者攻击者使用代理池分散请求。此时需要组合多个 limit_req_zone,按不同维度限速。可以同时限制单 IP 和全局接口速率:
http {
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=5r/s;
limit_req_zone $server_name zone=per_server:10m rate=100r/s;
server {
location /api/ {
limit_req zone=per_ip burst=10 nodelay;
limit_req zone=per_server burst=50 nodelay;
proxy_pass http://backend;
}
}
}
多个 limit_req 指令按顺序执行,任何一个触发限制都会返回 503。per_server 作为全局兜底,防止单个 IP 限不住时整体流量失控。还可以用 map 定义更复杂的 key,比如对特定 UA 限速:
map $http_user_agent $limit_bot {
default "";
"~*python" $binary_remote_addr;
"~*scrapy" $binary_remote_addr;
}
limit_req_zone $limit_bot zone=bot_limit:10m rate=1r/s;
白名单是生产环境必备。内部服务、监控探针、管理后台不应该被限速。可以用 geo 模块标记白名单 IP,然后在 key 中排除:
geo $whitelist {
default 0;
10.0.0.0/8 1;
172.16.0.0/12 1;
}
map $whitelist $limit_key {
1 "";
0 $binary_remote_addr;
}
limit_req_zone $limit_key zone=api_limit:10m rate=10r/s;
当 $limit_key 为空字符串时,limit_req 不会对该请求计数,实现了白名单效果。注意空字符串的 key 仍然会占用少量内存,但不会触发限速逻辑。
limit_req 与 limit_conn 的配合limit_req 控制请求速率,limit_conn 控制并发连接数,两者解决不同层面的问题。一个 IP 可能以低速率保持大量长连接,limit_req 管不了这种情况。反过来,短时间高并发短连接,limit_conn 可能还没统计到连接就已经关闭。所以两者配合才能形成完整防护:
location /api/ {
limit_conn conn_per_ip 10;
limit_req zone=api_limit burst=20 nodelay;
limit_rate 500k;
proxy_pass http://backend;
}
limit_conn 限制每个 IP 同时最多 10 个连接,limit_req 限制请求速率,limit_rate 限制每个连接的带宽。三层防护下,即使攻击者用大量 IP 分散请求,每个 IP 的杀伤力也被大幅削弱。
状态码自定义与日志分析默认触发限速返回 503,但有些场景需要返回 429 Too Many Requests,更符合 HTTP 语义。用 limit_req_status 指令修改:
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
同时可以设置 limit_req_dry_run 开启空跑模式,只记录日志不实际拒绝请求,方便上线前调试:
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_dry_run on;
limit_req_status 429;
proxy_pass http://backend;
}
日志里需要记录限速触发情况,在 log_format 中加入 $limit_req_status 变量。这个变量在请求被限速时会记录被哪个 zone 限制以及延迟时间,正常请求则为空或 PASS。完整的日志格式:
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" limit_req=$limit_req_status';
access_log /var/log/nginx/access.log main;
通过分析 $limit_req_status 字段,可以统计哪些接口、哪些 IP 频繁触发限速,进而调整阈值或排查异常流量。
分布式环境下的限速策略单机 NGINX 的 limit_req 基于本地共享内存,无法跨服务器共享计数。在负载均衡后面有多台 NGINX 时,每台独立计数,实际限速效果会被乘以机器数量。比如 rate=10r/s,3 台 NGINX 就是 30r/s。解决这个问题有几种思路。一是在负载均衡层统一限速,NGINX 作为 LB 时对后端整体限速。二是使用 Redis 等外部存储实现分布式计数,需要配合 Lua 模块或 OpenResty 开发。三是调整单机速率,如果总速率要求 100r/s,有 4 台 NGINX,每台设 25r/s。这种方法简单但不够精确,因为流量分配可能不均。
使用 OpenResty 配合 Redis 实现分布式限速的简化示例:
location /api/ {
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
local key = "rate:" .. ngx.var.binary_remote_addr
local current = red:incr(key)
if current == 1 then
red:expire(key, 1)
end
if current > 10 then
ngx.status = 429
ngx.exit(429)
end
}
proxy_pass http://backend;
}
这个 Lua 脚本用 Redis 的 INCR 和 EXPIRE 实现了秒级计数器,所有 NGINX 实例共享同一个 Redis,达到全局统一限速。生产环境需要加上连接池、错误处理、降级策略,Redis 不可用时可以降级为本地限速。
常见误区与排错指南误区一:rate 设得太大就安全。rate 应该根据后端实际处理能力设定,设得太大等于没限,太小影响正常用户。建议通过压测确定后端 QPS 上限,rate 设为其 70% 左右,留出余量。
误区二:burst 设得越大越好。burst 过大意味着后端可能瞬间承受巨大压力,如果后端处理不过来,请求堆积在连接队列里,客户端超时反而更糟。burst 应该根据后端缓冲能力和客户端超时时间来定。
误区三:所有接口用同一套限速参数。登录接口、短信发送接口需要更严格的限制,静态资源可以宽松。应该按 location 粒度分别配置。
排错时首先检查 limit_req_zone 的 key 是否正确。用 curl 带上特定 header 测试,观察 access.log 里的 $limit_req_status。如果始终不触发限速,检查 zone 名称是否匹配、rate 单位是否写错。如果频繁误杀,检查是否有反向代理或 CDN 导致所有请求都来自同一 IP,这种情况需要用 $http_x_forwarded_for 或 $http_x_real_ip 作为 key,前提是已经用 real_ip 模块恢复了真实 IP。
limit_req 的日志级别是 error,触发限速时 error.log 会记录类似 "limiting requests, excess: 5.300 by zone" 的信息。excess 表示超出速率多少,这个值可以帮助判断 burst 设得是否合理。如果 excess 经常很大,说明 burst 可能不够,或者有异常流量需要排查。
高级用法:多层限速与动态调整对于复杂业务,可以嵌套多个 limit_req,实现阶梯式限速。比如先限制单 IP 每秒 5 次,再限制单 IP 每分钟 100 次,最后限制全局每秒 500 次。这样既能拦截高频攻击,又不会误伤偶尔突发的正常用户。
limit_req_zone $binary_remote_addr zone=ip_sec:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=ip_min:10m rate=100r/m;
limit_req_zone $server_name zone=global:10m rate=500r/s;
location /api/ {
limit_req zone=ip_sec burst=10 nodelay;
limit_req zone=ip_min burst=30 nodelay;
limit_req zone=global burst=100 nodelay;
proxy_pass http://backend;
}
动态调整限速参数需要借助 NGINX Plus 的 API 或者 OpenResty。NGINX Plus 支持动态修改共享内存中的限速参数,无需 reload。开源版可以通过定期 reload 配合配置管理工具实现准动态调整,但 reload 有短暂的服务中断风险。更优雅的方案是用 OpenResty 的 lua-resty-limit-traffic 库,将限速参数存储在 Redis 或 etcd 中,实时读取生效。
limit_req 是 NGINX 中最实用的防护模块之一,但用好它需要理解漏桶算法的本质,根据业务场景合理设置 rate、burst、delay 的组合,配合 limit_conn、白名单、日志监控形成完整体系。没有一劳永逸的配置,只有持续观察流量模式并迭代优化,才能在高并发下保持服务的稳定和可用。
