lolly/docs/26-nginx-lua-guide.md
xfy ae8ea1ce0c docs: 添加 nginx 高级主题文档 (Lua/安全/API网关/动态配置)
新增 5 篇深度文档:
- Lua 模块深度指南:OpenResty、ngx_lua、cosocket
- 安全深度指南:WAF、DDoS 防护、OWASP Top 10
- API 网关配置:路由设计、JWT 验证、限流配额
- 动态配置与服务发现:etcd/Consul、nginx-unit

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-03 16:57:22 +08:00

41 KiB
Raw Permalink Blame History

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_luaNGINX 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-redisOpenResty 已内置)

基础操作

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-mysqlOpenResty 已内置)

基础操作

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本地+ L2Redis
缓存预热 系统启动时 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 WAFWeb 应用防火墙)配置

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;
        }
    }
}

8. 参考文档