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-2565.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/API | MySQL/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_rewrite | rewrite_by_lua (server 块) | 全局URL重写 |
| ④ rewrite | rewrite_by_lua (location 块) | URI重写、参数校验、重定向 |
| ⑦ access | access_by_lua | 鉴权、限流、WAF、IP黑白名单 |
| ⑩ content | content_by_lua | 生成响应内容(与 proxy_pass 互斥) |
| ⑩ (body filter) | body_filter_by_lua | 修改响应体(每chunk触发一次) |
| ⑩ (header filter) | header_filter_by_lua | 修改响应头 |
| ⑪ log | log_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_lua | Master 启动时(只执行一次) | 加载全局模块、预编译正则 |
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-redis | Redis | ✅ | ❌ (手动) | 连接前可能需要 AUTH |
lua-resty-mysql | MySQL | ✅ | ❌ | 注意字符集 |
lua-resty-http | HTTP 客户端 | ✅ | ❌ | 自动处理 keepalive |
lua-resty-dns | DNS 解析 | ✅ | 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 个过期 key9.6.2 与 Redis 的选择
| 维度 | lua_shared_dict | Redis |
|---|---|---|
| 延迟 | < 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()
end9.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.3 | LuaJIT 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
end9.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_processes | Worker 进程数 | auto |
worker_connections | 每 worker 最大连接数 | 10240 |
multi_accept | 一次 accept 多个连接 | on |
sendfile | 零拷贝文件传输 | on |
tcp_nopush | sendfile 时优化包发送 | 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_path | Lua 模块搜索路径 |
lua_shared_dict | 共享内存字典 (跨 Worker) |
lua_socket_log_errors | cosocket 错误日志 |
十一、常见场景代码片
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 看成“事件循环 + 缓冲器 + 背压隔离层”,
再把它看成“反向代理配置文件”;
这样你更容易看懂线上为什么会慢。
登录后即可发表评论 👇