高并发防护:Nginx 流量控制

问题背景

电商大促、接口被人刷、恶意爬虫、突发流量------这些场景有一个共同的特点:流量超过了后端服务的处理能力。如果不在接入层做流量控制,所有请求一股脑打到后端,后端服务会先扛不住:数据库连接池耗尽、内存溢出、CPU 打满,最终整个服务雪崩。

Nginx 是互联网架构中最常见的流量网关,几乎所有 Web 服务都会在前面加一层 Nginx。它天然支持多种流量控制机制:请求速率限制、并发连接数限制、单个 IP 的连接数限制、带宽限制、以及更复杂的基于地理位置、请求路径、认证状态的流量调度。

本文面向初中级运维工程师和后端开发,讲解 Nginx 流量控制的核心模块(limit_req、limit_conn、limit_rate)、配置语法、实战场景、排查路径和常见坑点。内容基于 Nginx 1.24(主流发行版默认版本),但限流模块的基本语法在 1.18、1.22 等版本中保持兼容。

核心概念

Nginx 流量控制的三把刀

Nginx 的流量控制主要靠三个模块:

limit_req:请求速率限制。基于漏桶算法(leaky bucket),限制客户端的请求速率。比如限制每个 IP 每秒最多 100 个请求,超过的请求会被延迟处理或直接拒绝。适合防爬虫、防刷接口、防 CC 攻击。

limit_conn:并发连接数限制。限制同一个 IP(或同一个虚拟服务器)同时建立的连接数。比如限制每个 IP 最多 10 个并发连接,超过的连接会被拒绝。适合防止一个客户端占满所有连接。

limit_rate:带宽限制。限制单个连接的最大传输速率。比如限制单个客户端下载速度最大 500KB/s,防止一个大文件请求把带宽吃满。适合限制文件下载服务、CDN 源站等场景。

这三个模块可以单独使用,也可以组合使用。常见的组合是:limit_req + limit_conn + limit_rate,既限制请求速率,又限制并发连接,还限制单连接带宽。

限流算法的选择

Nginx 支持两种限流算法:

漏桶算法(Leaky Bucket):请求以固定速率处理,超出桶容量的请求被缓存或拒绝。漏桶算法的特点是"削峰填谷"------即使流量突发,输出速率是恒定的。Nginx 的 limit_req 使用的是漏桶算法。

令牌桶算法(Token Bucket):系统以固定速率向桶中添加令牌,请求需要获取令牌才能处理。令牌桶允许一定程度的突发流量(桶内已有的令牌可以一次性用完)。Nginx 的 limit_req 也支持令牌桶模式(通过 burst 参数)。

连接数限制不使用算法:limit_conn 就是简单的计数,超出数量直接拒绝,不涉及算法。

限流范围:全局还是局部

Nginx 的限流可以作用在多个层级:

HTTP 层级(http {} 块):对整个 Nginx 实例生效,是全局配置。

Server 层级(server {} 块):对特定虚拟主机生效。

Location 层级(location {} 块):对特定 URL 路径生效,最常用。

ngx_http_map_module:配合 map 指令可以根据变量灵活设置限流范围。

优先级:Location > Server > HTTP。内层配置会覆盖外层配置。

limit_req 请求速率限制

基本配置

limit_req 是最常用的限流模块。启用限流需要两个指令:

limit_req_zone:定义限流规则(共享内存区域),告诉 Nginx "在哪里限流、按什么 key 限流、限到多少"。

limit_req:应用限流规则,在 server 或 location 块中使用。

复制代码
http {
    # 定义限流区域
    # $binary_remote_addr 是客户端 IP 的二进制形式(比 $remote_addr 更省内存)
    # zone=api_limit:10m 表示共享内存区域名为 api_limit,大小 10MB
    # rate=10r/s 表示限制每秒 10 个请求
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    server {
        listen 80;

        location /api/ {
            # 使用限流区域,并设置桶大小(burst)和拒绝行为
            # burst=20 表示最多缓存 20 个超出速率的请求(令牌桶效果)
            # nodelay 表示 burst 内的请求不延迟立即处理(但总量仍受 rate 限制)
            limit_req zone=api_limit burst=20 nodelay;

            proxy_pass http://backend;
        }
    }
}

limit_req_zone 放在 http {} 块中(全局配置),limit_req 放在 location {} 块中(应用配置)。

burst 和 nodelay 的作用

burst 是理解 limit_req 的关键。假设 rate=10r/s, burst=20:

  • 前 10 个请求会立即处理(每秒 10 个)。

  • 第 11-30 个请求会进入 burst 队列(最多 20 个),依次等待处理(每秒处理 10 个)。

  • 第 31 个及之后的请求会被拒绝(返回 503 或按下文的配置处理)。

如果没有 burst,所有超出 rate 的请求都会直接被拒绝。有了 burst,系统会"容忍"一定程度的突发流量,而不是一刀切拒绝。

nodelay 的影响

  • 不带 nodelay:burst 队列中的请求会有延迟(按 rate 速率排队处理)。

  • 带 nodelay:burst 队列中的请求会立即处理(不排队),但 rate 仍然生效(burst 耗尽后立即拒绝)。nodelay 适合"允许短暂突发但不允许持续超速"的场景。

实际例子:rate=10r/s, burst=20, nodelay。第一个 30 个请求会全部进入(10 个正常 + 20 个 burst),之后的请求会被拒绝。如果 30 个请求是在 1 秒内打完的,实际 QPS 是 30,但持续时间只有 1 秒。如果流量持续超过 10r/s,burst 会在几秒内耗尽。

多维度限流 key

除了按 IP 限流,还可以按其他维度:

