生产环境中的Nginx限流策略

描述

问题背景

生产环境中的 Nginx 限流需求通常来自以下场景:

接口被恶意刷:攻击者用脚本高频调用登录/注册/下单接口

爬虫过度抓取:搜索引擎爬虫或采集程序在短时间内发起大量请求,耗尽后端资源

突发流量冲击:促销活动、热点事件导致流量瞬间暴增,后端来不及扩容

单个用户滥用:API 调用频率超出合理范围,影响其他正常用户

Nginx 内置的 ngx_http_limit_req_module(基于漏桶算法)和 ngx_http_limit_conn_module(基于并发连接数)可以满足绝大部分限流需求,无需引入额外的限流组件。

本文从基础配置到生产级最佳实践,涵盖 IP 限流、API Key 限流、白名单绕过、自定义错误响应、灰度验证等场景。

一、核心原理:漏桶算法

Nginx 的 limit_req 模块基于漏桶(Leaky Bucket)算法实现。类比一个底部有小孔的水桶:

rate:水桶底部小孔的流水速率 —— 即允许通过的请求速率

burst:水桶的容量 —— 突发请求的缓冲队列

nodelay/delay:是否让超出 burst 的请求排队等待

关键区别(相比令牌桶):

漏桶强制限流:输出的速率是恒定的,适用于保护后端不被突发流量冲垮

令牌桶允许突发:短期内可用完积累的令牌,适用于需要应对突发流量的场景

Nginx 的 limit_req 属于漏桶实现,但通过 burst + nodelay 参数可以模拟令牌桶的行为。

二、配置指令详解

2.1 核心指令

指令 作用域 说明
limit_req_zone http 定义共享内存区域和请求速率
limit_req http/server/location 在指定范围内启用限流
limit_req_status http/server/location 自定义拒绝响应码(默认 503,推荐 429)
limit_req_log_level http/server/location 拒绝和延迟的日志级别
limit_req_dry_run http/server/location 1.17.1+ 干跑模式(只统计不限制)

2.2 limit_req_zone 参数

 

http {
    limit_req_zone $binary_remote_addr zone=per_ip:10m rate=10r/s;
    #              ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^
    #              key(限流依据)    zone名:内存大小  速率
}

 

$binary_remote_addr:二进制格式的客户端 IP。IPv4 占 4 字节,IPv6 占 16 字节,比 $remote_addr(字符串形式的 IP)更节省内存。推荐使用 $binary_remote_addr。

zone=name:size:共享内存名称和大小。64 位系统上每个状态约 128 字节,10MB ≈ 可存储 8 万个客户端状态。

rate=10r/s:每秒最多 10 个请求。支持 r/s(每秒)和 r/m(每分钟),如 30r/m 表示每分钟 30 个请求。

2.3 limit_req 参数

 

location /api/ {
    limit_req zone=per_ip burst=20 nodelay;
    #             ^^^^^^^ ^^^^^^^^^
    #             指定zone   突发队列大小
}

 

zone:指定使用的限流区域名称

burst:允许的突发请求数。当请求超过 rate 后,超出的请求会进入一个队列等待。burst=20 表示队列容量为 20。

nodelay:如果加上 nodelay,队列中的请求不延迟处理,直接放行。如果不加 nodelay,超出 rate 的请求会被延迟处理(即排队等待),导致响应变慢。

delay=N:1.15.7+ 引入的精细控制。前 N 个超出请求不延迟,后续超出请求才延迟。

三、基础配置示例

3.1 基于 IP 限流(最常用)

 

