问题背景
电商大促、接口被人刷、恶意爬虫、突发流量——这些场景有一个共同的特点:流量超过了后端服务的处理能力。如果不在接入层做流量控制,所有请求一股脑打到后端,后端服务会先扛不住:数据库连接池耗尽、内存溢出、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_tables或nginx -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
";
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 数量
检查限流返回码:
# 模拟大量请求 foriin{1..50};docurl -s -o /dev/null -w"%{http_code} "http://example.com/api/test;done| sort | uniq -c
正常输出应该类似:40 200和10 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./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) asyncfunctionfetchWithRetry(url, options, maxRetries =3){ for(leti =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; } returnresponse; } 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!MD5; ssl_prefer_server_ciphers on; # 开启 session 缓存减少握手 ssl_session_cache shared10m; 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 发大量请求)
foriin{1..100};docurl -s -o /dev/null -w"%{http_code}
"http://example.com/api/test;done| sort | uniq -c
参考资料
Nginx 官方文档:http://nginx.org/en/docs/http/ngx_http_limit_req_module.html
Nginx 限流模块源码:ngx_http_limit_req_module.c
Leaky Bucket 算法详解:https://en.wikipedia.org/wiki/Leaky_bucket
OpenResty 官方文档:https://openresty.org/
限流算法比较:令牌桶 vs 漏桶
-
Web
+关注
关注
2文章
1313浏览量
75275 -
流量控制
+关注
关注
0文章
31浏览量
9953 -
nginx
+关注
关注
0文章
203浏览量
13267
原文标题:高并发防护:Nginx 流量控制实战教程
文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
ATM流量控制器IP核的设计和实现
【labview课程设计】基于labview的流量控制系统和电子秤
流量控制仪表和液位控制仪表系统故障分析步骤
定量控制系统
VIC-D145 MKP流量控制器
MKP TSC-230 质量流量控制器
讲解矢量控制的基础
什么是数据通信的信息流量控制
什么是ATM流量控制和拥塞控制
气动泵流量控制系统的设计
基于单片机的流量控制系统设计
系统讲解Nginx流量控制的核心模块与基本配置
评论