复制代码
# 按 server_name(虚拟主机)限流
limit_req_zone $server_name zone=server_limit:10m rate=100r/s;

# 按请求路径限流(需要配合 map)
map $request_uri $req_uri_key {
    ~^/api/   $binary_remote_addr;
    ~^/admin/ $binary_remote_addr;
    default   "";
}

limit_req_zone $req_uri_key zone=path_limit:10m rate=50r/s;

# 按 header 值限流(如 API Key)
limit_req_zone $http_authorization zone=apikey_limit:10m rate=100r/s;

# 按 cookie 值限流
limit_req_zone $cookie_session_id zone=session_limit:10m rate=5r/s;

自定义限流返回码

默认情况下,超出限流的请求返回 503(Service Temporarily Unavailable)。可以自定义:

复制代码
location /api/ {
    limit_req zone=api_limit burst=20;
    # 返回 429 Too Many Requests(更符合语义)
    limit_req_status 429;

    # 自定义错误页面
    error_page 429 /err429.html;
}

429 比 503 更精确地表达了"请求过多"的语义,建议使用 429。

多级限流配置

一个完整的多级限流策略,通常是这样设计的:

复制代码
http {
    # 全局限流:每个 IP 每秒 100 个请求(宽松,兜底)
    limit_req_zone $binary_remote_addr zone=global:10m rate=100r/s;

    # API 限流:每个 IP 每秒 10 个请求(严格,防刷)
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    # 登录接口:每个 IP 每秒 1 个请求(更严格,防暴力破解)
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;

    server {
        listen 80;

        # 登录接口:burst=5,允许小范围突发
        location = /login {
            limit_req zone=login burst=5 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }

        # API 接口:burst=20,rate 更严格
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }

        # 其他请求:全局限流兜底
        location / {
            limit_req zone=global burst=50;
            proxy_pass http://backend;
        }
    }
}

这样设计的好处:登录接口最严格(直接防暴力破解),API 接口次之(防爬虫),其他请求最宽松(不影响正常用户)。

限流与共享内存

limit_req_zone 使用共享内存(nginx.conf 中配置)来存储限流状态。共享内存在 Nginx worker 进程之间共享,因此可以实现分布式限流(多个 worker 进程共享同一个计数器)。

共享内存大小选择:1MB 大约可以存储 16000 个 key(IP)。10MB 大约可以存储 160000 个 key。如果你的服务面向大量独立 IP,10-20MB 通常够用;如果需要精确限流到更多 IP,增加共享内存大小。

可以通过 /proc/net/ip_tablesnginx -V 确认共享内存配置。

limit_conn 并发连接数限制

基本配置

limit_conn 的配置比 limit_req 简单,因为它不需要 burst 参数(连接是建立/断开的,不存在"排队"的概念)。

复制代码
http {
    # 定义连接数限流区域
    # zone=conn_limit:10m 表示共享内存区域名为 conn_limit,大小 10MB
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    server {
        listen 80;

        location /download/ {
            # 限制每个 IP 最多 5 个并发连接
            limit_conn conn_limit 5;

            # 限制整个 server 最多 1000 个并发连接
            limit_conn conn_limit 1000;

            # 带宽限制:单连接最大 500KB/s
            limit_rate 500k;

            alias /data/files/;
        }
    }
}

连接数 vs 请求数

连接数和请求数是两个不同的概念:

  • 连接:TCP 连接,建立时消耗一个连接数,关闭时释放。一个连接可以发送多个 HTTP 请求(HTTP/1.1 keep-alive)。

  • 请求:HTTP 请求。每个 HTTP 请求都经过一个 TCP 连接发送。

如果只限制连接数,不限制请求数,一个客户端可以通过一个连接发送大量请求(比如在 1 个连接里每秒发 1000 个请求),后端服务可能还是扛不住。

建议同时配置 limit_req 和 limit_conn,双重保险。

连接数限制的实际效果

当连接数超限后,Nginx 会直接关闭多余的连接(不转发到后端)。客户端会看到连接被重置或收到 500 错误。这种行为比 limit_req 更"粗暴"------limit_req 还会让请求排队或返回 503,limit_conn 直接断连接。

实际使用中,limit_conn 适合以下场景:

  • 限制单个 IP 的并发下载数量(防止多线程下载工具把带宽吃满)

  • 限制 WebSocket 连接数(WebSocket 是长连接,更适合用连接数限制)

  • 限制代理到后端的连接数(保护后端服务不被过多连接打垮)

常见错误配置

limit_conn_zone 只能在 http {} 块中定义,不能在 server {} 或 location {} 块中定义。这是新手容易犯的错误:

复制代码
# 错误:limit_conn_zone 在 server 块中
server {
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;  # 语法错误
}

# 正确:limit_conn_zone 在 http 块中
http {
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
}

limit_rate 带宽限制

基本配置

复制代码
server {
    listen 80;

    location /files/ {
        # 单连接最大下载速度 1MB/s
        limit_rate 1m;

        # 传输 10MB 后限速(允许前 10MB 全速下载)
        limit_rate_after 10m;

        alias /data/files/;
    }
}

limit_rate_after 指定在传输了多少数据之后开始限速。这个参数很有用:比如视频服务,前 10MB 是关键数据(视频头信息),下载完成后可以开始限速,不影响首播体验但节省带宽。

带宽限制的应用场景

带宽限制主要用在以下场景:

文件下载服务:限制单连接下载速度,防止一个大文件把服务器带宽吃满影响其他服务。配合 CDN 使用时,源站带宽通常有限,限速可以保护源站。

视频点播/直播:限制直播推流/拉流带宽,防止恶意用户上传超大流媒体文件。

