Nginx限流防CC:limit_req与limit_conn怎么分工

很多站长给 Nginx 加上 limit_req 之后,要么 CC 攻击照样打进来,要么正常用户被 429 挡在门外。问题通常不在参数大小,而在于没分清 limit_req 管速率、limit_conn 管并发。本文用 9 个问答讲清两者的分工、漏桶与令牌桶的差别、三个参数怎么算、多维度限流 zone 怎么写,以及限流之外还需要补什么才能形成闭环。

Q:limit_req 和 limit_conn 到底有什么区别?

一句话概括:limit_req 限制的是「单位时间发出多少请求」,limit_conn 限制的是「同一时刻存在多少连接」。前者管速度,后者管占用时长。

  • limit_req:按速率统计。一个 IP 每秒 50 个请求会被拦,因为它在短时间内的请求数超过阈值。
  • limit_conn:按并发表计数。100 个连接各自挂 300 秒不结束,每秒请求数可能只有几个,速率限流完全看不出来,但源站的工作进程被全部占住。

由此得到一条判断规则:CC 攻击打的是速率,慢速攻击打的是并发。只配其中一个,必然留一半缺口。绝大多数站点的正确做法是两个都配,并按业务路径分组设置。

Q:漏桶、令牌桶和并发上限分别在防什么?

这三种机制不是可以互换的选项,它们解决的是不同问题:

  • 漏桶(limit_req 默认行为):请求先进入队列,按 rate 匀速放行,burst 决定队列长度,队列满了直接拒绝。特点是输出平滑,但超出的请求会排队等待,表现为变慢。
  • 令牌桶(limit_req 配 nodelay):桶里预置 burst 个令牌,请求来了立即消耗令牌放行,不排队,桶空了才拒绝。特点是允许瞬时突发,只禁止长期超速。
  • 并发上限(limit_conn):纯粹的并发计数器,不随时间衰减,只有连接关闭才减一。
# http 段
limit_req_zone  $binary_remote_addr zone=req_per_ip:20m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m;

server {
    # 速率:瞬时允许突发 30 个,不排队,超出直接拒
    limit_req  zone=req_per_ip burst=30 nodelay;

    # 并发:单 IP 同时最多 50 个连接
    limit_conn conn_per_ip 50;

    limit_req_status  429;
    limit_conn_status 429;
}

为什么防 CC 更推荐 nodelay:如果不加,攻击流量会先把队列占满,正常用户的请求排在攻击请求后面一起超时,等于帮攻击者完成了一次拒绝服务。

Q:rate、burst、nodelay 这三个参数怎么定?

三个参数各有一条计算思路,别拍脑袋:

  • rate:取「单个真实 IP 的正常峰值 × 2 到 3 倍」。不要用全站峰值除以 IP 数,那是平均值,会把正常的集中访问全部误杀。
  • burst:按「一个 IP 在一秒内正常会发出的请求数」估算。手机浏览器首屏往往同时发起 20 到 40 个请求,桌面端更多。如果静态资源和动态接口共用同一个 zone,burst 要给到 50 以上,否则首屏就会看到 429。
  • nodelay:防 CC 场景必须加,理由见上一条。
# 阈值定完必须压测验证,否则只能等攻击那天才知道对不对
ab -n 5000 -c 100 https://www.example.com/
wrk -t4 -c100 -d60s --latency https://www.example.com/

# 看 429 比例:正常压测应完全无 429,攻击特征流量应被拦
awk '$9 == 429' access.log | wc -l
awk '{print $9}' access.log | sort | uniq -c | sort -rn | head

经验值参考:普通企业站单 IP rate=10r/s 配 burst=30 已经够宽松;接口站可以收紧到 rate=5r/s 配 burst=10;静态资源不要用这个 zone,交给 CDN 更划算。

Q:limit_conn 的阈值怎么算才不误伤正常用户?

关键认知只有一句:限流的计数维度是 IP,但 IP 不等于用户。企业出口 NAT、运营商 CGNAT、校园网,一个公网 IP 后面可能有几百上千人。

# 阈值 = 单用户浏览器并发 × 该 IP 背后可能的人数 × 安全系数
# 单用户对单域名常见 6 个并发连接,NAT 出口 200 人:
#   6 × 200 × 2 = 2400
limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m;
limit_conn_zone $server_name        zone=conn_total:2m;

server {
    limit_conn conn_per_ip 2400;   # 单 IP,给 NAT 留足余量
    limit_conn conn_total  8000;   # 源站总并发兜底,防止整体被打穿
}

两个提醒:不要把 limit_conn 设成 1 或 2,手机浏览器瞬时并发经常在 6 到 10 之间,设太小等于把正常用户挡掉;一定要配一个以 $server_name 为 key 的总并发 zone,因为单 IP 阈值对分布式攻击无效,总并发兜底是最后一道闸。

Q:两个模块同时开启,怎么在日志里分清是谁拦的?

两个模块都默认返回 503,混在一起你根本看不出是速率超了还是并发超了,调参会变成盲猜。所以第一步是把返回码分开:

