Skip to content

Nginx / OpenResty 深度解析 ​

#组件 · #Nginx · #反向代理 · #负载均衡 · #OpenResty · #Web服务器

从进程模型到配置实战,覆盖反向代理、负载均衡、WebSocket、流量复制、location 匹配及所有关键参数。

一、进程模型 ​

1.1 Master-Worker 架构 ​

mermaid
flowchart TB
    Master["Master 进程 (root)<br/>· 读取配置、管理 Worker<br/>· 信号控制: reload/reopen/stop<br/>· 不处理请求"]

    subgraph Workers["Worker 进程池"]
        W0["Worker 0<br/>(epoll 事件循环)"]
        W1["Worker 1<br/>(epoll 事件循环)"]
        WN["Worker N<br/>(epoll 事件循环)"]
    end

    Master -->|"fork() 创建"| Workers
    Master -->|"监控健康状态"| Workers

    Socket["共享监听 socket<br/>SO_REUSEPORT 内核级负载均衡"]
    Socket --> W0
    Socket --> W1
    Socket --> WN

    CacheMgr["Cache Manager<br/>(可选, 缓存管理)"]
    CacheLoader["Cache Loader<br/>(可选, 缓存加载)"]
    Master -.-> CacheMgr
    Master -.-> CacheLoader

核心特性:

特性说明
事件驱动epoll (Linux) / kqueue (FreeBSD),非阻塞 I/O
工作进程worker_processes auto (通常=CPU 核数)
惊群问题accept_mutex off + SO_REUSEPORT (1.11.3+) 内核级负载均衡
优雅重启nginx -s reload 旧 worker 处理完退出,新 worker 读新配置

惊群问题(Thundering Herd)为什么存在?怎么解决的? ​

当多个 Worker 进程同时 epoll_wait 在同一个 listen socket 上时,一个新连接到来会唤醒所有 Worker,但只有一个能 accept 成功,其余白白被唤醒(浪费 CPU)。

传统方案 — accept_mutex(Nginx 早期默认 on):
  Worker 们竞争一个共享锁(文件锁或原子变量)
  只有拿到锁的 Worker 才把 listen fd 加入自己的 epoll
  → 同一时刻只有一个 Worker 监听 → 无惊群
  → 但锁竞争本身有开销,且负载不均(拿到锁的 Worker 可能已经很忙)

现代方案 — SO_REUSEPORT(Linux 3.9+,Nginx 1.9.1+):
  每个 Worker 创建自己独立的 listen socket,绑定相同端口
  内核为每个 socket 维护独立的 accept 队列
  新连接到来时,内核用哈希算法选择一个 socket 投递
  → 完全无锁、无惊群、内核级负载均衡
  → 性能比 accept_mutex 高 2-3 倍(尤其在高并发短连接场景)

配置:
  listen 80 reuseport;   # 启用 SO_REUSEPORT
  # accept_mutex 在 1.11.3+ 默认 off(因为 reuseport 更优)

为什么 accept_mutex off + SO_REUSEPORT 是最优组合?

accept_mutex on 的问题:
  1. 锁竞争开销(虽然是 spinlock,但高并发下仍有 CPU 浪费)
  2. 负载不均:拿到锁的 Worker 可能已经有很多连接在处理
  3. 延迟抖动:没拿到锁的 Worker 要等下一轮

SO_REUSEPORT 的优势:
  1. 零锁竞争(每个 Worker 有独立的 accept 队列)
  2. 内核级负载均衡(按源 IP:Port 哈希分配)
  3. 更好的 CPU 缓存亲和性(连接始终在同一 Worker 处理)

1.2 OpenResty 的扩展 ​