API 图片服务:限制图片下载速度,防止图片被快速批量爬取。

爬虫限制:配合 User-Agent 或 Referer 检查,限制已知爬虫的带宽。

带宽限制与连接数限制的配合

单独使用带宽限制有一个问题:一个客户端可以建立多个连接,每个连接都达到带宽上限,总体带宽仍然是 N × limit_rate。

解决方案:同时使用 limit_conn 限制连接数:

复制代码
server {
    listen 80;

    location /files/ {
        # 每个 IP 最多 3 个连接
        limit_conn conn_limit 3;

        # 每个连接最大 500KB/s
        limit_rate 500k;

        alias /data/files/;
    }
}

这样每个 IP 的最大带宽是 3 × 500KB/s = 1.5MB/s,实现了有效的带宽控制。

实战配置模板

防爬虫/防刷标准配置

以下是一个完整的防爬虫限流配置,适用于大多数 Web 服务:

复制代码
http {
    # 定义限流区域
    # 全局限流(宽松兜底)
    limit_req_zone $binary_remote_addr zone=global:10m rate=200r/s;

    # API 限流(严格)
    limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;

    # 登录限流(最严格)
    limit_req_zone $binary_remote_addr zone=login:10m rate=3r/s;

    # 连接数限流
    limit_conn_zone $binary_remote_addr zone=conn:10m;

    # 白名单(不限制)
    geo $limit {
        default 1;
        10.0.0.0/8 0;
        172.16.0.0/12 0;
        192.168.0.0/16 0;
    }

    map $limit $limit_key {
        0 "";
        1 $binary_remote_addr;
    }

    server {
        listen 80;
        server_name example.com;

        # 登录接口:burst=5,rate 最严格
        location = /login {
            limit_req zone=login burst=5 nodelay;
            limit_req_status 429;
            limit_conn conn 5;
            proxy_pass http://backend;
        }

        # API 接口:burst=30,rate 严格
        location /api/ {
            limit_req zone=api burst=30 nodelay;
            limit_req_status 429;
            limit_conn conn 20;
            proxy_pass http://backend;
        }

        # 静态资源:允许突发,rate 宽松
        location /static/ {
            limit_req zone=global burst=100;
            limit_rate_after 2m;
            limit_rate 2m;
            alias /data/static/;
        }

        # 其他请求:全局限流兜底
        location / {
            limit_req zone=global burst=50;
            proxy_pass http://backend;
        }

        # 自定义错误页面
        error_page 429 /429.html;
        location = /429.html {
            internal;
            root /usr/share/nginx/html;
        }
    }
}

灰度发布限流配置

配合权重做灰度发布的流量控制:

复制代码
upstream backend {
    server 192.168.1.101:8080 weight=5;  # 新版本 50% 流量
    server 192.168.1.102:8080 weight=5;
    server 192.168.1.201:8080;             # 老版本 50% 流量
}

server {
    listen 80;

    # 基于 cookie 做灰度分流
    map $cookie_version $backend_pool {
        "new"    "backend_new";
        default  "backend_old";
    }

    upstream backend_new {
        server 192.168.1.101:8080;
        server 192.168.1.102:8080;
    }

    upstream backend_old {
        server 192.168.1.201:8080;
    }

    # 灰度版本的限流配置可以更严格
    location /api/ {
        limit_req zone=api burst=30 nodelay;
        proxy_pass http://backend;
    }
}

日志与监控

限流事件的日志记录

Nginx 限流事件会记录在 access_log 中,但默认日志格式不会区分"被限流的请求"和"正常请求"。可以通过添加变量来记录:

复制代码
http {
    # 定义日志格式,包含限流状态
    log_format limit_log '$remote_addr - $request_id - $remote_user [$time_local] '
                          '"$request" $status $body_bytes_sent '
                          '"$http_referer" "$http_user_agent" '
                          'limit_req_status=$limit_req_status '
                          'limit_conn_status=$limit_conn_status';

    server {
        access_log /var/log/nginx/access.log limit_log;

        location /api/ {
            limit_req zone=api burst=20 nodelay;
            # 设置变量(但这个变量需要 Lua 或第三方模块支持)
            set $limit_req_status "";  # 标准模块不支持,需要 http://小屋/ngx_http_req_status_module
            proxy_pass http://backend;
        }
    }
}

标准 Nginx 不自带 limit_req_status 和 limit_conn_status 变量,需要通过第三方模块(ngx_http_req_status_module)或 OpenResty(Lua)来实现精确的限流日志。如果不需要精确日志,在 access_log 中通过 $status=429 来间接统计被限流的请求数量:

复制代码
# 统计 429 错误数量
tail -f /var/log/nginx/access.log | grep '" 429 '

限流监控指标

生产环境建议收集以下限流相关指标:

  • QPS:Nginx 处理的总请求数,通过 access_log 统计。

  • 429 错误率:被限流拒绝的请求占比,过高说明限流阈值偏低或遭遇攻击。

  • limit_req 队列长度:通过 nginx-module-sts 或 OpenResty 获取。

  • 连接数 :当前并发连接数,通过 nginx -s reload 前后的 netstat | grep ESTABLISHED | wc -l 监控。

  • 后端响应时间:限流不应影响正常请求的质量,如果 200 响应的 P99 延迟也在上升,说明限流阈值或后端容量有问题。

Prometheus + Grafana 监控方案:

复制代码
# 开启 stub_status(Nginx 内置)
location /nginx_status {
    stub_status on;
    access_log off;
    allow 127.0.0.1;
    deny all;
}

/nginx_status 会输出:当前连接数(active)、已建立连接(Reading/Writing/Waiting)、处理请求数(Total)。

