Nginx 限流与防刷配置:保护后端服务的实用方案

后端服务最怕什么?不是代码有 bug,而是流量瞬间被打满。爬虫、恶意刷接口、CC 攻击、甚至自家前端写了个死循环,都能让一台健康的服务器在几秒内 CPU 飙满、响应超时。Nginx 作为流量入口,是实施限流与防刷的第一道防线。这篇文章不讲理论空话,直接给出可落地的配置方案。

为什么限流要放在 Nginx 层

在应用层做限流当然可以,但有两个问题:一是请求已经到达应用,消耗了连接、线程和内存资源;二是每个服务都要重复实现限流逻辑,维护成本高。Nginx 在反向代理层拦截,能在请求进入后端之前就决定放行还是拒绝,效率高、覆盖广、对业务代码零侵入。

Nginx 提供了两个核心限流模块:

  • ngx_http_limit_req_module:基于漏桶算法限制请求速率
  • ngx_http_limit_conn_module:限制并发连接数

配合 mapgeoif 等指令,可以组合出相当灵活的防刷策略。

基础限流:限制请求速率

定义限流区域

http 块中定义共享内存区域:

http {
    # 以客户端 IP 为 key,分配 10MB 共享内存,每秒允许 10 个请求
    limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;

    # 以服务器名称为 key,限制单虚拟主机的总速率
    limit_req_zone $server_name zone=req_per_server:10m rate=1000r/s;
}

$binary_remote_addr$remote_addr 更省内存(4 字节 vs 7-15 字节),10MB 大约能存 16 万个 IP 的状态。rate=10r/s 表示每个 IP 每秒允许 10 个请求,Nginx 实际按毫秒精度处理,即每 100ms 放行一个。

在 location 中启用限流

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

这里有几个关键参数需要理解:

  • burst:允许的突发请求数。漏桶算法中,超出速率的请求会排队等待,burst 就是队列长度。burst=20 意味着瞬间最多有 20 个请求排队。
  • nodelay:排队请求立即转发,不延迟处理。如果不加这个参数,超出的请求会按速率逐个放行,客户端会感受到明显延迟。
  • limit_req_status:被限流时返回的状态码,默认是 503,建议改为 429(Too Many Requests),语义更准确。

不同接口用不同策略

登录、短信验证码、支付等敏感接口需要更严格的限流:

limit_req_zone $binary_remote_addr zone=strict:10m rate=1r/s;

location /api/sms/send {
    limit_req zone=strict burst=2 nodelay;
    limit_req_status 429;
    proxy_pass http://backend;
}

每秒 1 次、突发 2 次,基本能挡住短信轰炸类攻击。

并发连接限制

请求速率限制的是"每秒多少个请求",但有些慢接口(如文件上传、报表导出)单个请求就占用大量资源,需要限制并发连接数:

http {
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
    limit_conn_zone $server_name zone=conn_per_server:10m;

    server {
        location /api/upload {
            limit_conn conn_per_ip 5;
            limit_conn conn_per_server 200;
            limit_conn_status 429;
            proxy_pass http://backend;
        }
    }
}

每个 IP 最多 5 个并发连接,整个虚拟主机最多 200 个。超过限制的请求直接返回 429。

白名单:别把自己人限了

限流最尴尬的情况是把自己的监控、CDN 回源或内部调用给限了。用 geo 模块排除可信 IP:

geo $limited {
    default 1;
    10.0.0.0/8 0;
    172.16.0.0/12 0;
    192.168.0.0/16 0;
    203.0.113.50 0;  # 公司出口 IP
}

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

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

$limit_key 为空字符串时,Nginx 不会对该请求做限流统计。这种写法比在 location 里写一堆 allow/deny 更清晰,也更容易维护。

防刷进阶:识别异常流量

拦截空 User-Agent 和异常 Referer

大量爬虫和扫描器不会设置 User-Agent:

map $http_user_agent $bad_ua {
    default 0;
    ""      1;
    "~*curl"  1;
    "~*python" 1;
    "~*scrapy" 1;
    "~*httpclient" 1;
}

server {
    if ($bad_ua) {
        return 403;
    }
}

注意 ifserver 块中要谨慎使用,这里只做 return 是安全的。

限制请求方法

大部分 API 只需要 GET 和 POST:

if ($request_method !~ ^(GET|POST|HEAD)$) {
    return 405;
}

防御慢速攻击

Slowloris 类攻击通过保持大量慢速连接耗尽服务器资源:

server {
    client_body_timeout 10s;
    client_header_timeout 10s;
    send_timeout 10s;
    keepalive_timeout 15s;
    client_max_body_size 10m;
    client_body_buffer_size 128k;
}

这些超时参数能有效缩短恶意连接的存活时间。

日志与监控:限流不能是黑盒

被限流的请求需要记录,否则无法判断是误伤还是攻击:

log_format limit_log '$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 limit_log;

配合 limit_req_log_level warn; 可以在 Nginx error log 中看到限流触发记录。生产环境建议将 429 响应单独收集,接入监控告警,观察限流触发频率的变化趋势。

一套完整的配置示例

把上面的方案整合起来:

http {
    # 限流区域定义
    limit_req_zone $binary_remote_addr zone=general:10m rate=20r/s;
    limit_req_zone $binary_remote_addr zone=strict:10m rate=1r/s;
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

    # 白名单
    geo $trusted {
        default 0;
        10.0.0.0/8 1;
        192.168.0.0/16 1;
    }

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

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

        # 全局超时
        client_body_timeout 10s;
        client_header_timeout 10s;
        send_timeout 10s;

        # 通用接口
        location /api/ {
            limit_req zone=general burst=40 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }

        # 敏感接口
        location /api/sms/ {
            limit_req zone=strict burst=2 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }

        # 上传接口
        location /api/upload {
            limit_conn conn_per_ip 5;
            limit_conn_status 429;
            client_max_body_size 20m;
            proxy_pass http://backend;
        }
    }
}

几个容易踩的坑

共享内存不够用limit_req_zone 的 10m 不是随便写的。如果 key 是 IP,每个状态约 64 字节,10m 大约存 16 万个。超出后 Nginx 会开始淘汰旧记录,限流效果下降。高流量场景建议加到 50m 甚至 100m。

rate 设得太低:前端一个页面可能同时发十几个 API 请求,如果 rate 设成 5r/s,正常用户都会被限。建议先观察线上 QPS 分布,再设定阈值。

忘记 nodelay:不加 nodelay 时,burst 内的请求会被延迟处理,用户会感觉接口变慢。除非你确实想平滑流量,否则都建议加上。

只限流不监控:限流阈值是拍脑袋定的,不监控触发频率就不知道是否合理。至少要观察一周的 429 比例,再调整参数。

限流不是一劳永逸的事,而是一个持续调优的过程。从宽松的策略开始,观察数据,逐步收紧,才能在保护后端和用户体验之间找到平衡点。

未经允许不得转载:任鹏个人博客 » Nginx 限流与防刷配置:保护后端服务的实用方案

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