后端接口突然变慢,日志里显示同一个 IP 在几十秒内发起了上千次请求。如果把这种情况交给应用代码去处理,要么改造量大,要么防护点分散。更合适的做法是在接入层 Nginx 上直接拦掉异常流量。Nginx 自带的两个模块提供了非常实用的限流能力:ngx_http_limit_req_module 控制请求速率,也就是每秒允许通过多少个请求;ngx_http_limit_conn_module 控制并发连接数,也就是同一个客户端同时能建立多少个连接。两者配合使用,可以挡住大部分 CC 攻击、爬虫滥用以及单 IP 暴力破解。

从实现原理上看,limit_req 使用令牌桶算法,允许一定程度的突发流量,而不是严格按照固定窗口计数。limit_conn 则更简单直接,每个 key 对应一个连接计数器,连接建立时加一,关闭时减一。两个模块都依赖共享内存区域来保存计数状态,因此多个 worker 进程之间可以共享同一份限流数据。下面会把这两套配置拆开详细说明,并给出可以直接落地的示例。
一、请求速率限制:limit_req_module 配置详解
ngx_http_limit_req_module 的核心思想是根据客户端标识(通常是 IP)维护一个请求频率计数器,超过设定速率就返回错误。配置分为两步:先在 http 层用 limit_req_zone 定义共享内存区和速率,再在 server 或 location 层用 limit_req 启用限制。
limit_req_zone 的语法为 limit_req_zone key zone=名称:大小 rate=速率;。其中 key 一般使用 $binary_remote_addr,它是二进制形式的客户端 IP,比 $remote_addr 字符串更省内存。比如 zone 设置为 10m,大约能存储 16 万个 IP 的状态,对于多数中小站点足够使用。rate 可以写成每秒请求数,如 rate=10r/s,也可以写成每分钟请求数,如 rate=600r/m。需要注意的是,rate 指的是平均速率,不是瞬时峰值。
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
listen 80;
server_name ipipp.com;
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend_pool;
}
}
}上面的配置表示对每个 IP,访问 /api/ 路径的请求平均速率不能超过每秒 10 个。burst 参数表示允许在短时间内超出平均速率的请求数量,这部分请求会排队等待处理。如果不加 nodelay,排队请求会按照固定间隔依次放行,可能会让部分请求等待较长时间。加上 nodelay 后,排队请求立即被处理,但超出 burst 上限的请求直接返回 503。nodelay 不会改变突发总额,只是让突发请求不再被延迟。需要注意,burst 越大,瞬时能穿透的请求就越多,实际配置时需要结合后端承受能力权衡。
默认的返回状态码是 503,如果希望返回更明确的状态码,可以用 limit_req_status 429; 改成 429 Too Many Requests。此外,可以通过 limit_req_log_level 设置日志级别,默认是 error,建议保留,方便观察哪些请求被限流了。
二、并发连接数限制:limit_conn_module 配置详解
请求速率限制关注的是单位时间内的请求数量,而并发连接限制关注的是同时处于活动状态的连接数。有些场景下,请求速率可能很低,但每个连接持续时间很长,比如文件下载、视频播放。这时候仅靠 rate 限制就不够,因为一个慢速连接也能长时间占用资源。limit_conn 模块可以限制同一个 key 同时保持的最大连接数,超过后新的连接会被拒绝。
配置同样分两步,先在 http 层定义共享内存区和 key,再用 limit_conn 指令启用。与 limit_req_zone 类似,limit_conn_zone $binary_remote_addr zone=conn_zone:10m; 定义一个名为 conn_zone 的共享内存区。然后在 location 中使用 limit_conn conn_zone 5;,表示同一 IP 最多同时建立 5 个连接。如果超过这个数量,Nginx 会返回 503 或你指定的状态码。
http {
limit_conn_zone $binary_remote_addr zone=download_conn:10m;
server {
listen 80;
server_name ipipp.com;
location /download/ {
limit_conn download_conn 5;
limit_rate 200k;
alias /data/files/;
}
}
}这个示例还额外配置了 limit_rate 200k;,它限制的是每个连接的传输速度,单位是字节每秒,这里表示每个连接最多 200KB/s。limit_rate 和 limit_conn 不是同一个模块,但经常搭配使用:限制连接数防止连接被占满,限制速率防止单个连接把带宽吃完。需要注意 limit_rate 只对响应体生效,请求体不受控制。
如果希望针对不同路径使用不同的并发限制,可以定义多个 limit_conn_zone,例如登录接口限制 2 个连接,下载接口限制 5 个连接。不过要记住,不同的 zone 是独立计数的,同一个 IP 在不同 zone 里的状态互不影响。
三、真实场景下的组合配置示例
实际生产环境往往不是单一规则就能覆盖所有需求。比如一个网站有普通页面、登录接口、文件下载接口,各自对限流的要求差别很大。普通页面可以放宽速率,登录接口要严防暴力破解,下载接口要控制并发和带宽。下面给出一个相对完整的 Nginx 配置片段,展示如何将这些规则组合起来。
http {
# 全站基础限流,每个 IP 每秒最多 20 个请求
limit_req_zone $binary_remote_addr zone=base_limit:10m rate=20r/s;
# 登录接口专用限流,每个 IP 每秒最多 1 个请求
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/s;
# 下载并发限制
limit_conn_zone $binary_remote_addr zone=download_conn:10m;
server {
listen 80;
server_name ipipp.com;
# 普通页面限流
location / {
limit_req zone=base_limit burst=30 nodelay;
root /var/www/html;
}
# 登录接口限流
location /login {
limit_req zone=login_limit burst=3 nodelay;
limit_req_status 429;
proxy_pass http://backend_pool;
}
# 下载接口并发限制
location /download/ {
limit_conn download_conn 5;
limit_rate 100k;
alias /data/files/;
}
}
}在这个配置里,普通页面允许每秒 20 个请求,可以突发 30 个,适合应对正常的浏览器并发请求。登录接口被限制到每秒 1 个请求,突发 3 个,能有效阻止密码爆破。下载接口不限制请求速率,但限制同一 IP 最多 5 个连接,并且每个连接限速 100KB/s,避免带宽被少数用户占满。
需要特别提醒,登录接口如果使用了 CDN 或反向代理,客户端 IP 可能变成代理节点的 IP。此时直接用 $binary_remote_addr 会把所有经过同一个代理的用户算成一个 key,导致误伤。这种情况下应该改用 $http_x_forwarded_for 来获取真实 IP,但这个头可以被伪造,且长度较大,需要在限流准确性和内存占用之间做选择。一般建议在 Nginx 前端自行处理真实 IP 识别,然后使用一个内部自定义变量作为限流 key。
四、如何测试与验证限流效果
配置完成后不能凭感觉判断是否生效,需要用工具压测。ApacheBench(ab)是最常见的工具之一,可以通过并发数和总请求数来观察返回码。例如对一个速率限制为 10r/s 的接口,使用 ab -n 1000 -c 50 http://ipipp.com/api/test,结果中非 2xx 的响应比例应该明显上升,同时 Nginx 的 error.log 里会出现类似 limiting requests 的记录。
# 模拟 1000 个请求,50 个并发 ab -n 1000 -c 50 http://ipipp.com/api/test # 查看限流日志 tail -f /var/log/nginx/error.log | grep limiting
除了 ab,wrk 和 curl 循环也可以用来做简单验证。wrk 更适合长连接下的压力测试,能更真实地反映并发连接限制的效果。例如 wrk -t 4 -c 100 -d 30s http://ipipp.com/download/test.iso,如果 limit_conn 设置为 5,那么同时处于活动状态的连接数不会超过 5,其余连接会收到 503 或被排队。
验证时需要关注几个点:被限流的请求返回的状态码是否符合预期;正常流量是否被误伤;突发的请求在 burst 范围内是否能快速处理;不同 worker 进程下的计数是否一致。如果发现限流没有生效,检查配置是否在正确的层级,以及 zone 名称是否拼写一致。limit_req_zone 和 limit_req 中的 zone 名称必须完全匹配,否则 Nginx 会报错或忽略配置。
五、限流配置的常见误区和优化建议
一个常见的误区是把 rate 设置得过高或者 burst 设置得过大,导致限流形同虚设。rate 是平均速率,burst 是允许的突发量。如果 rate=100r/s 且 burst=200,对于大多数中小站点来说已经很难触发限制。实际配置应该根据后端服务的真实承受能力来设定,而不是拍脑袋。可以先从保守值开始,比如全站 rate=20r/s,登录接口 rate=1r/s,然后根据监控数据逐步调整。
另一个容易忽略的问题是共享内存的容量。limit_req_zone 和 limit_conn_zone 后面的 size 参数决定了能存储多少个 key 的状态。如果内存区满了,Nginx 会返回 503 并记录错误,但这可能不是我们想要的限流行为。通常每个 key 大约占用 64 字节到 100 字节,如果需要记录 10 万个 IP,10m 内存基本够用。但如果网站访问量很大,建议预留 50m 以上,并定期观察共享内存使用情况。
另外,limit_req 和 limit_conn 可以叠加使用,但不要给所有请求都加上非常严格的限制,否则会降低用户体验。更合理的做法是对核心接口单独设置严格限流,对静态资源只做连接数和带宽限制,对普通页面保持较宽松的速率。同时可以为搜索引擎爬虫设置白名单,避免误伤正常抓取。Nginx 本身没有直接的白名单指令,但可以通过 map 或 if 结合控制,不过要注意 if 在 location 中可能导致一些副作用,建议优先使用 geo 模块配合自定义变量。
最后,限流是防御手段之一,不能替代应用层的安全校验和资源优化。如果后端服务本身存在性能瓶颈,限流只能延缓问题爆发,并不能根治。真正健壮的系统应该在上游做好扩容、缓存和熔断,在接入层用 Nginx 限流来兜底。这样即使遇到突发流量,也能保证大部分用户正常使用。