复制代码
# curl 输出示例
Active connections: 291
server accepts handled requests
 16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106

通过 Prometheus 的 nginx_exporter 可以将 stub_status 指标接入 Prometheus,再配合 Grafana 看板展示。

排查路径

限流配置不生效或效果不符合预期,是常见问题。以下是排查路径。

限流完全不生效

如果所有请求都不受限制,逐项检查:

复制代码
# 1. 检查 limit_req_zone 是否在 http 块中正确定义
grep -n "limit_req_zone" /etc/nginx/nginx.conf

# 2. 检查 limit_req 是否在 location 块中引用了正确的 zone 名称
grep -n "limit_req" /etc/nginx/nginx.conf

# 3. 检查 Nginx 配置语法是否正确
nginx -t

# 4. 检查 Nginx 是否 reload 成功
nginx -s reload
systemctl status nginx | grep "active (running)"

限流模块需要 ngx_http_limit_req_module 编译进 Nginx。可以通过 nginx -V | grep limit_req 确认模块是否存在。

burst 参数不生效

burst 参数只在配合 limit_req 使用时生效,不在 limit_req_zone 中配置。检查是否把 burst 写在了 limit_req_zone 里(burst 只能写在 limit_req 指令中):

复制代码
# 错误:burst 写在 limit_req_zone 中
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s burst=20;

# 正确:burst 写在 limit_req 中
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

location /api/ {
    limit_req zone=api burst=20 nodelay;
}

白名单不生效

如果配置了 geo 白名单但限流仍然生效:

复制代码
geo $limit {
    default 1;
    10.0.0.0/8 0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

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

排查 geo 和 map 的配置是否正确。可以用 nginx -T(大写 T,输出完整配置包括 include 的文件)确认配置是否正确加载。

限流阈值如何确定

限流阈值不是拍脑袋定的,需要基于历史流量数据确定。

复制代码
# 查看历史 QPS 峰值
awk '{print $4}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

# 查看按分钟统计的请求量
awk '{print substr($4, 14, 5)}' /var/log/nginx/access.log | sort | uniq -c | sort -k2 | head -50

# 查看 95 分位的并发连接数
awk '{print $NF}' /var/log/nginx/access.log | sort | awk 'BEGIN{c=0} {a[c++]=$1} END{print a[int(c*0.95)]}'

建议:限流阈值设置为历史峰值的 1.5-2 倍,给正常业务留出波动空间,同时防止正常峰值触发限流。

高级限流场景

基于地理位置的限流

配合 geo 模块,可以根据客户端 IP 的地理位置做限流:

复制代码
http {
    # 定义地理位置到变量
    geo $geo {
        default        world;
        127.0.0.0/8   local;
        10.0.0.0/8     local;
        172.16.0.0/12  local;
        192.168.0.0/16 local;

        # 中国大陆
        1.0.1.0/24     cn;
        1.0.2.0/23     cn;
        # ... 更多中国 IP 段
        # 注意:实际生产环境建议使用现成的 IP 库文件

        # 示例:读取 IP 库文件
        include /etc/nginx/geo.conf;
    }

    # 根据地理位置设置不同的限流 key
    map $geo $limit_key {
        local "";
        cn $binary_remote_addr;
        world "";
    }

    limit_req_zone $limit_key zone=cn_api:10m rate=50r/s;
    limit_req_zone $limit_key zone=world_api:10m rate=5r/s;

    server {
        location /api/ {
            # 中国用户每秒 50 请求
            limit_req zone=cn_api burst=100 nodelay;

            # 海外用户每秒 5 请求(防爬虫)
            limit_req zone=world_api burst=10 nodelay;

            proxy_pass http://backend;
        }
    }
}

使用地理位置限流时,IP 库的准确性是关键。推荐使用 MaxMind GeoIP2 数据库或阿里云/腾讯云的 IP 定位服务。

基于 User-Agent 的限流

防爬虫的另一种思路是限制特定 User-Agent 的请求频率:

复制代码
http {
    # 根据 User-Agent 设置限流 key
    map $http_user_agent $ua_key {
        default $binary_remote_addr;
        ~*bot $binary_remote_addr;
        ~*crawler $binary_remote_addr;
        ~*spider $binary_remote_addr;
        ~*curl $binary_remote_addr;
    }

    limit_req_zone $ua_key zone=ua_limit:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=ua_limit burst=20 nodelay;

            # 拒绝明显的爬虫
            if ($http_user_agent ~* "webzip|harvest|scan|grab") {
                return 403;
            }

            proxy_pass http://backend;
        }
    }
}

分布式限流与共享内存

在多 Nginx 实例环境下,每个 Nginx worker 进程有自己的限流计数器。如果不做额外处理,分布式部署时每个节点的限流是独立的,客户端可以绕过单节点限流。

解决方案是使用 Redis 等外部存储来共享限流状态:

复制代码
http {
    # 使用 Lua 和 Redis 实现分布式限流
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";

    # 初始化 Redis 连接
    lua_shared_dict ratelimit 10m;

    server {
        location /api/ {
            access_by_lua_block {
                local redis = require "resty.redis"
                local red = redis:new()

                red:set_timeout(1000)
                local ok, err = red:connect("127.0.0.1", 6379)

                if not ok then
                    ngx.log(ngx.ERR, "Redis connect error: ", err)
                    return
                end

                local key = "limit:" .. ngx.var.binary_remote_addr
                local limit = 100
                local window = 1

                local current = tonumber(red:get(key))
                if current and current >= limit then
                    ngx.exit(429)
                end

                red:incr(key)
                if not current then
                    red:expire(key, window)
                end
            }

            proxy_pass http://backend;
        }
    }
}

