将 docs/ 根目录下的 nginx 相关文档统一移动到 docs/nginx/ 子目录, 提高文档组织性和可维护性。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
41 KiB
41 KiB
NGINX Lua 模块深度指南
概述
NGINX Lua 模块(ngx_http_lua_module)是 OpenResty 平台的核心组件,它将 Lua 脚本语言嵌入 NGINX,使 NGINX 具备强大的动态脚本能力。本文档深入介绍 Lua 模块的核心概念、API 和最佳实践。
1. OpenResty 简介
1.1 什么是 OpenResty
OpenResty 是一个基于 NGINX 的高性能 Web 平台,由 agentzh(章亦春)创建。它在标准 NGINX 基础上集成了:
- LuaJIT:高性能 Lua 解释器,执行速度接近原生代码
- ngx_lua:NGINX Lua 嵌入模块
- 丰富的 Lua 库:Redis、MySQL、Memcached 等客户端库
- 协程调度器:异步非阻塞 I/O 支持
1.2 OpenResty 与传统 NGINX 的区别
| 特性 | 标准 NGINX | OpenResty |
|---|---|---|
| 脚本能力 | 有限(NJS) | 强大(完整 Lua 支持) |
| 性能 | 高 | 更高(LuaJIT) |
| 生态系统 | 模块扩展 | 丰富的 Lua 库 |
| 学习曲线 | 平缓 | 中等 |
| 典型应用 | 反向代理 | API 网关、WAF、边缘计算 |
1.3 安装 OpenResty
使用包管理器安装
Ubuntu/Debian:
# 添加 OpenResty 仓库
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main"
# 安装
sudo apt-get update
sudo apt-get install -y openresty
CentOS/RHEL:
# 添加仓库
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
# 安装
sudo yum install -y openresty
macOS:
brew install openresty
从源码编译安装
# 下载源码
wget https://openresty.org/download/openresty-1.25.3.1.tar.gz
tar -xzf openresty-1.25.3.1.tar.gz
cd openresty-1.25.3.1
# 配置编译选项
./configure \
--prefix=/usr/local/openresty \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_v3_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-pcre-jit \
--with-luajit
# 编译安装
make -j$(nproc)
sudo make install
# 添加到环境变量
echo 'export PATH=/usr/local/openresty/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
验证安装
# 检查版本
openresty -v
# 输出:nginx version: openresty/1.25.3.1
# 检查 LuaJIT
openresty -V 2>&1 | grep luajit
# 确认包含 --with-luajit
# 启动 OpenResty
sudo openresty
# 测试
curl http://localhost/
2. ngx_lua 核心指令
ngx_lua 提供了多个执行阶段指令,允许在不同请求处理阶段执行 Lua 代码。
2.1 执行阶段概览
请求处理流程:
┌─────────────────────────────────────────┐
│ init_by_lua │ ← NGINX 启动时
├─────────────────────────────────────────┤
│ init_worker_by_lua │ ← 每个 worker 启动时
├─────────────────────────────────────────┤
│ ssl_certificate_by_lua │ ← SSL 证书阶段(可选)
├─────────────────────────────────────────┤
│ set_by_lua │ 设置变量值 │
├─────────────────────────────────────────┤
│ rewrite_by_lua │ URL 重写 │
├─────────────────────────────────────────┤
│ access_by_lua │ 访问控制 │
├─────────────────────────────────────────┤
│ content_by_lua │ 生成响应内容 │
├─────────────────────────────────────────┤
│ header_filter_by_lua │ 处理响应头 │
├─────────────────────────────────────────┤
│ body_filter_by_lua │ 处理响应体 │
├─────────────────────────────────────────┤
│ log_by_lua │ 日志阶段 │
└─────────────────────────────────────────┘
2.2 指令详解
init_by_lua
在 NGINX 启动时执行,用于全局初始化。
http {
# 加载 Lua 模块,预编译代码
init_by_lua_block {
require "cjson"
require "resty.redis"
require "resty.mysql"
-- 预编译正则表达式
local regex = [[\d+]]
local m, err = ngx.re.match("hello 123", regex, "jo")
-- 全局配置
config = {
redis_host = "127.0.0.1",
redis_port = 6379,
cache_ttl = 300
}
-- 打印启动信息
ngx.log(ngx.NOTICE, "OpenResty initialized with LuaJIT")
}
}
适用场景:
- 预加载常用模块
- 初始化全局变量/配置
- 建立数据库连接池
- 编译正则表达式
init_worker_by_lua
在每个 worker 进程启动时执行。
http {
init_worker_by_lua_block {
local delay = 5 -- 5秒间隔
local handler
-- 定时任务:健康检查
handler = function(premature)
if premature then
return
end
-- 执行健康检查
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("http://127.0.0.1:8080/health", {
method = "GET",
timeout = 2000
})
if res and res.status == 200 then
ngx.log(ngx.INFO, "Health check passed")
else
ngx.log(ngx.ERR, "Health check failed: ", err)
end
-- 重新注册定时器
local ok, err = ngx.timer.at(delay, handler)
if not ok then
ngx.log(ngx.ERR, "Failed to create timer: ", err)
end
end
-- 启动定时器
ngx.timer.at(delay, handler)
ngx.log(ngx.NOTICE, "Worker ", ngx.worker.id(), " started")
}
}
适用场景:
- 启动后台定时任务
- worker 级别的初始化
- 定时数据采集
- 缓存预热
set_by_lua
设置 NGINX 变量值。
location /api {
set_by_lua_block $api_backend {
local version = ngx.var.http_x_api_version
if version == "v2" then
return "backend_v2"
else
return "backend_v1"
end
}
proxy_pass http://$api_backend;
}
限制:
- 不能使用阻塞 I/O
- 不能使用
ngx.sleep - 不能使用 cosocket
rewrite_by_lua
在 rewrite 阶段执行,用于 URL 重写和重定向。
location / {
rewrite_by_lua_block {
local uri = ngx.var.uri
local args = ngx.var.args
-- 统一处理尾部斜杠
if uri ~= "/" and uri:sub(-1) == "/" then
uri = uri:sub(1, -2)
end
-- 旧 URL 兼容处理
if uri:match("^/old%-api/") then
local new_uri = uri:gsub("^/old%-api", "/api/v1")
return ngx.redirect(new_uri .. (args and "?" .. args or ""), 301)
end
-- 设置内部变量
ngx.var.target_service = "user_service"
}
proxy_pass http://backend;
}
access_by_lua
在 access 阶段执行,用于访问控制和认证。
location /admin {
access_by_lua_block {
local token = ngx.var.http_authorization
if not token then
ngx.header["WWW-Authenticate"] = "Bearer"
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- JWT 验证
local jwt = require "resty.jwt"
local jwt_obj = jwt:verify("secret_key", token:gsub("Bearer ", ""))
if not jwt_obj.verified then
ngx.log(ngx.ERR, "JWT verification failed: ", jwt_obj.reason)
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 设置用户信息到变量
ngx.var.user_id = jwt_obj.payload.sub
ngx.var.user_role = jwt_obj.payload.role
}
proxy_pass http://admin_backend;
}
content_by_lua
生成响应内容的核心指令。
location /api/status {
content_by_lua_block {
local cjson = require "cjson"
local status = {
nginx_version = ngx.var.nginx_version,
lua_version = _VERSION,
worker_id = ngx.worker.id(),
worker_count = ngx.worker.count(),
time = ngx.time(),
connections = ngx.var.connections_active
}
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode(status))
}
}
header_filter_by_lua
修改响应头。
location / {
proxy_pass http://backend;
header_filter_by_lua_block {
-- 添加安全头部
ngx.header["X-Frame-Options"] = "SAMEORIGIN"
ngx.header["X-XSS-Protection"] = "1; mode=block"
ngx.header["X-Content-Type-Options"] = "nosniff"
-- 移除敏感头部
ngx.header["Server"] = nil
ngx.header["X-Powered-By"] = nil
-- 根据条件设置缓存
if ngx.var.uri:match("^/api/") then
ngx.header["Cache-Control"] = "no-store, no-cache, must-revalidate"
end
}
}
body_filter_by_lua
修改响应体(基于流式处理)。
location / {
proxy_pass http://backend;
body_filter_by_lua_block {
local chunk = ngx.arg[1]
local eof = ngx.arg[2]
-- 累积响应体
if ngx.ctx.body then
ngx.ctx.body = ngx.ctx.body .. chunk
else
ngx.ctx.body = chunk
end
-- 最后一块数据处理
if eof then
local body = ngx.ctx.body
-- 敏感信息脱敏
body = body:gsub("\"phone\":\"(%d%d%d)%d%d%d%d(\d%d%d%d)\"",
"\"phone\":\"%1****%2\"")
body = body:gsub("\"email\":\"([^@]+)@[^\"]+\"",
"\"email\":\"%1@***\"")
ngx.arg[1] = body
else
-- 非最后一块,输出空并标记不完成
ngx.arg[1] = nil
ngx.arg[2] = false
end
}
}
log_by_lua
日志阶段处理。
http {
log_by_lua_block {
local uri = ngx.var.uri
local status = ngx.var.status
local request_time = ngx.var.request_time
-- 慢请求记录
if tonumber(request_time) > 1.0 then
ngx.log(ngx.WARN, "Slow request: ", uri,
" status=", status,
" time=", request_time)
end
-- 统计信息发送到监控
if status >= 500 then
local statsd = require "resty.statsd"
statsd.increment("nginx.error." .. status)
end
}
}
2.3 指令上下文支持
| 指令 | http | server | location | upstream |
|---|---|---|---|---|
init_by_lua |
✓ | ✗ | ✗ | ✗ |
init_worker_by_lua |
✓ | ✗ | ✗ | ✗ |
set_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 |
✗ | ✗ | ✗ | ✓ |
3. Lua 共享字典(ngx.shared.DICT)
共享字典是在所有 worker 进程间共享的内存缓存。
3.1 定义共享字典
http {
# 语法:lua_shared_dict <name> <size>
lua_shared_dict cache 10m; # 通用缓存
lua_shared_dict sessions 5m; # 会话存储
lua_shared_dict rate_limit 1m; # 限流计数
lua_shared_dict locks 1m; # 锁存储
}
内存估算:
- 每个 key-value 对约占用 60-80 字节(小数据)
- 1MB 可存储约 12,000-16,000 个简单键值对
3.2 基本操作
-- 获取字典实例
local cache = ngx.shared.cache
-- 存储数据(支持过期时间)
cache:set("key", "value", 300) -- 300秒后过期
cache:set("key", "value", 0) -- 永不过期
-- 带过期时间的存储
cache:set("session:123", user_data, 1800) -- 30分钟会话
-- 获取数据
local value, flags = cache:get("key")
if value then
ngx.say("Value: ", value)
else
ngx.say("Cache miss")
end
-- 删除数据
cache:delete("key")
-- 原子递增(用于计数器)
local new_val, err = cache:incr("counter", 1, 0) -- 从0开始,每次+1
-- 批量获取
cache:set("user:1", "Alice")
cache:set("user:2", "Bob")
cache:set("user:3", "Charlie")
local keys = {"user:1", "user:2", "user:3"}
local values = cache:get(keys)
3.3 高级操作
local cache = ngx.shared.cache
-- 安全添加(仅当 key 不存在时设置)
local success, err, forcible = cache:add("key", "value", 300)
if not success then
ngx.log(ngx.ERR, "Failed to add: ", err)
end
-- 安全替换(仅当 key 存在时设置)
local success = cache:replace("key", "new_value")
-- 原子操作:如果不存在则设置
local ok = cache:add("lock:process", "1", 10)
if ok then
-- 获取到锁
-- 执行操作
cache:delete("lock:process")
end
-- 获取过期时间
local ttl, err = cache:ttl("key")
if ttl then
ngx.say("TTL: ", ttl, " seconds")
end
-- 获取信息
local info = cache:get_keys(0) -- 0 表示获取所有 keys
ngx.say("Keys count: ", #info)
-- 清空字典(慎用)
cache:flush_all()
-- 过期数据清理
cache:flush_expired(100) -- 清理最多100个过期条目
3.4 应用场景
分布式限流
http {
lua_shared_dict rate_limit 10m;
server {
location /api {
access_by_lua_block {
local limit = ngx.shared.rate_limit
local key = "rate:" .. ngx.var.binary_remote_addr
-- 令牌桶算法实现
local now = ngx.time()
local rate = 10 -- 每秒10个请求
local burst = 20 -- 突发容量
local last = limit:get(key .. ":last")
local tokens = limit:get(key .. ":tokens")
if not last then
tokens = burst
else
local elapsed = now - last
tokens = math.min(burst, tokens + elapsed * rate)
end
if tokens < 1 then
return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
end
tokens = tokens - 1
limit:set(key .. ":tokens", tokens)
limit:set(key .. ":last", now)
}
proxy_pass http://api_backend;
}
}
}
会话存储
http {
lua_shared_dict sessions 20m;
server {
location /login {
content_by_lua_block {
local cjson = require "cjson"
local sessions = ngx.shared.sessions
-- 验证用户名密码
local username = ngx.var.arg_username
local password = ngx.var.arg_password
if not authenticate(username, password) then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 创建会话
local session_id = ngx.md5(ngx.time() .. ngx.var.remote_addr)
local session_data = {
username = username,
login_time = ngx.time(),
ip = ngx.var.remote_addr
}
sessions:set("session:" .. session_id,
cjson.encode(session_data), 3600)
-- 设置 Cookie
ngx.header["Set-Cookie"] = "session=" .. session_id ..
"; Path=/; HttpOnly; Secure"
ngx.say("Login successful")
}
}
location /profile {
access_by_lua_block {
local cjson = require "cjson"
local sessions = ngx.shared.sessions
-- 获取 session
local cookie = ngx.var.cookie_session
if not cookie then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local data = sessions:get("session:" .. cookie)
if not data then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 解析会话数据
local session = cjson.decode(data)
ngx.var.user_name = session.username
}
proxy_pass http://backend;
}
}
}
缓存穿透防护
-- 缓存穿透防护(防止缓存击穿和雪崩)
local function get_with_lock(cache, key, ttl, fetch_func)
-- 1. 尝试从缓存获取
local value = cache:get(key)
if value then
return value
end
local lock_key = "lock:" .. key
local lock_ttl = 10 -- 锁超时时间
-- 2. 尝试获取锁
local ok = cache:add(lock_key, "1", lock_ttl)
if not ok then
-- 3. 未获取到锁,等待后重试
ngx.sleep(0.1)
return cache:get(key)
end
-- 4. 获取到锁,从数据源加载
local ok2, result = pcall(fetch_func)
if ok2 and result then
cache:set(key, result, ttl)
end
-- 5. 释放锁
cache:delete(lock_key)
return result
end
-- 使用示例
local value = get_with_lock(ngx.shared.cache, "user:123", 300, function()
-- 从数据库获取
return fetch_from_db("user", 123)
end)
4. Cosocket API(非阻塞网络 I/O)
Cosocket 是 OpenResty 提供的非阻塞网络 I/O 接口,支持 TCP、UDP 和 Unix Domain Socket。
4.1 TCP Cosocket
-- 创建 TCP socket
local sock = ngx.socket.tcp()
-- 设置超时
sock:settimeout(5000) -- 5秒超时
-- 连接服务器
local ok, err = sock:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Failed to connect: ", err)
return
end
-- 发送数据
local bytes, err = sock:send("PING\r\n")
if not bytes then
ngx.log(ngx.ERR, "Failed to send: ", err)
return
end
-- 接收数据
local line, err = sock:receive("*l") -- 接收一行
if not line then
ngx.log(ngx.ERR, "Failed to receive: ", err)
return
end
ngx.say("Response: ", line)
-- 关闭连接
sock:close()
4.2 高级用法
local sock = ngx.socket.tcp()
-- 连接池复用
sock:setkeepalive(60000, 100) -- 60秒超时,最多100个连接
-- 指定模式接收
local data, err = sock:receive(1024) -- 接收最多1024字节
local data, err = sock:receive("*a") -- 接收所有数据
local data, err = sock:receiveuntil("\r\n") -- 接收直到指定分隔符
-- 批量发送
local ok, err = sock:send({
"GET / HTTP/1.1\r\n",
"Host: example.com\r\n",
"Connection: close\r\n",
"\r\n"
})
4.3 UDP Cosocket
local sock = ngx.socket.udp()
-- 设置超时
sock:settimeout(2000)
-- 设置目标
local ok, err = sock:setpeername("127.0.0.1", 53)
if not ok then
ngx.log(ngx.ERR, "Failed to set peer: ", err)
return
end
-- 发送 DNS 查询
local query = build_dns_query("example.com")
local ok, err = sock:send(query)
-- 接收响应
local data, err = sock:receive(512)
if data then
local result = parse_dns_response(data)
ngx.say("IP: ", result)
end
sock:close()
4.4 异步 HTTP 请求
使用 resty.http 库进行异步 HTTP 请求:
# 安装 lua-resty-http
luarocks install lua-resty-http
local http = require "resty.http"
-- 简单 GET 请求
local httpc = http.new()
local res, err = httpc:request_uri("http://api.example.com/data", {
method = "GET",
headers = {
["Accept"] = "application/json"
}
})
if res then
ngx.status = res.status
ngx.say(res.body)
else
ngx.status = 502
ngx.say("Request failed: ", err)
end
高级 HTTP 请求
local http = require "resty.http"
local httpc = http.new()
-- 配置超时
httpc:set_timeout(5000)
-- 建立连接(连接复用)
local ok, err = httpc:connect("api.example.com", 443)
if not ok then
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
end
-- SSL 握手
local session, err = httpc:ssl_handshake(false, "api.example.com", false)
-- 发送请求
local res, err = httpc:request({
method = "POST",
path = "/v1/users",
headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. token
},
body = [[{"name":"John","email":"john@example.com"}]]
})
if not res then
ngx.log(ngx.ERR, "Request failed: ", err)
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
end
-- 流式读取响应
local reader = res.body_reader
repeat
local chunk, err = reader(8192)
if err then
ngx.log(ngx.ERR, "Read error: ", err)
break
end
if chunk then
ngx.print(chunk)
end
until not chunk
-- 保持连接复用
local ok, err = httpc:set_keepalive(60000, 100)
5. 与 Redis/MySQL 集成
5.1 Redis 集成
# 安装 lua-resty-redis(OpenResty 已内置)
基础操作
local redis = require "resty.redis"
-- 创建连接
local red = redis:new()
red:set_timeout(1000) -- 1秒超时
-- 连接
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Failed to connect: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- 基本操作
red:set("key", "value")
red:set("key", "value", 300) -- 带过期时间
local res, err = red:get("key")
if res == ngx.null then
ngx.say("Key not found")
else
ngx.say("Value: ", res)
end
-- 列表操作
red:lpush("queue", "task1")
red:lpush("queue", "task2")
local task = red:rpop("queue")
-- 哈希操作
red:hset("user:1001", "name", "Alice")
red:hset("user:1001", "age", "30")
local user = red:hgetall("user:1001")
-- 事务
red:multi()
red:incr("counter")
red:lpush("log", "new entry")
local res, err = red:exec()
-- 连接池
local ok, err = red:set_keepalive(60000, 100)
Redis 连接池封装
-- redis_pool.lua
local redis = require "resty.redis"
local _M = {}
function _M.get_connection()
local red = redis:new()
red:set_timeout(1000)
-- 使用 Unix socket(性能更好)
local ok, err = red:connect("unix:/var/run/redis/redis.sock")
if not ok then
-- 回退到 TCP
ok, err = red:connect("127.0.0.1", 6379)
if not ok then
return nil, err
end
end
-- 认证(如有密码)
-- local res, err = red:auth("password")
return red, nil
end
function _M.return_connection(red)
if not red then
return
end
local ok, err = red:set_keepalive(60000, 100)
if not ok then
red:close()
end
end
return _M
缓存模式封装
-- cache.lua
local redis_pool = require "redis_pool"
local cjson = require "cjson"
local _M = {}
function _M.get(key, ttl, fetch_func)
local red, err = redis_pool.get_connection()
if not red then
-- Redis 不可用,直接获取数据
return fetch_func()
end
-- 尝试从 Redis 获取
local data, err = red:get("cache:" .. key)
if data and data ~= ngx.null then
redis_pool.return_connection(red)
return cjson.decode(data)
end
-- 从数据源获取
local result = fetch_func()
if result then
-- 异步写入 Redis
red:set("cache:" .. key, cjson.encode(result), "EX", ttl)
end
redis_pool.return_connection(red)
return result
end
return _M
5.2 MySQL 集成
# 安装 lua-resty-mysql(OpenResty 已内置)
基础操作
local mysql = require "resty.mysql"
-- 创建连接
local db, err = mysql:new()
if not db then
ngx.log(ngx.ERR, "Failed to create mysql: ", err)
return
end
db:set_timeout(1000)
-- 连接数据库
local ok, err, errcode, sqlstate = db:connect({
host = "127.0.0.1",
port = 3306,
database = "test",
user = "root",
password = "password",
charset = "utf8mb4",
max_packet_size = 1024 * 1024,
pool = "mysqlpool" -- 连接池名称
})
if not ok then
ngx.log(ngx.ERR, "Failed to connect: ", err)
return
end
-- 查询
local res, err, errcode, sqlstate = db:query("SELECT * FROM users WHERE id = 1")
if not res then
ngx.log(ngx.ERR, "Query failed: ", err)
return
end
-- 处理结果
for i, row in ipairs(res) do
ngx.say("User: ", row.name, ", Email: ", row.email)
end
-- 插入/更新
local res, err = db:query([[
INSERT INTO users (name, email)
VALUES ('John', 'john@example.com')
]])
if res then
ngx.say("Inserted ID: ", res.insert_id)
end
-- 事务
local res, err = db:query("START TRANSACTION")
local res1, err1 = db:query("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
local res2, err2 = db:query("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if res1 and res2 then
db:query("COMMIT")
else
db:query("ROLLBACK")
end
-- 放回连接池
local ok, err = db:set_keepalive(60000, 100)
MySQL 连接池封装
-- mysql_pool.lua
local mysql = require "resty.mysql"
local cjson = require "cjson"
local _M = {
config = {
host = "127.0.0.1",
port = 3306,
database = "app",
user = "app_user",
password = "app_pass",
charset = "utf8mb4",
max_packet_size = 1024 * 1024,
pool_size = 50
}
}
function _M.query(sql)
local db, err = mysql:new()
if not db then
return nil, err
end
db:set_timeout(3000)
local ok, err = db:connect({
host = _M.config.host,
port = _M.config.port,
database = _M.config.database,
user = _M.config.user,
password = _M.config.password,
charset = _M.config.charset,
max_packet_size = _M.config.max_packet_size,
pool = "mysqlpool"
})
if not ok then
return nil, err
end
local res, err = db:query(sql)
db:set_keepalive(60000, _M.config.pool_size)
if not res then
return nil, err
end
return res, nil
end
-- 参数化查询(防 SQL 注入)
function _M.escape(str)
return ngx.quote_sql_str(str)
end
return _M
5.3 综合示例:用户信息查询
location /api/user {
content_by_lua_block {
local cjson = require "cjson"
local user_id = ngx.var.arg_id
if not user_id or not user_id:match("^%d+$") then
ngx.status = 400
ngx.say("Invalid user ID")
return
end
-- 1. 尝试从 Redis 获取
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if ok then
local data = red:get("user:" .. user_id)
if data and data ~= ngx.null then
ngx.header["Content-Type"] = "application/json"
ngx.header["X-Cache"] = "HIT"
ngx.say(data)
red:set_keepalive(60000, 100)
return
end
red:set_keepalive(60000, 100)
end
-- 2. 从 MySQL 查询
local mysql = require "resty.mysql"
local db = mysql:new()
db:set_timeout(2000)
local ok, err = db:connect({
host = "127.0.0.1",
port = 3306,
database = "users",
user = "readonly",
password = "readonly",
charset = "utf8mb4"
})
if not ok then
ngx.status = 500
ngx.say("Database error")
return
end
local sql = "SELECT id, name, email, created_at FROM users WHERE id = " .. user_id
local res, err = db:query(sql)
db:set_keepalive(60000, 50)
if not res or #res == 0 then
ngx.status = 404
ngx.say("User not found")
return
end
local user = res[1]
local response = cjson.encode(user)
-- 3. 异步写入 Redis(不阻塞响应)
local ok, err = red:connect("127.0.0.1", 6379)
if ok then
red:set("user:" .. user_id, response, "EX", 300) -- 缓存5分钟
red:set_keepalive(60000, 100)
end
ngx.header["Content-Type"] = "application/json"
ngx.header["X-Cache"] = "MISS"
ngx.say(response)
}
}
6. 性能优化技巧
6.1 LuaJIT 优化
-- 1. 使用局部变量缓存全局变量
local ngx = ngx
local cjson = require "cjson"
local http = require "resty.http"
-- 2. 避免在循环中创建函数
local function process_item(item)
return item * 2
end
for i = 1, 1000 do
process_item(i) -- 比内联函数更高效
end
-- 3. 使用 table.new 预分配(OpenResty 扩展)
local new_tab = require "table.new"
local t = new_tab(100, 0) -- 预分配100个数组元素
-- 4. 字符串拼接使用 table.concat
local parts = {}
for i = 1, 100 do
parts[i] = "item" .. i
end
local result = table.concat(parts, ",")
-- 5. 使用 ngx.re 而不是 Lua 正则
-- 高效
local m, err = ngx.re.match(str, [[\d+]], "jo")
-- 低效
local m = str:match("%d+")
-- 6. 避免使用 pairs/ipairs 进行数值索引遍历
-- 高效
for i = 1, #arr do
local v = arr[i]
end
-- 低效
for i, v in ipairs(arr) do
end
6.2 连接池优化
-- 合理设置连接池大小
local pool_size = 100
local keepalive_timeout = 60000 -- 60秒
-- Redis
red:set_keepalive(keepalive_timeout, pool_size)
-- MySQL
db:set_keepalive(keepalive_timeout, pool_size)
-- HTTP
httpc:set_keepalive(keepalive_timeout, pool_size)
6.3 缓存策略
| 策略 | 适用场景 | 实现方式 |
|---|---|---|
| 本地缓存 | 热点数据、配置信息 | ngx.shared.DICT |
| Redis 缓存 | 分布式缓存、会话 | resty.redis |
| 多级缓存 | 高并发读取 | L1(本地)+ L2(Redis) |
| 缓存预热 | 系统启动时 | init_worker_by_lua |
| 缓存穿透防护 | 防止缓存击穿 | 互斥锁 + 空值缓存 |
6.4 Worker 间通信
-- 使用共享字典实现 worker 间通信
http {
lua_shared_dict ipc 1m;
init_worker_by_lua_block {
local ipc = ngx.shared.ipc
-- 订阅消息
local check_message
check_message = function(premature)
if premature then return end
-- 获取消息
local msg = ipc:get("broadcast")
if msg then
-- 处理消息
ngx.log(ngx.INFO, "Worker ", ngx.worker.id(),
" received: ", msg)
ipc:delete("broadcast")
end
ngx.timer.at(0.1, check_message)
end
ngx.timer.at(0.1, check_message)
}
}
6.5 内存管理
-- 1. 及时释放大对象
local large_data = fetch_large_data()
-- 处理数据
process(large_data)
large_data = nil -- 显式释放引用
-- 2. 使用弱引用表(缓存场景)
local weak_cache = setmetatable({}, {
__mode = "v"
})
-- 3. 避免闭包捕获大对象
local function create_handler(config)
-- 只捕获需要的字段
local timeout = config.timeout
return function()
-- 使用 timeout,不持有整个 config
end
end
-- 4. 控制字符串创建
-- 使用 string.sub 而不是正则提取
local prefix = str:sub(1, 10)
7. 完整配置示例
7.1 API 网关配置
user openresty;
worker_processes auto;
error_log /var/log/openresty/error.log warn;
pid /run/openresty.pid;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include /usr/local/openresty/nginx/conf/mime.types;
default_type application/octet-stream;
# Lua 共享字典
lua_shared_dict cache 50m;
lua_shared_dict rate_limit 10m;
lua_shared_dict sessions 20m;
lua_shared_dict locks 5m;
# Lua 库路径
lua_package_path "/usr/local/openresty/site/lualib/?.lua;;";
lua_package_cpath "/usr/local/openresty/site/lualib/?.so;;";
# 全局初始化
init_by_lua_block {
require "cjson"
require "resty.redis"
require "resty.mysql"
-- 全局配置
CONFIG = {
redis = { host = "127.0.0.1", port = 6379 },
mysql = {
host = "127.0.0.1", port = 3306,
database = "api_db",
user = "api_user", password = "api_pass"
},
jwt_secret = "your-secret-key",
rate_limit = { rps = 100, burst = 200 }
}
}
# Worker 初始化
init_worker_by_lua_block {
-- 定期清理过期缓存
local flush_expired
flush_expired = function(premature)
if premature then return end
ngx.shared.cache:flush_expired(100)
ngx.timer.at(30, flush_expired)
end
ngx.timer.at(30, flush_expired)
}
# 日志格式
log_format api_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"'
' cache=$upstream_http_x_cache';
access_log /var/log/openresty/access.log api_log;
# 性能优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# 限流区域
limit_req_zone $binary_remote_addr zone=ip:10m rate=10r/s;
# 上游服务器
upstream api_backend {
server 127.0.0.1:8080 weight=5;
server 127.0.0.1:8081 weight=5;
keepalive 100;
}
# 主服务器
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL 配置
ssl_certificate /etc/ssl/certs/api.crt;
ssl_certificate_key /etc/ssl/private/api.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 安全头部
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 全局访问控制
location / {
# IP 白名单检查
access_by_lua_block {
local whitelist = { ["10.0.0.0/24"] = true }
-- 实现 IP 检查逻辑
}
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 健康检查
location /health {
access_log off;
content_by_lua_block {
local cjson = require "cjson"
local health = {
status = "healthy",
time = ngx.time(),
worker = ngx.worker.id()
}
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode(health))
}
}
# API 路由
location /api/v1/ {
# 限流
limit_req zone=ip burst=20 nodelay;
access_by_lua_block {
-- JWT 认证
local token = ngx.var.http_authorization
if not token then
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 验证 JWT(简化示例)
-- 实际使用 resty.jwt 库
}
header_filter_by_lua_block {
ngx.header["X-API-Version"] = "v1"
}
proxy_pass http://api_backend;
}
# 登录接口
location /api/auth/login {
limit_req zone=ip burst=5 nodelay;
content_by_lua_block {
local cjson = require "cjson"
ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
ngx.status = 400
ngx.say(cjson.encode({ error = "No body" }))
return
end
local args = cjson.decode(data)
-- 验证用户名密码
-- 生成 JWT
ngx.header["Content-Type"] = "application/json"
ngx.say(cjson.encode({
token = "jwt_token_here",
expires_in = 3600
}))
}
}
# 静态资源
location /static/ {
alias /var/www/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
}
7.2 WAF(Web 应用防火墙)配置
http {
lua_shared_dict waf_rules 10m;
lua_shared_dict waf_block 50m;
init_by_lua_block {
-- 加载 WAF 规则
local cjson = require "cjson"
local rules = {
{
id = "1001",
name = "SQL Injection",
pattern = [[(?:union|select|insert|update|delete|drop|create)\s+]],
severity = "high",
action = "block"
},
{
id = "1002",
name = "XSS Attack",
pattern = [[<script[^>]*>[\s\S]*?</script>]],
severity = "high",
action = "block"
},
{
id = "1003",
name = "Path Traversal",
pattern = [[\.\./\.\.]],
severity = "medium",
action = "block"
}
}
-- 存储到共享字典
local waf_rules = ngx.shared.waf_rules
for _, rule in ipairs(rules) do
waf_rules:set(rule.id, cjson.encode(rule))
end
}
server {
listen 80;
server_name protected.example.com;
location / {
access_by_lua_block {
local cjson = require "cjson"
local waf_rules = ngx.shared.waf_rules
local waf_block = ngx.shared.waf_block
local ip = ngx.var.remote_addr
-- 检查 IP 是否被封锁
if waf_block:get("block:" .. ip) then
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- 获取所有规则
local rules = waf_rules:get_keys(0)
local matched = false
local matched_rule = nil
-- 检查请求
local check_string = ngx.var.request_uri .. " " ..
(ngx.var.http_user_agent or "") .. " " ..
(ngx.var.http_cookie or "")
for _, rule_id in ipairs(rules) do
local rule_data = waf_rules:get(rule_id)
if rule_data then
local rule = cjson.decode(rule_data)
local m, err = ngx.re.match(check_string, rule.pattern, "ijo")
if m then
matched = true
matched_rule = rule
break
end
end
end
if matched then
-- 记录攻击
ngx.log(ngx.ERR, "WAF blocked request from ", ip,
" Rule: ", matched_rule.id, " - ", matched_rule.name)
-- 增加计数
local count, err = waf_block:incr("count:" .. ip, 1, 0)
if count and count > 100 then
-- 封锁 IP
waf_block:set("block:" .. ip, "1", 3600)
ngx.log(ngx.ERR, "IP blocked: ", ip)
end
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
proxy_pass http://backend;
}
}
}