http {
    # 定义限流区域:依据客户端 IP,每秒最多 10 次请求
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    server {
        listen 80;
        server_name api.example.com;

        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

 

这个配置的效果:

每个 IP 每秒最多 10 次请求

允许突发 20 个请求(超过 10r/s 后的 20 个请求会被立即处理)

超过 10r/s + 20 burst 的请求返回 429

不使用 nodelay 的话,超出 rate 的请求会被延迟到下一周期处理,每个请求的响应时间会逐渐增加

理解 burst + nodelay 的行为:

假设 rate=10r/s, burst=20, nodelay:

第 1 秒内收到 30 个请求:前 10 个正常处理,中间 20 个通过 burst 立即处理,超过 30 个返回 429

第 2 秒内只收到 5 个请求:正常处理,同时 burst 队列"恢复"容量

如果不加 nodelay:

第 1 秒内收到 30 个请求:前 10 个正常处理,中间 20 个排队以 10r/s 的速率处理,所以需要 2 秒才能全部处理完

最后 10 个请求的响应时间会显著增加

3.2 基于 API Key 限流

适用于多租户 API 场景:不同 API Key 有不同的速率限制。

 

http {
    # 依据请求头 X-API-Key 的值限流
    limit_req_zone $http_x_api_key zone=per_key:10m rate=100r/m;

    server {
        location /v1/ {
            # 如果请求头中没有 API Key,拒绝
            if ($http_x_api_key = "") {
                return 401;
            }
            limit_req zone=per_key burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

 

注意:这里用了 $http_x_api_key,这是 Nginx 变量命名规则——HTTP 请求头 X-API-Key 对应变量 $http_x_api_key(全小写、下划线替换横线)。

3.3 组合限流:IP + Key 双重保护

同时限制单 IP 和单 API Key 的请求频率:

 

http {
    limit_req_zone $binary_remote_addr zone=per_ip:10m rate=5r/s;
    limit_req_zone $http_x_api_key zone=per_key:10m rate=100r/m;

    server {
        location /v1/ {
            # 同时应用 IP 限流和 Key 限流(与关系)
            limit_req zone=per_ip burst=10 nodelay;
            limit_req zone=per_key burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

 

注意:多个 limit_req 指令是与(AND)关系——必须同时满足所有规则。记录日志时会分别记录每条规则的命中情况。

四、高级配置场景

4.1 delay 参数精细控制(1.15.7+)

nodelay 是"要么直接放行,要么直接拒绝",适合需要严格保护的接口。但过于刚性,可能导致正常用户在突发后瞬间被限。

delay 参数提供了中间状态:超出 rate 一定量后才开始延迟。

 

location /api/ {
    limit_req zone=per_ip burst=12 delay=8;
    #                    ^^^^^^^^^ ^^^^^^^
    #                    burst=12  delay=8: 前 8 个超出请求不延迟
}

 

行为说明(rate=10r/s, burst=12, delay=8):

时间窗口内请求数 行为
≤ 10 个 全部正常处理
11~18 个 前 10 个正常,超出 8 个以内直接处理(不延迟)
19~22 个 前 10 个正常,超出 8 个直接处理,再超出部分延迟处理
> 22 个 前 10 个正常,超出 8 个直接处理,部分延迟,部分直接 429

delay 参数的适用场景:允许用户在短时间内多请求几个,但也不能完全无限制。

4.2 白名单绕过(geo + map 模块)

内部 IP 或健康检查请求不应该被限流。使用 geo + map 在白名单 IP 后跳过限流:

 

http {
    # 定义白名单
    geo $limit {
        default         1;             # 默认限流
        127.0.0.1       0;             # 本机不限流
        10.0.0.0/8      0;             # 内网段不限流
        172.16.0.0/12   0;             # 内网段
        192.168.0.0/16  0;             # 内网段
        # 可继续添加其他白名单 IP 段
    }

    # 根据 $limit 决定限流 key
    map $limit $limit_key {
        0 "";                          # 空 key 不触发限流
        1 $binary_remote_addr;         # 非白名单使用 IP 作为 key
    }

    limit_req_zone $limit_key zone=whitelist:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=whitelist burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

 

原理:当 $limit_key 为空字符串时,limit_req_zone 不会为这个请求创建状态,即不触发限流。

4.3 自定义错误响应

默认限流返回 503 Service Unavailable,但 503 可能被客户端或浏览器重试。推荐统一返回 429(Too Many Requests),并给出 JSON 格式的响应体。

方法一:使用 error_page 拦截

 

http {
    limit_req_status 429;
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    server {
        error_page 429 = @rate_limit;

        location @rate_limit {
            internal;                    # 内部重定向,外部不可访问
            default_type application/json;
            add_header Retry-After 5;    # 告诉客户端 5 秒后重试
            return 429 '{"code":429,"message":"请求过于频繁,请稍后重试","retry_after":5}';
        }

        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

 

注意:被限流的请求会产生一个内部重定向到 @rate_limit 块,这比直接 return 多一层处理开销。如果对性能要求极高,可以考虑 Lua 限流方案。

方法二:使用变量模板(性能更好,避免内部重定向)

 

http {
    limit_req_status 429;
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            error_page 429 /429.json;    # 直接指向一个静态文件
            proxy_pass http://backend;
        }

        location = /429.json {
            internal;
            default_type application/json;
            return 429 '{"code":429,"message":"请求过于频繁"}';
        }
    }
}

 

4.4 基于 URI 的差异化限流

不同 API 路径可能需要不同的限流策略:

 

http {
    # 登录接口:严格控制
    limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;
    # 通用 API:中等控制
    limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
    # 静态资源:宽松
    limit_req_zone $binary_remote_addr zone=static:10m rate=100r/s;

    server {
        location /api/login/ {
            limit_req zone=login burst=5 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }

        location /api/ {
            limit_req zone=api burst=30 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }

        location /static/ {
            limit_req zone=static burst=200 nodelay;
            limit_req_status 429;
            root /var/www/static;
        }
    }
}

 

4.5 干跑模式(Dry Run,1.17.1+)

在正式启用限流之前,先用干跑模式观察影响:

 

http {
    limit_req_zone $binary_remote_addr zone=dry:10m rate=10r/s;

    server {
        location /api/ {
            limit_req_dry_run on;          # 开启干跑模式
            limit_req zone=dry burst=20 nodelay;

            # 干跑模式下不会真正拒绝请求
            proxy_pass http://backend;
        }
    }
}

 

干跑模式下,Nginx 会在日志中记录哪些请求"本应被限流",但实际不会拒绝。通过分析 $limit_req_status 变量(1.17.6+)可以评估限流策略的准确性。

 

log_format dry_run '$remote_addr - $remote_user [$time_local] '
                   '"$request" $status $body_bytes_sent '
                   'limit_req_status=$limit_req_status';

# limit_req_status 可能的值:
# PASSED — 放行(未触发限流)
# DELAYED — 被延迟(超出 rate)
# REJECTED — 被拒绝(超出 burst)
# DELAYED_DRY_RUN — 干跑模式下被延迟
# REJECTED_DRY_RUN — 干跑模式下被拒绝

 

五、并发连接数限制(limit_conn)

limit_req 控制的是请求速率(RPS),而 limit_conn 控制的是并发连接数。两者可以配合使用:

 

http {
    # 定义连接数限制区域
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

    server {
        location /api/ {
            # 限制每个 IP 最多 10 个并发连接
            limit_conn conn_per_ip 10;
            limit_conn_status 429;

            # 同时限制请求速率
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

 

limit_conn 的典型场景:限制单个用户的下载连接数(防止多线程下载器耗尽带宽)、限制 WebSocket 连接数。

六、配置验证与效果测试

6.1 验证配置语法

 

$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

 

6.2 限流效果压测

使用 ab(Apache Bench)模拟高并发:

 

# 发送 150 个请求,20 并发
$ ab -n 150 -c 20 http://api.example.com/api/test

# 查看结果中的请求分布
...
Complete requests:      150
Failed requests:        30    # 被限流的请求
Non-2xx responses:      30    # 返回 429 的请求
...
Requests per second:    45.50 [#/sec] (mean)

 

更精确的测试(使用 wrk):

 

$ wrk -t4 -c20 -d30s http://api.example.com/api/test

 

6.3 查看限流日志

 

# 查看被限流的请求日志
$ tail -f /var/log/nginx/error.log | grep "limiting requests"
2025/08/15 1522 [warn] 1234#0: *57123 limiting requests, excess: 5.0 by zone "per_ip", client: 1.2.3.4, ...

# 查看返回 429 的请求
$ grep "429" /var/log/nginx/access.log | awk '{print $1,$4,$7,$9}' | tail -10

 

限流日志中的 excess 值表示超出速率的部分,单位是请求数。配合 access.log 的客户端 IP,可以定位到哪些 IP 在频繁请求。

6.4 监控指标

如果使用 Nginx Plus 或 ngx_http_stub_status_module,可以采集限流相关指标。在开源版 Nginx 中,主要通过访问日志和错误日志中的 429 数量来监控限流效果。

 

# 统计每分钟的 429 数量
$ tail -10000 /var/log/nginx/access.log | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -rn

 

七、内存估算

创建 zone 时需要估算内存大小。64 位系统上每个客户端状态占用约 128 字节:

 

1MB ≈ 8,000 个客户端
10MB ≈ 80,000 个客户端
100MB ≈ 800,000 个客户端

 

根据业务的实际 IP 数量估算:

普通业务系统(日活 10 万):10MB 足够

面向公众的高流量服务:建议 50~100MB

需要区分 API Key 的场景:zone 大小取决于 API Key 的数量,每个 Key 也是一个独立状态

如果 zone 过小,会出现内存淘汰,导致部分客户端的状态被覆盖——限流将不再准确。

八、局限性与替代方案

8.1 Nginx 原生限流的局限

局限 说明 替代方案
限流状态无法跨节点共享 每个 Nginx 节点独立统计,集群下总限流不准 Redis + Lua、集中式限流网关
重启后统计清零 zone 存储在共享内存中,重启 Nginx 后所有计数归零 无持久化方案
无法按时间段限流 不支持"每小时 N 次"的滑动窗口 自定义 Lua 脚本
限流粒度有限 原生只支持 IP、请求头、URL 等变量作为 key OpenResty + Lua

8.2 Redis + Lua 补充方案

如果需要集群级别的分布式限流,可以在 Nginx 中嵌入 Lua 脚本(OpenResty / lua-nginx-module):

 

-- 简单的分布式滑动窗口限流
local key = "rate_limit:" .. ngx.var.binary_remote_addr
local limit = 10    -- 每秒最大请求数
local window = 1    -- 窗口大小(秒)

local red = redis:new()
red:set_timeout(100)
local ok, err = red:connect("127.0.0.1", 6379)

local current = red:incr(key)
if current == 1then
    red:expire(key, window)
end

if current > limit then
    ngx.exit(429)
end

 

8.3 连接数限制模块(limit_conn)的补充

如果限流需求不仅是请求速率,还包括下载带宽限制,需要 ngx_http_limit_conn_module + 第三方的 ngx_http_limit_rate 模块,或者使用 OpenResty。

九、生产环境部署注意事项

限流之前先上线监控:在启用限流前,先确认 Nginx 的 429 状态码已在监控中,否则限流上线后无法评估效果。

先干跑再正式:新上线的限流策略先用 limit_req_dry_run 观察至少一个业务周期,确认不会误伤正常用户后再正式启用。

限流阈值留有余量:不要卡着业务 QPS 的极限设置限流阈值。比如业务单机 QPS 峰值是 1000,限流设置在 1200 并开启 burst,而不是精确地设置 1000。

业务方确认阈值:限流阈值由后端业务方提供,而不是运维随意设定。需要确认正常用户在一次页面操作中会发起多少次 API 请求。

限流不等于拒绝:对于重要接口(支付、下单),限流后应该返回友好的提示信息(如"系统繁忙,请稍后重试"),而不是直接返回一个难看的 429 页面。

限流需要配合降级:限流是保护手段,不是解决方案。如果限流失效(如攻击流量超过 Nginx 处理能力),需要配合 WAF、CDN、DDoS 高防等其他手段组合防御。

十、总结

Nginx 限流配置的核心三要素:

 

1. limit_req_zone(定义限流规则) → 2. limit_req(应用到指定位置) → 3. limit_req_status(自定义拒绝码)

 

场景 推荐配置
登录接口防刷 rate=2r/s burst=5 nodelay
普通 API rate=20r/s burst=30 nodelay
多租户 API Key 作为 key + 单租户速率配置
内部系统 IP 白名单跳过限流
大促活动 临时降低 rate,配合 error_page 返回排队页

生产环境的最佳实践:

用 429 而不是 503 表示限流拒绝,方便监控和排查

用 delay=N 参数让突发请求中的小部分被延迟而非直接拒绝

用 $binary_remote_addr 而不是 $remote_addr 作为 key

用 geo + map 模块在白名单 IP 上跳过限流

新策略先干跑模式验证,再正式启用

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分