这种方式适合需要严格分布式限流的场景,但会引入额外的 Redis 依赖和延迟。简单场景下,Nginx 自带的共享内存限流通常够用。

动态限流与实时调整

有时需要根据后端服务的健康状态动态调整限流阈值。健康的后端可以承受更高流量,不健康的后端需要更严格的限流:

复制代码
http {
    # 使用变量存储限流 key 和 rate
    map $upstream_status $rate_key {
        default $binary_remote_addr;
        "200" $binary_remote_addr;
    }

    limit_req_zone $rate_key zone=api_normal:10m rate=100r/s;
    limit_req_zone $rate_key zone=api_healthy:10m rate=500r/s;

    server {
        location /api/ {
            # 后端全正常时用宽松配置
            set $limit_zone api_normal;
            if ($upstream_status = "200") {
                set $limit_zone api_healthy;
            }

            limit_req zone=$limit_zone burst=200 nodelay;
            proxy_pass http://backend;
        }
    }
}

更复杂的动态限流需要配合 Lua 或 OpenResty 实现。

连接复用与 upstream 连接池

限流控制的是 Nginx 到客户端的连接,但如果 Nginx 到 upstream(后端)的连接管理不当,也会成为瓶颈。

复制代码
upstream backend {
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;

    # 保持连接复用,减少连接建立开销
    keepalive 32;       # 保持 32 个空闲连接
    keepalive_requests 1000;  # 每个连接最多处理 1000 个请求后关闭
    keepalive_timeout 60s;    # 空闲连接超时
}

server {
    location /api/ {
        # 必须设置 proxy_http_version 1.1 才能使用 keepalive
        proxy_http_version 1.1;
        # 清空 Connection header,让 Nginx 自动处理
        proxy_set_header Connection "";

        limit_req zone=api burst=200 nodelay;
        proxy_pass http://backend;
    }
}

keepalive 连接池可以显著减少 Nginx 和 upstream 之间的连接建立/关闭开销,提升吞吐量。

Nginx 与后端服务的配合

upstream 健康检查

限流只能保护 Nginx 这一层,后端服务自身的保护还需要 upstream 健康检查:

复制代码
upstream backend {
    server 192.168.1.101:8080 max_fails=3 fail_timeout=30s;
    server 192.168.1.102:8080 max_fails=3 fail_timeout=30s;
    server 192.168.1.103:8080 backup;

    # 被动健康检查:某台后端连续失败 3 次,30 秒内不再尝试
    # backup 机器只会在主机器全部失败时启用
}

server {
    location /api/ {
        proxy_pass http://backend;

        # 主动健康检查(需要 nginx_upstream_check_module)
        check interval=3000 rise=2 fall=3 timeout=1000 type=http;
        check_http_send "HEAD /health HTTP/1.0\r\n\r\n";
        check_http_expect_alive http_2xx http_3xx;
    }
}

后端响应时间监控

限流不应该影响正常请求的质量。如果正常请求的响应时间也在上升,说明限流阈值或后端容量有问题:

复制代码
server {
    location /api/ {
        limit_req zone=api burst=100 nodelay;

        proxy_pass http://backend;

        # 记录后端响应时间
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;

        # 如果后端响应超过 3 秒,记录警告日志
        log_format upstream_time '$remote_addr - $request_time - $upstream_response_time';
        access_log /var/log/nginx/upstream.log upstream_time;
    }
}

通过分析 $upstream_response_time 变量,可以监控后端服务的响应质量。如果 P99 响应时间超过阈值,需要考虑扩容或优化。

连接数限制与后端容量的匹配

Nginx 到后端的连接数应该和后端服务的处理能力匹配。如果 Nginx 限流放过了大量请求,但后端只有 10 个 worker,连接池只有 10 个,会导致大量请求排队。

复制代码
upstream backend {
    server 192.168.1.101:8080;

    # 连接池大小(默认 8-16)
    keepalive 32;
}

server {
    location /api/ {
        limit_req zone=api burst=500 nodelay;
        limit_conn conn_limit 200;

        # Nginx 到后端的连接复用
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        proxy_pass http://backend;
    }
}

一般规则:upstream 的 keepalive 连接数应该等于或略大于后端服务的 worker 数。如果后端有 20 个 worker,keepalive 至少设置为 20。

常见坑点

坑一:burst 设置过大导致大量请求积压

burst 相当于一个请求缓冲区。如果 burst 设置过大(比如 1000),当流量突然增加时,大量请求会堆积在 burst 队列里。用户会感觉到:点击后等了很久才返回(因为请求在排队),然后突然收到大量 200 响应(队列中的请求集中处理完)。这不是理想的用户体验。

建议:burst 设置为 expected burst size 的 1.5-2 倍。expected burst size 是正常业务高峰期的 QPS × 用户可接受的最大等待时间。

坑二:limit_rate 影响静态资源加载体验

如果对所有静态资源都加 limit_rate,而 limit_rate 设置得过低(比如 100KB/s),用户访问一个 5MB 的图片需要等待 50 秒。这是不可接受的。

解决方案:使用 limit_rate_after 允许首屏资源全速加载,只对后续传输限速。或者对图片等静态资源不加限速,只对文件下载类资源限速。

坑三:proxy_pass 后端也有限流,导致叠加效应

如果 Nginx 配置了限流,后端服务(如 Tomcat、Spring Boot)也配置了限流,两个限流会叠加。当 Nginx 限流阈值内放过的请求,到后端又被限流了,用户会收到混乱的 429 错误,不知道是前端还是后端的问题。

建议:只在接入层(Nginx)做统一限流,后端不做限流(或只做告警不实际限制)。后端的职责是处理请求,不是限流。

坑四:共享内存不够导致限流失效