┌────────────────────────────────┐
│         Nginx Core             │
│  ┌──────────────────────────┐  │
│  │   LuaJIT VM (嵌入每个Worker│  │
│  │   · init_by_lua          │  │
│  │   · rewrite_by_lua       │  │
│  │   · access_by_lua        │  │
│  │   · content_by_lua       │  │
│  │   · header_filter_by_lua │  │
│  │   · body_filter_by_lua   │  │
│  │   · log_by_lua           │  │
│  │   · balancer_by_lua      │  │
│  └──────────────────────────┘  │
│          ↑ 协程调度 (同步写法)   │
│  ┌──────────────────────────┐  │
│  │  cosocket API (异步非阻塞) │  │
│  │  TCP/UDP/SSL/MySQL/Redis │  │
│  └──────────────────────────┘  │
└────────────────────────────────┘

每个请求在 LuaJIT VM 中是一个协程,通过 ngx.sleep / tcpsock:receive 等自动 yield/resume。


二、核心配置结构 ​

/etc/nginx/
├── nginx.conf          ← 主配置
├── conf.d/
│   ├── upstream.conf   ← 上游服务器组
│   ├── proxy.conf      ← 代理通用参数
│   └── sites/
│       └── example.conf
└── mime.types

主配置骨架 ​

nginx
# === 全局块 ===
user nginx;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log warn;
worker_rlimit_nofile 65535;

# === events 块 ===
events {
    use epoll;
    worker_connections 10240;
    multi_accept on;
}

# === http 块 ===
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    # 性能优化
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;

    # 上游定义
    upstream backend {
        server 10.0.0.1:8080 weight=3 max_fails=2 fail_timeout=30s;
        server 10.0.0.2:8080 weight=1;
        keepalive 32;
    }

    # 站点配置
    include conf.d/sites/*.conf;
}

三、反向代理配置 ​

3.1 基础反向代理 ​

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

    location / {
        proxy_pass http://backend;

        # === 请求头传递 ===
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # === 缓冲 (Buffering) ===
        proxy_buffering on;                  # 开启代理缓冲
        proxy_buffer_size 4k;                # 响应头缓冲区大小
        proxy_buffers 8 4k;                  # 响应体缓冲区 (数量×大小)
        proxy_busy_buffers_size 8k;          # 忙碌时最大缓冲
        proxy_max_temp_file_size 1024m;      # 临时文件上限

        # === 超时 ===
        proxy_connect_timeout 5s;            # 连接上游超时
        proxy_send_timeout 60s;              # 发送请求到上游超时
        proxy_read_timeout 60s;              # 读取上游响应超时
        proxy_next_upstream_timeout 0;       # 尝试下一台上游的总超时

        # === 缓冲请求体(文件上传) ===
        client_body_buffer_size 128k;        # 请求体缓冲区
        client_max_body_size 10m;            # 请求体最大限制
    }
}

3.2 关键参数详解 ​

代理缓冲 proxy_buffering ​

开启 (默认):
Client ←── Nginx ──→ Upstream
        (缓冲完整    (可快速释放
         响应后发送)  上游连接)

关闭:
Client ←←←←←← Nginx ←←←←←← Upstream
        (实时传输,适合 SSE/流式)

超时时间线 ​

Client ──→ Nginx ──→ Upstream

proxy_connect_timeout:     Nginx→Upstream TCP握手最大时间
proxy_send_timeout:        Nginx→Upstream 发送请求最大时间(两次write间隔)
proxy_read_timeout:        Nginx←Upstream 接收响应最大时间(两次read间隔)
keepalive_timeout:         Client←→Nginx 长连接保持时间

$remote_addr vs X-Forwarded-For ​

                       ┌─────────┐
$remote_addr 始终=     │  Nginx  │ ← TCP 直接对端 IP (可能是上跳代理)
                       └─────────┘

X-Forwarded-For:
  客户端 → CDN → 代理A → Nginx → 后端

  $proxy_add_x_forwarded_for = 原XFF + ", " + $remote_addr
  → "client_ip, cdn_ip, proxyA_ip"

获取真实客户端 IP:

nginx
# 信任前置代理的 XFF
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# 此后 $remote_addr = 真实客户端 IP

四、location 匹配规则 ​

4.1 匹配优先级 ​

优先级 (高→低):

1️⃣ = /exact       精确匹配 (立即停止)
2️⃣ ^~ /prefix     优先前缀匹配 (匹配后停止正则)
3️⃣ ~ \.php$      区分大小写正则
4️⃣ ~* \.PHP$     不区分大小写正则
5️⃣ /prefix       普通前缀匹配 (继续尝试正则)
6️⃣ /             通用匹配

4.2 匹配流程 ​

请求: GET /images/logo.png HTTP/1.1

1. 遍历所有精确匹配 (=):   没有 =/images/logo.png → 继续
2. 遍历所有前缀匹配 (^~/普通):
   ✅ /images/ 匹配 (记录,继续)
   ✅ / 匹配 (记录,但更长的不替换)
3. 选最长前缀: /images/
4. 如果有 ^~ 标记 → 直接使用该前缀
   没有 ^~ → 继续
5. 遍历正则 (~/~*):
   ✅ ~* \.(png|jpg)$ 匹配!
6. 使用正则匹配结果 → proxy_pass ...

4.3 实战示例 ​

nginx
server {
    # 精确匹配 → API 健康检查
    location = /health {
        return 200 "OK\n";
        add_header Content-Type text/plain;
    }

    # 优先前缀 → 静态资源不经过正则
    location ^~ /static/ {
        alias /var/www/static/;
        expires 7d;
        add_header Cache-Control "public, immutable";
    }

    # 正则 → 后端 API
    location ~ ^/api/(v[12])/ {
        proxy_pass http://backend_$1;
    }

    # 正则 → PHP
    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
    }

    # 普通前缀 → SPA 前端
    location / {
        root /var/www/frontend;
        try_files $uri $uri/ /index.html;
    }
}

五、负载均衡 ​

5.1 算法 ​

nginx
upstream backend {
    # 1. 轮询 (默认)
    # server 10.0.0.1:8080;
    # server 10.0.0.2:8080;

    # 2. 加权轮询
    server 10.0.0.1:8080 weight=3;
    server 10.0.0.2:8080 weight=1;

    # 3. IP Hash (会话保持)
    ip_hash;

    # 4. 最少连接
    # least_conn;

    # 5. Hash (自定义 key)
    # hash $request_uri consistent;

    # 6. 随机 (1.15.1+)
    # random two least_conn;
}

5.2 健康检查 ​

nginx
upstream backend {
    server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:8080 backup;    # 备用 (只在其他都不可用时)
    server 10.0.0.4:8080 down;      # 手动下线
}
  • max_fails=3:30 秒内失败 3 次标记为不可用
  • fail_timeout=30s:不可用 30 秒后重新探测
  • 探测方式:通过 proxy_connect_timeout 判断连接是否成功

5.3 长连接池 ​

nginx
upstream backend {
    server 10.0.0.1:8080;
    keepalive 32;              # 最多 32 个空闲长连接
    keepalive_timeout 60s;     # 空闲超时
    keepalive_requests 1000;   # 单连接最大请求数
}

location / {
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_pass http://backend;
}

5.4 upstream 连接的完整生命周期 ​

很多人只知道配 keepalive 32,但不理解一个 upstream 连接从创建到销毁经历了什么。

一个请求到达 Nginx 后,upstream 连接的完整流程:

1. 请求到达 Worker → 选择 upstream server(按负载均衡算法)
2. 检查连接池:是否有到该 server 的空闲长连接?
   ├─ 有 → 取出复用(跳过 TCP 握手,节省 1-RTT)
   └─ 无 → 新建 TCP 连接(三次握手)
3. 发送请求到 upstream
4. 等待响应(proxy_read_timeout 控制超时)
5. 收到响应 → 转发给客户端
6. 请求完成后:
   ├─ 如果配了 keepalive 且池未满 → 放回连接池
   ├─ 如果池已满 → 关闭连接
   └─ 如果响应头有 Connection: close → 关闭连接

关键参数对连接生命周期的影响:

nginx
upstream backend {
    server 10.0.0.1:8080;

    keepalive 32;              # 连接池大小(每 Worker 独立)
    # 含义:每个 Worker 最多保持 32 个到该 upstream 的空闲连接
    # 注意:这不是"最大连接数"!活跃连接不受此限制

    keepalive_timeout 60s;     # 空闲连接存活时间
    # 超过 60s 没有新请求使用这个连接 → 关闭

    keepalive_requests 1000;   # 单连接最大复用次数
    # 一个连接被复用 1000 次后强制关闭(防止内存泄漏)
}

# 必须配合以下设置才能真正启用长连接:
location / {
    proxy_http_version 1.1;           # HTTP/1.0 不支持 keepalive
    proxy_set_header Connection "";   # 清除客户端的 Connection: close
    proxy_pass http://backend;
}

为什么 keepalive 32 不是越大越好?

每个空闲连接占用:
  - 一个文件描述符(fd)
  - 内核 TCP 缓冲区(约 4-8KB)
  - Nginx 的连接结构体(约 1.5KB)

4 个 Worker × 32 个空闲连接 × 3 个 upstream = 384 个空闲连接
如果设为 1000:4 × 1000 × 3 = 12000 个空闲连接 → 浪费资源

经验值:
  - QPS < 1000: keepalive 16-32
  - QPS 1000-10000: keepalive 64-128
  - QPS > 10000: keepalive 128-256

5.5 proxy_cache 缓存机制 ​

Nginx 可以将 upstream 的响应缓存到本地磁盘/内存,后续相同请求直接返回缓存,不再请求后端。这是 CDN 的核心原理。

nginx
# 定义缓存区域(http 块中)
proxy_cache_path /var/cache/nginx
    levels=1:2                    # 目录层级(避免单目录文件过多)
    keys_zone=my_cache:100m       # 共享内存区域名称:大小(存 key 和元数据)
    max_size=10g                  # 磁盘最大占用
    inactive=60m                  # 60 分钟无访问则淘汰
    use_temp_path=off;            # 直接写入缓存目录(避免跨分区 rename)

server {
    location /api/ {
        proxy_cache my_cache;
        proxy_cache_key "$scheme$host$request_uri";  # 缓存 key
        proxy_cache_valid 200 302 10m;               # 200/302 缓存 10 分钟
        proxy_cache_valid 404 1m;                    # 404 缓存 1 分钟
        proxy_cache_valid any 5m;                    # 其他状态码 5 分钟

        # 缓存命中时添加响应头(方便调试)
        add_header X-Cache-Status $upstream_cache_status;
        # 值: MISS / HIT / EXPIRED / STALE / UPDATING / BYPASS

        proxy_pass http://backend;
    }
}

缓存 key 的计算和磁盘布局 ​

缓存 key: "https://example.com/api/users?page=1"
  → MD5 = "a1b2c3d4e5f6..."
  → 磁盘路径: /var/cache/nginx/4/d5/a1b2c3d4e5f6...
     (levels=1:2 → 最后 1 位做第一级目录,倒数 2-3 位做第二级)

keys_zone 共享内存中存储:
  - 缓存 key 的 MD5
  - 文件路径
  - 过期时间
  - 引用计数
  → 100m 的 keys_zone 约能存 80 万个缓存条目的元数据

缓存锁(防击穿) ​

nginx
location /api/ {
    proxy_cache my_cache;
    proxy_cache_lock on;              # 缓存锁:相同 key 只有一个请求穿透到后端
    proxy_cache_lock_timeout 5s;      # 等待锁超时后直接穿透
    proxy_cache_lock_age 5s;          # 持锁超时后释放锁让其他请求穿透

    # 效果:100 个并发请求同一个 URL
    #   无 lock: 100 个请求全部打到后端(缓存击穿)
    #   有 lock: 1 个请求打到后端,99 个等待缓存填充后直接返回

    proxy_pass http://backend;
}

缓存更新策略 ​

nginx
location /api/ {
    proxy_cache my_cache;

    # 策略1: 过期后异步更新(返回旧缓存 + 后台刷新)
    proxy_cache_use_stale updating;   # 更新期间返回旧缓存
    proxy_cache_background_update on; # 后台异步更新

    # 策略2: 主动清除(需要 ngx_cache_purge 模块)
    # location ~ /purge(/.*) {
    #     proxy_cache_purge my_cache "$scheme$host$1";
    # }

    # 策略3: 条件缓存(只缓存特定条件的响应)
    proxy_cache_bypass $cookie_nocache $arg_nocache;  # 带特定参数时跳过缓存
    proxy_no_cache $http_pragma;                       # 后端说不缓存就不缓存

    proxy_pass http://backend;
}

5.6 Nginx 内存池(Pool Allocator) ​

Nginx 的高性能秘密之一:每个连接/请求有自己的内存池,请求结束时一次性释放整个池,避免逐个 free 的开销。

传统 malloc/free 的问题:
  - 每个小对象独立 malloc → 内存碎片
  - 忘记 free → 内存泄漏
  - 频繁 malloc/free → 系统调用开销

Nginx 内存池设计:
  ┌─────────────────────────────────────────┐
  │ ngx_pool_t (请求级内存池)                │
  │                                          │
  │  ┌──────┐  ┌──────┐  ┌──────┐          │
  │  │Block1│→ │Block2│→ │Block3│→ ...     │
  │  │4KB   │  │4KB   │  │4KB   │          │
  │  └──────┘  └──────┘  └──────┘          │
  │                                          │
  │  大块分配(>4KB): 单独 malloc,挂链表    │
  │  ┌──────────┐  ┌──────────┐             │
  │  │Large 1   │→ │Large 2   │→ ...       │
  │  │(32KB)    │  │(64KB)    │             │
  │  └──────────┘  └──────────┘             │
  └─────────────────────────────────────────┘

分配: 从当前 Block 的空闲位置切一块(指针前移,O(1))
     Block 满了 → 分配新 Block 挂到链表
释放: 请求结束 → ngx_destroy_pool() → 一次性释放所有 Block
     不需要逐个 free!

优势:
  - 分配极快(指针前移,无锁)
  - 无内存碎片(整块释放)
  - 无泄漏风险(池销毁时全部回收)
  - 适合"请求级"生命周期的对象

六、WebSocket 代理 ​

nginx
server {
    listen 80;
    server_name ws.example.com;

    location /ws/ {
        proxy_pass http://ws_backend;
        proxy_http_version 1.1;

        # WebSocket 必须的头部 (协议升级)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 长连接超时
        proxy_read_timeout 3600s;    # 1 小时无数据断开
        proxy_send_timeout 3600s;

        # 禁用缓冲 (实时双向通信)
        proxy_buffering off;
    }
}

七、Stream 代理(TCP/UDP 四层) ​

nginx
# === stream 块 (和 http 块平级) ===
stream {
    log_format proxy '$remote_addr [$time_local] '
                     '$protocol $status $bytes_sent $bytes_received '
                     '$session_time "$upstream_addr"';

    # === TCP 负载均衡 ===
    upstream mysql_cluster {
        server 10.0.0.1:3306 weight=3 max_fails=3 fail_timeout=30s;
        server 10.0.0.2:3306 weight=1 backup;
    }

    server {
        listen 3306;
        proxy_pass mysql_cluster;
        proxy_connect_timeout 5s;
        proxy_timeout 600s;
        proxy_buffer_size 16k;
    }

    # === HTTPS 透传 (TLS Passthrough) ===
    upstream k8s_api {
        server 10.0.0.1:6443;
        server 10.0.0.2:6443;
    }

    server {
        listen 6443;
        proxy_pass k8s_api;
        proxy_connect_timeout 5s;
    }
}

HTTP vs Stream 的区别:

维度HTTP (七层)Stream (四层)
解析内容URI、Header、Body无,只看 TCP 字节流
路由location / server_name仅 listen 端口
TLS 终结支持(解密后路由)透传(不解密)
修改内容可添加/修改 Header不可修改
适用Web/APIMySQL/Redis/K8s API 等

八、流量复制(mirror) ​

nginx
location /api/ {
    proxy_pass http://production;
    # 生产流量镜像到测试环境 (不影响主请求)
    mirror /mirror;
    mirror_request_body on;     # 同时镜像请求体
}

location = /mirror {
    internal;                    # 仅内部调用
    proxy_pass http://test_env$request_uri;
    proxy_set_header X-Mirror-From $remote_addr;
    proxy_connect_timeout 200ms; # 镜像超快失败
    proxy_read_timeout 200ms;
}

OpenResty 更灵活的流量复制:

nginx
location /api/ {
    rewrite_by_lua_block {
        -- 20% 流量灰度到新版本
        if math.random(100) <= 20 then
            ngx.var.target = "new_backend"
        else
            ngx.var.target = "old_backend"
        end
    }
    proxy_pass http://$target;
}

九、OpenResty 核心用法 ​

9.1 请求阶段介入 ​

nginx
location /dynamic/ {
    # 1. 重写阶段 (URL重写、参数校验)
    rewrite_by_lua_block {
        local args = ngx.req.get_uri_args()
        if not args.token then
            ngx.exit(403)
        end
    }

    # 2. 访问阶段 (鉴权、限流、WAF)
    access_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        red:set_timeouts(1000, 1000, 1000)
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "redis failed: ", err)
            return ngx.exit(502)
        end
        -- 令牌桶限流
        local key = "rate:" .. ngx.var.remote_addr
        local remaining = red:eval([[
            local key = KEYS[1]
            local limit = tonumber(ARGV[1])
            local window = tonumber(ARGV[2])
            local current = redis.call('INCR', key)
            if current == 1 then
                redis.call('EXPIRE', key, window)
            end
            if current > limit then
                return 0
            end
            return limit - current
        ]], 1, key, 100, 1)
        if remaining == 0 then
            ngx.exit(429)
        end
        red:set_keepalive(10000, 100)
    }

    # 3. 内容阶段 (生成动态响应)
    content_by_lua_block {
        ngx.say('{"status":"ok","time":', ngx.time(), '}')
        ngx.header["Content-Type"] = "application/json"
    }

    # 4. 日志阶段 (采样、上报)
    log_by_lua_block {
        local latency = ngx.now() - ngx.req.start_time()
        if latency > 1.0 then
            ngx.log(ngx.WARN, "slow request: ", ngx.var.request_uri,
                    " latency=", latency)
        end
    }
}

9.2 动态路由 ​

nginx
location /api/ {
    set $backend '';
    rewrite_by_lua_block {
        local cjson = require "cjson"
        local res = ngx.location.capture("/internal/gateway")
        if res.status == 200 then
            local data = cjson.decode(res.body)
            ngx.var.backend = data.upstream
        else
            ngx.var.backend = "default_backend"
        end
    }
    proxy_pass http://$backend;
}

9.3 连接池管理 ​

lua
-- resty.redis 复用连接池 (cosocket)
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
-- ... 使用 ...
-- 放回连接池: pool_size, idle_timeout(ms)
red:set_keepalive(10000, 100)

-- resty.mysql
local mysql = require "resty.mysql"
local db = mysql:new()
db:set_timeout(1000)
local ok, err = db:connect{host="127.0.0.1", port=3306, database="test",
                            user="root", password=""}
-- ... 使用 ...
db:set_keepalive(10000, 100)

9.4 Nginx 执行阶段完整谱系 ​

OpenResty 在每个 Worker 的生命周期中暴露了 11 个执行阶段,理解各阶段的触发时机和适用场景是写好 OpenResty 代码的前提:

请求到达 →
┌─────────────────────────────────────────────────────┐
│ ① post_read      读取请求头后 (realip 模块在此工作)    │
│ ② server_rewrite  server{} 块内的 rewrite 指令        │
│ ③ find_config     location 匹配 (不可介入)             │
│ ④ rewrite         location{} 内的 rewrite 指令        │
│ ⑤ post_rewrite   rewrite 阶段收尾 (不可介入)           │
│ ⑥ preaccess      访问控制预备阶段                      │
│ ⑦ access         访问控制 (鉴权、限流)                 │
│ ⑧ post_access    访问控制收尾 (不可介入)               │
│ ⑨ try_files      try_files 指令处理 (不可介入)         │
│ ⑩ content        内容生成 (代理/静态文件/动态响应)      │
│ ⑪ log            日志记录 (请求结束, 但仍可访问上下文)   │
└─────────────────────────────────────────────────────┘
→ 响应返回客户端

各阶段对应的 *_by_lua 指令及用途:

阶段*_by_lua 指令典型用途
② server_rewriterewrite_by_lua (server 块)全局URL重写
④ rewriterewrite_by_lua (location 块)URI重写、参数校验、重定向
⑦ accessaccess_by_lua鉴权、限流、WAF、IP黑白名单
⑩ contentcontent_by_lua生成响应内容(与 proxy_pass 互斥)
⑩ (body filter)body_filter_by_lua修改响应体(每chunk触发一次)
⑩ (header filter)header_filter_by_lua修改响应头
⑪ loglog_by_lua日志采样、慢请求上报、自定义埋点

关键限制:

lua
-- ❌ 错误: access_by_lua 中不能调用 content_by_lua 的 API
access_by_lua_block {
    ngx.say("ok")  -- 报错! access 阶段没有输出能力
}

-- ❌ 错误: content_by_lua 和 proxy_pass 不能共存
location / {
    content_by_lua_block { ngx.say("hello") }
    proxy_pass http://backend;  -- 永远不会执行
}

-- ✅ 正确: 用 rewrite/access 做预处理,proxy_pass 做转发
location /api/ {
    access_by_lua_block {
        -- 鉴权逻辑
    }
    proxy_pass http://backend;  -- 鉴权通过后转发
}

初始化阶段(Worker 生命周期,非请求级别):

指令触发时机典型用途
init_by_luaMaster 启动时(只执行一次)加载全局模块、预编译正则
init_worker_by_lua每个 Worker 启动时初始化连接池、启动 ngx.timer 定时任务

9.5 cosocket 协程调度详解 ​

cosocket 是 OpenResty 性能的核心——它让异步非阻塞 I/O 能以同步写法表达。

9.5.1 协程调度原理 ​

┌──────────────────────────────────────────────┐
│                Nginx Worker                   │
│                                               │
│  请求 A 协程:  connect() → yield ┐            │
│  请求 B 协程:  执行业务逻辑       │            │
│  请求 C 协程:  receive() → yield  │   epoll    │
│               ...                │   event    │
│  请求 A 协程:  ← resume ←───────┘  (fd就绪)  │
│                                               │
│  每个请求 = 一个 Lua 协程                     │
│  每个 I/O 操作 = yield + 注册 epoll 事件      │
│  epoll 返回时 = resume 对应协程               │
└──────────────────────────────────────────────┘

与传统同步 I/O 的本质区别:

模式线程/进程I/O 等待时10K 并发代码写法
传统同步 (Tomcat)多线程线程阻塞❌ C10K 困难简单(同步)
Nginx 原生单线程+epoll事件回调✅ 轻松 C100K回调地狱
OpenResty cosocket单线程+协程协程 yield✅ 轻松 C100K简单(同步写法)
Go goroutine多协程+多线程goroutine 挂起✅ C100K简单(同步写法)

100% 非阻塞硬保证:cosocket 在 connect/send/receive 等所有 I/O 操作上强制非阻塞,挂起前设置超时,超时后自动终止——是 C100K 的基石。

9.5.2 连接池与生命周期 ​

lua
local redis = require "resty.redis"
local red = redis:new()

-- 连接池中获取/新建连接 (key: IP:Port 组成的 pool name)
local ok, err = red:connect("127.0.0.1", 6379, {
    pool_size = 100,      -- 池子最大连接数
    backlog = 10,         -- 池满时排队等待数 (-1 无限等待)
})

-- 使用连接...

-- 放回连接池 (而非关闭)
-- 参数: max_idle_timeout(ms), pool_size
local ok, err = red:set_keepalive(60000, 100)
-- 如果在这 60s 内复用,连接保持 TCP 存活,省去握手开销

连接池运作机制:

Worker 内:
  pool("127.0.0.1:6379")
  ┌──────┐  ┌──────┐  ┌──────┐
  │ conn1│  │ conn2│  │ conn3│  ← 最多 pool_size 个空闲连接
  └──────┘  └──────┘  └──────┘

connect(): 池中有空闲 → 取出复用
          池中无空闲 → 新建 TCP 连接 (不超过 pool_size)

set_keepalive(): 放回池中 → 供下次 connect() 复用
                 池满 → 关闭此连接

各 cosocket 库对比:

库用途连接池Pipeline注意事项
lua-resty-redisRedis✅❌ (手动)连接前可能需要 AUTH
lua-resty-mysqlMySQL✅❌注意字符集
lua-resty-httpHTTP 客户端✅❌自动处理 keepalive
lua-resty-dnsDNS 解析✅N/A异步 DNS
ngx.socket.tcp原始 TCP✅N/A最底层,需自行实现协议

9.5.3 常见的错误用法 ​

lua
-- ❌ 忘记 set_keepalive → 连接泄漏, 耗尽上游连接数
local red = redis:new()
red:connect("127.0.0.1", 6379)
-- ... 用完不归还 ...
return  -- 连接不会被放回池中!

-- ✅ 用 pcall 保护, 确保归还
local red = redis:new()
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    return ngx.exit(502)
end
local res, err = red:get("key")
red:set_keepalive(60000, 100)
if err then
    return ngx.exit(500)
end

-- ❌ 协程内阻塞操作 (如 os.execute, 文件 I/O)
content_by_lua_block {
    os.execute("sleep 5")  -- 阻塞整个 Worker!
}

-- ✅ 使用 ngx.timer.at 异步执行

9.6 lua_shared_dict 共享内存 ​

lua_shared_dict 是同一 Nginx 实例内跨 Worker、跨请求共享数据的唯一机制(基于共享内存,非网络通信)。

9.6.1 基本使用 ​

nginx
# nginx.conf — 声明共享内存区域
lua_shared_dict cache 100m;       # 缓存数据
lua_shared_dict locks 10m;        # 分布式锁
lua_shared_dict counters 10m;     # 计数器
lua
-- 读写
local cache = ngx.shared.cache
cache:set("key", "value", 60)     -- 60s 过期
local val, flags = cache:get("key")

-- 原子操作
cache:incr("counter", 1)          -- 原子递增
local val = cache:get("counter")

-- LRU 淘汰 (空间满时自动触发)
cache:add("key", "value", 60)     -- 仅 key 不存在时设置 (原子)
cache:replace("key", "new", 60)   -- 仅 key 存在时替换
cache:safe_set("key", "value", 60) -- 内存不足返回 nil + "no memory" 而非报错

-- 批量操作
cache:get_keys(0)                 -- 获取所有 key (最多返回 1024)
cache:flush_all()                 -- 清空所有 (生产慎用)
cache:flush_expired(10)           -- 清理最多 10 个过期 key

9.6.2 与 Redis 的选择 ​

维度lua_shared_dictRedis
延迟< 1μs (进程内)~0.1-1ms (网络)
持久化❌ 重启丢失✅ RDB/AOF
容量MB 级别 (~100MB)GB 级别
数据结构K-V 简单字典String/List/Set/ZSet/Hash
多实例共享❌ 单实例内✅ 网络访问
适用热缓存、计数器、限流、临时锁持久数据、复杂结构、跨服务

最佳实践:热点数据(如 IP 黑名单、限流计数器)放 lua_shared_dict,持久化数据走 Redis。

9.6.3 实战:跨 Worker 的请求计数器 ​

lua
-- init_worker_by_lua_block (每个 Worker 启动)
local counters = ngx.shared.counters

-- log_by_lua_block (每个请求结束)
local key = ngx.var.server_name .. ":" .. ngx.var.status
counters:incr(key, 1)

-- 定时打印 (ngx.timer)
local function report()
    local keys = counters:get_keys(0)
    for _, k in ipairs(keys) do
        local v = counters:get(k)
        ngx.log(ngx.INFO, k, " = ", v)
    end
    counters:flush_all()
end

9.7 ngx.timer 定时器 ​

ngx.timer 让 OpenResty 能够在不依赖外部 cron 的情况下执行后台任务。

9.7.1 基本用法 ​

lua
-- init_worker_by_lua_block
local function worker_init()
    ngx.log(ngx.INFO, "worker started, pid=", ngx.worker.pid())

    -- 每 10 秒执行一次 (零延迟首次启动)
    local function heartbeat(premature)
        if premature then return end  -- Worker 关闭时 premature=true
        -- 上报心跳 / 清理过期 token / 同步配置
        ngx.log(ngx.INFO, "heartbeat from worker ", ngx.worker.id())
        ngx.timer.at(10, heartbeat)  -- 递归调度
    end
    ngx.timer.at(0, heartbeat)       -- 立即开始首次
end

两个接口的区别:

API延迟适用
ngx.timer.at(delay, callback)延迟 delay 秒后执行定时任务
ngx.timer.every(delay, callback)每 delay 秒执行周期性任务(内部用 at 递归实现)

9.7.2 典型场景 ​

场景1:定期从 Redis/DB 拉取配置

lua
local config_cache = ngx.shared.cache

local function sync_config(premature)
    if premature then return end
    local red = redis:new()
    red:connect("127.0.0.1", 6379)
    local rules = red:get("waf_rules")
    if rules then
        config_cache:set("waf_rules", rules)  -- 写入共享内存
    end
    red:set_keepalive(60000, 100)
    ngx.timer.at(30, sync_config)  -- 30s 轮询
end

init_worker_by_lua_block {
    ngx.timer.at(0, sync_config)
}

场景2:延迟批量上报日志

lua
local log_buffer = {}

log_by_lua_block {
    table.insert(log_buffer, {
        uri = ngx.var.request_uri,
        status = ngx.var.status,
        latency = ngx.now() - ngx.req.start_time(),
    })
}

local function flush_logs(premature)
    if premature then return end
    if #log_buffer > 0 then
        local batch = log_buffer
        log_buffer = {}  -- 清空
        -- 批量上报到 Kafka / HTTP
        local http = require "resty.http"
        local hc = http.new()
        hc:request_uri("http://log-collector/batch", {
            method = "POST",
            body = cjson.encode(batch),
        })
    end
    ngx.timer.at(5, flush_logs)
end

注意事项:

  • 每个 ngx.timer 回调在一个独立的轻协程中运行,与请求协程隔离
  • 回调中可以做 cosocket I/O(连接 Redis/HTTP 等)
  • 避免在回调中执行长时间同步操作(阻塞整个 Worker)
  • Worker 重启时 premature=true,做好清理

9.8 LuaJIT 特性深入 ​

OpenResty 使用 LuaJIT 而非标准 Lua,这是其高性能的关键。

9.8.1 LuaJIT vs 标准 Lua ​

维度标准 Lua 5.1/5.3LuaJIT 2.1
执行方式解释执行 (字节码)JIT 编译 (热点→机器码)
速度基准值10-100× (纯计算)
FFI❌ 需 C 模块绑定✅ 内联 C 声明,直接调用 C 函数
内存限制无硬限制1GB (LuaJIT 2.0) / 可配置
GC标记-清除增量标记-清除
Trace 限制N/A单 trace 不能太复杂(会 trace abort)

9.8.2 FFI:零开销调用 C ​

lua
-- 无需编写 C 绑定, 直接声明 C 函数原型
local ffi = require "ffi"

ffi.cdef[[
    unsigned long long rdtsc(void);          -- 直接调用 C 标准库
    int inet_pton(int af, const char *src, void *dst);
]]

-- 调用 CPU 时间戳计数器 (纳秒级, 无任何封装开销)
local cycles = ffi.C.rdtsc()

-- 调用系统函数
local buf = ffi.new("char[16]")
ffi.C.inet_pton(2, "127.0.0.1", buf)

为什么 FFI 比 Lua C API 快?

传统方式 (lua-cjson):
  Lua → C API (lua_push/lua_get) → C 函数 → C API → Lua
  每次调用有 3-5 次 VM 栈操作开销

FFI 方式:
  Lua → JIT 编译为 → 直接 C 调用 → 返回
  零 VM 开销,与手写 C 几乎同速

FFI 实用案例 — 高性能 IP 匹配:

lua
ffi.cdef[[
    typedef struct { uint32_t ip; int mask; } cidr_t;
    int match_cidr(uint32_t ip, cidr_t *list, int count);
]]

-- 预加载 C 动态库
local lib = ffi.load("ipmatcher")
-- 直接调用 C 函数做批量 CIDR 匹配, 比纯 Lua 快 100×+

9.8.3 JIT 编译与优化 ​

JIT 工作原理:

Lua 源码 → 字节码 → 解释执行 → 热点检测 → Trace → 机器码

  热循环被编译为原生机器码, 后续执行绕开解释器

常见导致 JIT 失败(trace abort)的写法:

lua
-- ❌ NYI (Not Yet Implemented): 某些操作会阻止 JIT
string.gmatch(...)     -- NYI
coroutine.wrap(...)    -- NYI
unpack(...)            -- NYI (部分版本)

-- ❌ 循环内类型不稳定 → trace abort
for i = 1, 100 do
    local x = arr[i]
    if type(x) == "number" then  -- x 可能是 number 或 string
        ...
    end
end

-- ✅ 保持热点路径类型稳定
-- 分离不同类型的处理逻辑, 让 JIT 更容易 trace

查看 JIT 状态:

lua
-- 请求中查看 JIT 统计
local jit = require "jit"
jit.v = true  -- 开启 verbose 模式 (输出 trace 信息到 error.log)

-- 或使用 ngx-lua-jit-stats 模块监控

9.9 WAF / API 网关实战 ​

9.9.1 SQL 注入检测 ​

lua
-- access_by_lua_block
local function detect_sqli(val)
    -- 检测常见 SQL 注入特征
    local patterns = {
        [[(\%27)|(\')|(\-\-)|(\%23)|(#)]],              -- 注释符号
        [[((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(;))]],  -- 条件注入
        [[\w*((\%27)|(\'))((\%6F)|o|(\%4F))((\%72)|r|(\%52))]], -- 'or' 模式
        [[((\%27)|(\'))\s*union]],                        -- union select
    }
    for _, p in ipairs(patterns) do
        if ngx.re.match(val, p, "isjo") then
            return true
        end
    end
    return false
end

-- 检测 GET 参数
local args = ngx.req.get_uri_args()
for k, v in pairs(args) do
    if detect_sqli(tostring(v)) then
        ngx.log(ngx.WARN, "SQLi detected: ", ngx.var.remote_addr,
                " key=", k, " val=", v)
        ngx.exit(403)
    end
end

9.9.2 IP 黑白名单 + 动态更新 ​

nginx
lua_shared_dict ip_blacklist 10m;
lua_shared_dict ip_whitelist 10m;
lua
-- access_by_lua_block
local blacklist = ngx.shared.ip_blacklist
local whitelist = ngx.shared.ip_whitelist
local ip = ngx.var.remote_addr

-- 白名单优先
if whitelist:get(ip) then
    return
end

-- 黑名单拦截
if blacklist:get(ip) then
    ngx.log(ngx.WARN, "blocked IP: ", ip)
    return ngx.exit(403)
end

-- 动态添加黑名单 (通过管理 API)
-- location /admin/block_ip { content_by_lua: blacklist:set(ip, "1", 3600) }

9.9.3 CC 攻击防护(滑动窗口) ​

lua
-- 每 IP 每 location 的滑动窗口限流
local limit = ngx.shared.limit
local ip = ngx.var.binary_remote_addr  -- 用二进制 IP 节省内存
local key = "cc:" .. ip .. ":" .. (ngx.var.host or "") .. ngx.var.request_uri

local window = 10        -- 10 秒窗口
local max_req = 30       -- 窗口内最多 30 次

local current = limit:get(key) or 0
if current >= max_req then
    -- 触发 CC 防护, 加入临时黑名单
    local blacklist = ngx.shared.ip_blacklist
    blacklist:set(ip, "1", 300)  -- 封禁 5 分钟
    ngx.log(ngx.WARN, "CC blocked: ", ngx.var.remote_addr)
    return ngx.exit(429)
end

-- 设置过期 + 原子递增
limit:set(key, current + 1, window)

9.9.4 简易 API 网关:鉴权 + 路由 + 染色 ​

nginx
upstream api_v1 { server 10.0.0.1:8080; }
upstream api_v2 { server 10.0.0.2:8080; }
upstream api_canary { server 10.0.0.99:8080; }

lua_shared_dict gateway_routes 10m;
lua
-- access_by_lua_block: JWT 鉴权 + 路由
local cjson = require "cjson"
local jwt = require "resty.jwt"

-- 1. 解析 JWT
local auth_header = ngx.var.http_authorization
if not auth_header then
    return ngx.exit(401)
end
local token = string.match(auth_header, "Bearer%s+(.+)")
local jwt_obj = jwt:verify("secret_key", token)
if not jwt_obj.verified then
    return ngx.exit(401)
end

-- 2. 根据 JWT claims 染色路由
local user_id = jwt_obj.payload.sub

-- 金丝雀: 特定用户走 canary
if ngx.shared.gateway_routes:get("canary:" .. user_id) then
    ngx.var.backend = "api_canary"
-- AB 测试: user_id % 100 < 10 走 v2
elseif user_id % 100 < 10 then
    ngx.var.backend = "api_v2"
else
    ngx.var.backend = "api_v1"
end

-- 3. 注入染色头到后端
ngx.req.set_header("X-User-Id", tostring(user_id))
ngx.req.set_header("X-Backend", ngx.var.backend)
nginx
# location 块配合
location /api/ {
    set $backend "api_v1";
    access_by_lua_file lua/auth_router.lua;
    proxy_pass http://$backend;
}

连接与性能 ​

参数说明建议值
worker_processesWorker 进程数auto
worker_connections每 worker 最大连接数10240
multi_accept一次 accept 多个连接on
sendfile零拷贝文件传输on
tcp_nopushsendfile 时优化包发送on
tcp_nodelay禁用 Nagle 算法 (实时性)on

代理缓冲 ​

参数说明默认
proxy_buffer_size响应头缓冲区4k|8k
proxy_buffers响应体缓冲区 (数量 大小)8 4k|8k
proxy_busy_buffers_size忙碌时上限8k|16k
proxy_buffering是否启用缓冲on

代理超时 ​

参数说明建议
proxy_connect_timeout连接上游超时3-5s
proxy_send_timeout发送请求超时30-60s
proxy_read_timeout读响应超时30-60s(长连接加大)

客户端缓冲/超时 ​

参数说明
client_body_buffer_size请求体缓冲区 (超出写临时文件)
client_max_body_size请求体上限 (默认 1m)
client_header_buffer_size请求头缓冲区 (默认 1k)
keepalive_timeout长连接保持时间 (默认 75s)

OpenResty ​

参数说明
lua_code_cache生产必须 on,开发可 off
lua_package_pathLua 模块搜索路径
lua_shared_dict共享内存字典 (跨 Worker)
lua_socket_log_errorscosocket 错误日志

十一、常见场景代码片 ​

HTTPS + HTTP 跳转 ​

nginx
# === Nginx 1.25.1+ 推荐写法(http2 独立指令) ===
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;          # http2 不再写在 listen 中
    http2 on;                # 1.25.1+ 独立的 http2 开关
    server_name example.com;

    ssl_certificate /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://backend;
        proxy_set_header X-Forwarded-Proto https;
    }
}

# === Nginx 1.25.0+ HTTP/3 (QUIC) ===
server {
    listen 443 quic reuseport;   # QUIC/HTTP3
    listen 443 ssl;              # TCP fallback
    http2 on;

    ssl_certificate /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # 告知浏览器支持 HTTP/3(Alt-Svc 头)
    add_header Alt-Svc 'h3=":443"; ma=86400';

    location / {
        proxy_pass http://backend;
    }
}

版本说明:listen 443 ssl http2 在 Nginx 1.25.1 起废弃(产生 warning),1.26.0+ 完全移除支持。请使用独立的 http2 on; 指令。HTTP/3(QUIC)从 1.25.0 起作为实验性功能提供,需编译时添加 --with-http_v3_module。


### CORS 跨域

```nginx
location /api/ {
    # 简单处理
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS";
    add_header Access-Control-Allow-Headers "Authorization,Content-Type";

    if ($request_method = OPTIONS) {
        return 204;
    }

    proxy_pass http://backend;
}

限速 (速率限制) ​

nginx
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

location /api/ {
    limit_req zone=api_limit burst=20 nodelay;
    proxy_pass http://backend;
}
# rate=10r/s:  平均每秒 10 请求
# burst=20:    突发最多排队 20 个
# nodelay:     超出 burst 立即 503 (而不是排队)

gzip 压缩 ​

nginx
gzip on;
gzip_min_length 1000;
gzip_types text/plain application/json application/javascript text/css;
gzip_comp_level 6;
gzip_vary on;
gzip_proxied any;

十二、工程实践:Nginx 真正决定上限的是事件循环与缓冲策略 ​

12.0 一眼看懂:Nginx 为什么会越堆越慢 ​

mermaid
flowchart LR
    A["请求进入 Worker"] --> B["上游响应 / Lua 逻辑"]
    B --> C["事件循环吞吐下降"]
    C --> D["连接与缓冲区堆积"]
    D --> E["502 / 504 / RT 上升"]
现象优先看什么常见误判
504 增多upstream RT、超时链路只怪 Nginx 配置
CPU 不高但连接很多Worker 吞吐、慢客户端、慢上游只怪机器空闲却不工作
keepalive 命中低upstream 连接复用、节点数、生命周期只把池子调大
OpenResty 抖动阻塞操作、timer、shared dict只怪 LuaJIT 性能

12.1 Nginx 快,不是因为“转发简单”,而是因为它尽量不把执行单元阻塞住 ​

Nginx 的核心优势不是“配置写起来方便”,而是:

  • 一个 Worker 用事件循环管理大量连接
  • 网络 I/O 尽量非阻塞
  • 请求处理链路尽量短
  • 把慢客户端、慢上游和业务逻辑解耦

所以 Nginx 的性能上限,本质上取决于:

  • Worker 是否被长时间占住
  • 缓冲区是否合理
  • 上游连接是否复用
  • 某些阶段是否引入了阻塞操作

12.2 很多瓶颈不是 CPU 打满,而是事件循环吞吐下降 ​

线上常见现象是:

  • CPU 不是特别高
  • 连接数开始堆积
  • RT 上升
  • 502/504 增多

这通常意味着问题不一定是“算力不够”,而是:

  • Worker 处理单个请求的停留时间变长
  • 同样数量的 Worker,每秒能处理的请求数下降
  • listen 队列、upstream 队列、客户端连接逐层积压
mermaid
flowchart LR
    A["上游响应变慢 / Lua逻辑变重"] --> B["Worker 单请求占用时间变长"]
    B --> C["事件循环吞吐下降"]
    C --> D["连接堆积 / RT 上升"]
    D --> E["502、504、超时增多"]

12.3 proxy_buffering 的本质是“解耦客户端速度和上游速度” ​

proxy_buffering on 的价值并不只是“提高性能”,更重要的是:

  • 上游返回快时,Nginx 尽快把响应收完,释放 upstream 连接
  • 客户端下载慢时,由 Nginx 自己慢慢吐给客户端
  • 避免后端应用线程/连接长期被慢客户端拖住

但它的代价也很明确:

  • 占内存
  • 大响应可能落临时文件
  • buffer 配得太激进会放大单机内存压力

因此它不是简单的开/关问题,而是:你想把背压留在上游,还是留在 Nginx 自己这里。

12.4 upstream keepalive 影响的不只是延迟,还有后端存活能力 ​

如果没有 keepalive 或复用配置不合理,典型后果是:

  • 频繁 TCP 握手
  • 后端连接数上涨
  • TIME_WAIT / 端口消耗增加
  • TLS 握手成本放大

而 keepalive 配太大,也可能带来:

  • 后端空闲连接占满
  • 单个 Nginx 节点囤积过多上游连接
  • 多实例下连接分布失衡

所以 upstream keepalive 的核心不是“越大越好”,而是要和:

  • 后端连接池容量
  • 节点数
  • 请求并发模式
  • 响应时间分布

一起看。

12.5 OpenResty 最大的风险不是 Lua 慢,而是把阻塞带进 Worker ​

OpenResty 真正危险的地方通常不是脚本本身,而是以下行为:

  • 在请求阶段做阻塞文件 I/O
  • 调用外部命令
  • 错误使用第三方库,绕开 cosocket
  • 在 access/rewrite 阶段做过重逻辑
  • timer / shared dict / 后台同步任务设计不当

因为只要 Worker 被阻塞,就不是“这一个请求慢一点”,而是这个 Worker 上所有共享事件循环的连接都受影响。

12.6 一个典型故障链:client -> nginx -> app -> mysql ​

mermaid
sequenceDiagram
    participant C as Client
    participant N as Nginx
    participant A as App
    participant M as MySQL

    C->>N: HTTP 请求
    N->>A: 转发请求
    A->>M: SQL 查询
    Note over M: 慢 SQL / 锁等待 / 刷盘抖动
    M-->>A: 返回变慢
    Note over A: upstream 连接占用时间变长
    A-->>N: 响应变慢
    Note over N: upstream 队列和客户端连接同时堆积
    N-->>C: 504 / RT 飙升

这里用户最后看到的是 Nginx 超时,但根因可能在 MySQL、应用线程池、下游 RPC,甚至更下层的 I/O。

12.7 常见误判 ​

现象容易误判为实际可能是
504 增多Nginx 配置不行上游响应慢
CPU 不高但连接很多机器还很空闲Worker 吞吐下降
worker_connections 快到上限连接数太大慢客户端/慢上游导致连接占用变长
OpenResty RT 抖动LuaJIT 性能不够阻塞操作或外部依赖慢
upstream keepalive 命中低Nginx 没优化好节点太多/连接生命周期太短

12.8 排障顺序 ​

现象优先看什么
502/504 增多access log、error log、upstream RT
连接堆积stub_status、ss -ant, listen 队列
CPU 高是否惊群、正则、压缩、Lua 逻辑过重
CPU 不高但 RT 高upstream、下游数据库、应用线程池
仅 OpenResty 路由慢Lua 阶段逻辑、shared dict、timer 任务

12.9 一个实战原则 ​

text
先把 Nginx 看成“事件循环 + 缓冲器 + 背压隔离层”,
再把它看成“反向代理配置文件”;
这样你更容易看懂线上为什么会慢。

参考 ​

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记