server {
    limit_req  zone=req_per_ip burst=30 nodelay;
    limit_conn conn_per_ip 2400;

    limit_req_status  429;   # 速率超限
    limit_conn_status 503;   # 并发超限

    limit_req_log_level  warn;
    limit_conn_log_level error;

    log_format limit '$remote_addr $status $request_uri '
                     'req=$request_time conn=$connection_requests';
    access_log /var/log/nginx/access.log limit;
}

顺手说一句:不要用 444 做普通限流。444 直接断开连接、不返回响应,客户端拿不到任何提示,你自己也失去诊断信息,它还更适合明确的黑名单落地。另外限流返回的 429 页面要尽量轻,返回一个纯静态 HTML 就行,不要让它再走一遍 PHP 和数据库。

Q:为什么配了限流,CC 还是能打进来?

三个最常见的原因,逐条对照就能找到漏点:

  • 单 IP 阈值对分布式无效:一千个 IP 每个只发 5 请求每秒,没有任何一个触发阈值,总量照样把源站打满。这时必须补「全局维度」与「路径维度」的限流。
  • 只限了首页:很多配置只在 location / 上写了 limit_req,攻击者直接打 /search、/api/list 这类重路径,完全没有限制。
  • 静态资源吃掉了额度:图片、CSS、JS 和动态接口共用同一个 zone,一个正常用户刷首屏就消耗掉几十次配额,攻击者再加一点就触发误封。静态资源要么单独 zone,要么直接交给 CDN。
# 关键路径单独低阈值,普通路径宽松
location /api/ {
    limit_req  zone=req_per_ip burst=5 nodelay;
    limit_conn conn_per_ip 20;
}
location /search {
    limit_req  zone=req_per_ip burst=3 nodelay;
}
location ~* \.(jpg|png|css|js|woff2)$ {
    # 静态资源不做 IP 限速,只限制并发,主要靠 CDN 兜住
    limit_conn conn_per_ip 100;
    expires 30d;
}

Q:多维度限流怎么配才能挡住换 IP 的 CC?

思路是把计数 key 从「纯 IP」升级成「业务维度组合」。有两个可直接复用的技巧:

  • 路径归一化:同一接口带不同参数会被算成不同的桶,导致限流形同虚设。必须先用 map 把参数归一化,让同一接口的所有请求落进同一个桶。
  • 空 key 机制:Nginx 对空字符串 key 的请求不计入 limit_req 统计,这是给白名单放行、同时只限可疑来源的基础手法。
map $uri $uri_group {
    default                            "";
    "~*^/api/(list|search|detail)"     "api_hot";
    "~*^/user/"                        "user_zone";
}

map $uri_group $limit_key {
    default                             $binary_remote_addr;
    "api_hot"                           "hot:$binary_remote_addr";   # 热点接口按 IP 独立计数
}

limit_req_zone $limit_key zone=req_multi:20m rate=5r/s;

# 同 Cookie 维度:识别换 IP 但保留 Cookie 的客户端
limit_req_zone $cookie_uid zone=req_uid:20m rate=10r/s;

注意两点:不要直接用 $request_uri 做 key,参数一变就是一个新桶,等于没有限流;Cookie 维度不能单独依赖,脚本不带 Cookie 时 key 为空,反而绕过限速,所以它只能作为 IP 维度的补充。

Q:限流之外还要配什么才能形成闭环?

限流只是 Nginx 这一层,真正的抗 CC 是分层配合:

  • CDN 层:吸收静态请求和四层流量峰值,让源站只处理动态请求。源站压力下降一个量级,限流阈值就不用卡得那么死。
  • WAF 层:按 URL 频次、User-Agent、指纹、验证码挑战做拦截,处理「分布式且每个 IP 都合规」的攻击。
  • Nginx 层:速率与并发兜底,也就是本文讲的部分。
  • 应用层:登录、短信、搜索这类接口加人机验证与业务风控。
# 灰度上线:先观察不拦,确认阈值合理再开启
# 开源版没有 dry run,做法是把 rate 放大 10 倍配合 warn 日志,观察一周
limit_req_log_level warn;
# 观察 access.log 中 429 的来源与时间分布,确认误伤率为零后再收紧

上线顺序建议:先只记录不拦截,再开普通路径,最后开关键接口。观察期一定要包含周末和业务高峰,很多误伤只在流量集中的时段才会暴露。

Q:还有什么补充建议?

  • 限流挡不住「慢」:Slowloris 这类攻击每个连接请求极少,速率限流完全看不见,必须靠 limit_conn 加上 client_header_timeout、client_body_timeout 一起收紧。
  • 阈值跟着业务走:大促、上新、投放之前复核一次,别用三个月前的数字。
  • 先降速再封禁:第一档处置永远是限速,最后一档才是封 IP。NAT 出口一封,封的是一整栋楼。
  • 规则独立成文件:把 limit 相关配置放进单独的 include 文件,改错时能单文件回滚,不用整站 reload 排查。

一句话总结:limit_req 决定「多快算超速」,limit_conn 决定「占多少算失控」,两者配合再叠加关键路径低阈值与全局兜底,才是可用的 Nginx 防 CC 配置。