如果 limit_req_zone 的共享内存满了,新的 key(如新 IP)无法记录,老的 key 可能被覆盖。这会导致部分 IP 的限流记录丢失,限流变得不可靠。

监控共享内存使用量的方法:观察 nginx worker 进程的内存使用,或者通过第三方模块暴露指标。如果发现共享内存使用量接近上限(zone=api:10m),及时增加大小。

坑五:limit_conn 和 keepalive 混淆

HTTP/1.1 keep-alive 允许一个 TCP 连接发送多个请求。如果用 limit_conn 限制连接数,正常浏览器会在一个连接里顺序发请求,不会触发连接数限制。但如果后端服务(如 FastCGI)用了短连接,Nginx 到后端的每个请求都可能建立新连接,limit_conn 可能会错误地限制到后端的连接。

坑六:IP 获取不准确(代理/负载均衡场景)

如果 Nginx 前端还有代理或负载均衡(如 F5、Cloudflare、SLB),直接使用 $binary_remote_addr 可能获取到的是代理 IP 而不是真实客户端 IP。

解决方案:

复制代码
http {
    # 优先从 X-Forwarded-For 获取真实 IP
    map $http_x_forwarded_for $real_ip {
        default $binary_remote_addr;
        ~^(\d+\.\d+\.\d+\.\d+) $1;
    }

    limit_req_zone $real_ip zone=api:10m rate=50r/s;
}

需要注意:如果恶意用户可以伪造 X-Forwarded-For 头,这种方式可能被绕过。企业级场景建议在前端代理层做 IP 限制,Nginx 只做转发。

坑七:限流与 Gzip 压缩的交互

Gzip 压缩会消耗 CPU 资源。如果同时配置了限流和 Gzip,大量请求在限流队列中等待时,CPU 可能被 Gzip 压缩占用,导致限流效果不稳定。

建议:限流和 Gzip 不要同时对高并发路径使用,或者错峰配置。

性能调优

Nginx worker 数量与连接数

Nginx 的性能与 worker 数量配置密切相关:

复制代码
# 查看当前 worker 数量
ps aux | grep nginx | grep worker

# 查看每个 worker 的连接数
netstat -an | grep ESTABLISHED | grep nginx | wc -l

一般建议:worker 数量等于 CPU 核心数,这样每个 worker 可以充分使用一个 CPU 核心,避免进程切换开销:

复制代码
worker_processes auto;  # 自动设置为 CPU 核心数
worker_rlimit_nofile 65535;  # 每个 worker 打开的文件描述符上限

events {
    worker_connections 10240;  # 每个 worker 的最大连接数
    multi_accept on;  # 一次接受多个新连接
    use epoll;  # 使用 epoll(Linux)
}

限流共享内存大小估算

共享内存大小取决于需要存储多少个 key(IP)。估算公式:

复制代码
共享内存大小 = key数量 × (key大小 + 计数结构大小)

对于 $binary_remote_addr:
- key 大小:4 字节(IPv4)或 16 字节(IPv6)
- 计数结构大小:~16 字节(超时时间等元数据)

估算:1MB ≈ 16000 个 IPv4 key

如果你的服务预计每天有 100 万独立 IP 访问,共享内存至少需要 64MB(1000000 / 16000 ≈ 63MB)。

限流与 CPU 使用率

Nginx 的限流检查是在请求处理主流程中完成的,burst 队列会占用内存。burst 越大,占用内存越多,CPU 消耗也会略有增加。

如果发现 Nginx worker 的 CPU 使用率异常高(正常情况下 Nginx 应该 CPU 使用率很低),可能的原因:

  • Gzip 压缩级别过高(建议 1-3)

  • 正则表达式匹配过于复杂

  • SSL handshake 开销(如果是 HTTPS)

  • 限流共享内存竞争(大量 worker 同时访问共享内存)

风险提醒

生产环境修改限流配置时需要注意以下风险。

reload 会重置限流状态 。Nginx reload(nginx -s reload)会保留已有连接,但新建连接的限流状态会重新计算。共享内存中的限流计数器不会丢失,但部分请求可能在 reload 瞬间遇到限流策略切换。

不要把限流阈值设得过低。限流是为了保护后端,不是为了为难用户。如果限流阈值设得太低,正常用户也会被拒绝,影响业务。建议先观察实际流量,再调整阈值。

429 和 503 的用户体验不同。429 是标准的"请求过多"响应,客户端可以据此做退避重试(如 exponential backoff)。503 是"服务不可用",客户端可能会立即重试。推荐使用 429 并配置合理的 Retry-After header:

复制代码
add_header Retry-After 60 always;

带宽限制会影响 CDN 效果。如果 Nginx 是 CDN 源站,对源站限流会限制 CDN 节点的拉取速度,影响 CDN 缓存命中率。源站限流建议只限制直接访问(不接受直接用户请求),CDN 节点走内网专线绕过限流。

限流日志会增加磁盘 I/O。如果 access_log 写入频繁,限流事件的日志记录会显著增加磁盘写入量。建议对限流相关路径使用独立的日志文件,并配置日志轮转。

验证方式

限流配置完成后,需要验证以下内容。

配置语法正确

复制代码
nginx -t
# 输出 should be successful 表示配置正确

测试限流是否生效

复制代码
# 使用 ab(Apache Bench)压测,观察限流行为
ab -n 100 -c 10 http://example.com/api/test

# 正常情况下,大部分请求应该 200,极少部分 429(burst 内的请求)
# 如果全是 200,说明限流未生效
# 如果全是 429,说明限流阈值设置过低

# 使用 wrk 更精确的压测
wrk -t4 -c100 -d30s http://example.com/api/test

# 观察输出中的 Non-2xx responses 数量

检查限流返回码

复制代码
# 模拟大量请求
for i in {1..50}; do curl -s -o /dev/null -w "%{http_code}\n" http://example.com/api/test; done | sort | uniq -c

正常输出应该类似:40 20010 429(burst=30, rate 允许的范围)。

检查 Nginx 状态页面

复制代码
curl http://127.0.0.1/nginx_status
# 观察 Active connections 和 Total 的变化

压力测试前的准备

复制代码
# 1. 确认后端服务可以承受一定压力
# 2. 确认监控告警已就绪
# 3. 确认回滚方案已准备好
# 4. 通知相关团队

# 压测时监控 Nginx 和后端的指标
watch -n 1 "curl -s http://127.0.0.1/nginx_status"

回滚方案

限流配置如果导致正常用户被误杀,需要快速回滚。

回滚 Nginx 配置

复制代码
# 保留当前配置作为备份
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%Y%m%d%H%M%S)

# 回滚到上一个正常版本(通过 git 或备份)
git checkout HEAD /etc/nginx/nginx.conf
# 或
cp /etc/nginx/nginx.conf.bak.<timestamp> /etc/nginx/nginx.conf

# 检查配置并 reload
nginx -t && nginx -s reload

临时关闭限流

复制代码
# 如果限流是紧急故障原因,可以临时注释掉 limit_req 指令
# 编辑配置
sed -i 's/limit_req zone=api/limit_req zone=api off;/' /etc/nginx/nginx.conf

# 验证并 reload
nginx -t && nginx -s reload

快速放宽限流阈值

复制代码
# 如果限流阈值过低导致误杀,可以通过 sed 快速修改 rate 和 burst 值
sed -i 's/rate=10r/s/rate=50r/' /etc/nginx/nginx.conf
sed -i 's/burst=20/burst=100/' /etc/nginx/nginx.conf

nginx -t && nginx -s reload

GitOps 回滚

如果使用 GitOps 管理 Nginx 配置(如 Ansible、Chef、GitLab CI),恢复上一个 commit 的配置文件即可触发自动部署:

复制代码
git revert HEAD
git push
# 等待 CI/CD 流水线自动部署

总结

Nginx 流量控制的核心是三层防护:

第一层:请求速率限制(limit_req)。这是最常用的限流手段。rate 决定正常流量下的通过能力,burst 决定对突发流量的容忍程度。rate 设置的参考依据是后端服务的实际 QPS 承受能力,burst 设置的参考依据是用户可接受的最大等待时间。

第二层:并发连接数限制(limit_conn)。防止单个客户端占满所有连接。配合 limit_rate 使用,可以有效防止大文件下载把带宽吃满。连接数限制比请求速率限制更粗暴,适合对长连接(下载、WebSocket)场景进行控制。

第三层:带宽限制(limit_rate) 。限制单连接最大传输速率,适用于文件下载、视频点播等场景。使用 limit_rate_after 可以允许首屏内容全速加载,只对后续传输限速,改善用户体验。

三层限流的配置有一个通用原则:限流阈值宁高勿低,先让业务跑起来,再慢慢收紧。限流过严导致正常用户无法访问是生产故障,比限流失效更严重。上线后观察 1-2 周的流量数据,再根据实际峰值调整阈值。

最后记住:限流是手段,不是目的。限流的目的是保护后端服务不被突发流量打垮,同时保证公平性(不让少数用户占满所有资源)。如果限流总是触发,说明根本问题是后端容量不足,应该优先扩容或优化后端性能,而不是一味收紧限流阈值。

附录:限流配置速查表

以下是常见场景的限流配置参考值,可以根据实际情况调整。

场景 rate burst limit_conn 限流 key
登录接口 3r/s 5 5 IP
注册接口 5r/s 10 5 IP
API 接口(通用) 20r/s 50 20 IP
静态资源 200r/s 100 50 IP
文件下载 100r/s 20 3 IP
内部服务 1000r/s 200 100 IP
CDN 源站 500r/s 100 50 IP

不同后端容量的限流调整

后端服务处理能力是设置限流阈值的重要参考。以下是不同后端配置的参考值:

后端配置 推荐 rate 推荐 burst 说明
单机 4 核 8G 50-100r/s 100-200 基础配置
集群 4 台同配置 200-400r/s 400-800 水平扩展
K8s 10 副本 500-1000r/s 1000-2000 容器化自动扩缩容
物理机集群 20 台 2000-5000r/s 5000-10000 大型互联网服务

限流触发后的客户端行为建议

限流返回 429 后,客户端应该如何处理:

复制代码
// 客户端退避重试示例(JavaScript)
asyncfunction fetchWithRetry(url, options, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        const response = await fetch(url, options);

        if (response.status === 429) {
            // 获取 Retry-After 头
            const retryAfter = response.headers.get('Retry-After');
            const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, i) * 1000;

            console.log(`限流触发,等待 ${delay}ms 后重试`);
            awaitnewPromise(resolve => setTimeout(resolve, delay));
            continue;
        }

        return response;
    }
    thrownewError('超过最大重试次数');
}

常用限流相关的 Nginx 内置变量

复制代码
# 限流相关的内置变量
$limit_rate          # 当前连接的限速(limit_rate 的值)
$limit_rate_after    # 限速开始前的字节数
$binary_remote_addr  # 客户端 IP 的二进制形式(4 或 16 字节)
$remote_addr         # 客户端 IP(字符串形式)

# 请求相关变量
$request_time        # 请求处理时间(秒)
$request_length      # 请求长度(字节)
$body_bytes_sent     # 发送给客户端的字节数

# upstream 相关变量
$upstream_status     # upstream 返回状态码
$upstream_response_time  # upstream 响应时间

这些变量可以在 access_log 中使用,用于分析限流效果和后端性能。

高级故障排查

限流配置不生效的全面排查

如果限流完全不生效,需要逐项排查:

复制代码
# 1. 确认 limit_req_zone 是否在 http {} 块中正确定义
grep -n "limit_req_zone" /etc/nginx/nginx.conf

# 2. 确认 limit_req 是否在 location {} 块中引用了正确的 zone 名称
grep -n "limit_req" /etc/nginx/nginx.conf

# 3. 确认 Nginx 配置语法是否正确
nginx -t

# 4. 确认 Nginx 是否 reload 成功
nginx -s reload
systemctl status nginx | grep "active (running)"

# 5. 确认模块已编译
nginx -V 2>&1 | grep limit_req

限流失效的隐蔽原因

以下是一些容易忽略的限流失效原因:

原因一:proxy_cache 绕过了限流

如果配置了 proxy_cache,Nginx 可能直接从缓存返回响应,不经过限流检查:

复制代码
location /api/ {
    proxy_cache_valid 200 60s;
    proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
    proxy_cache_bypass $http_cache_bypass;

    limit_req zone=api burst=100 nodelay;
    proxy_pass http://backend;
}

如果客户端的请求命中了缓存,请求不会经过 limit_req,导致限流绕过。解决方法是确保缓存 key 不包含用户标识,或者在缓存命中时也做限流检查。

原因二:gzip 压缩导致 Content-Length 变化

如果使用 limit_rate_after,限速基于字节数。但 gzip 压缩后的响应大小和原始大小不同,可能导致限速不准确。

原因三:HTTP/2 多路复用

HTTP/2 允许多个请求在一个连接上并发传输,limit_conn 限制的是连接数而非请求数。在 HTTP/2 场景下,连接数限制的效果会弱化。

429 错误但限流未触发的排查

有时候客户端收到 429 但限流配置看起来正常:

复制代码
# 1. 检查是否有其他 Nginx 层在做限流
grep -rn "limit_req\|limit_conn" /etc/nginx/

# 2. 检查是否有上游代理/负载均衡在做限流
# Cloudflare、AWS ALB、SLB 等都可能有限流配置

# 3. 检查后端服务是否自己返回了 429
# 某些 API 网关(如 Kong、APISIX)会在后端返回 429

限流阈值计算的实际案例

假设有一个电商秒杀接口,预期 QPS 为 5000,后端有 20 台 4 核服务器,每台可以处理 250 QPS。如何设置限流?

复制代码
# 场景分析:
# 1. 后端总处理能力:20 × 250 = 5000 QPS
# 2. 留 20% 余量:5000 × 0.8 = 4000 QPS
# 3. 正常情况下,rate 设置为 4000r/s 即可
# 4. 突发流量:允许 burst = 正常 QPS × 2 = 8000

limit_req_zone $binary_remote_addr zone=seckill:50m rate=4000r/s;

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

这个案例说明:限流阈值应该基于后端实际处理能力,而不是拍脑袋。

高并发场景下的限流优化

在高并发场景(如双十一秒杀),Nginx 本身的性能也可能成为瓶颈:

复制代码
# 1. 增加 worker 数量
worker_processes auto;

# 2. 增加 worker 连接数
worker_connections 65535;

# 3. 开启多连接接受
events {
    multi_accept on;
    use epoll;
}

# 4. 开启连接复用
http {
    keepalive_timeout 65;
    keepalive_requests 10000;
}

限流与 HTTPS 的性能

如果使用 HTTPS,SSL 握手会消耗 CPU。限流和 HTTPS 同时使用时,SSL 开销可能会影响限流的准确性:

复制代码
# 优化 HTTPS 性能
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

# 开启 session 缓存减少握手
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

常用命令速查

复制代码
# 测试 Nginx 配置
nginx -t

# 重载配置
nginx -s reload

# 优雅关闭
nginx -s quit

# 强制关闭
nginx -s stop

# 查看 Nginx 主进程 PID
cat /var/run/nginx.pid

# 检查配置(包括 include 的文件)
nginx -T

# 查看 Nginx 版本和编译参数
nginx -V

# 统计日志中的 429 数量
grep '" 429 ' /var/log/nginx/access.log | wc -l

# 统计 QPS
awk '{print $4}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# 模拟限流测试(单 IP 发大量请求)
for i in {1..100}; do curl -s -o /dev/null -w "%{http_code}\n" http://example.com/api/test; done | sort | uniq -c
相关推荐
秋漓2 小时前
Nginx学习与应用
运维·学习·nginx
skywalk81636 小时前
nginx的配置软件Nginx UI
运维·nginx·ui
NGINX开源社区8 小时前
NGINX Ingress Controller 中的 Cache Policy:VirtualServer 实战指南
java·前端·nginx
johnny2338 小时前
Nginx可视化管理工具:NPM、nginx config、Nginx UI、NginxWebUI、Nginx Pulse
nginx
Linux运维老纪9 小时前
nginx 打造高性能 API 网关(‌Building a High-Performance API Gateway with Nginx)
linux·运维·mysql·nginx·云计算·运维开发
FenceRain1 天前
Nginx 升级,平滑升级不停服务
服务器·网络·nginx
武器大师721 天前
实战踩坑:Gerrit HTTP 克隆失败解决方案
运维·nginx·gerrit
Plastic garden1 天前
Docker Compose 的 RuoYi nginx exporter Prometheus + Alertmanager + 钉钉告警
nginx·docker·prometheus
一个儒雅随和的男子1 天前
Nginx底层原理介绍
运维·nginx