一、问题场景:当OpenResty开始"拖延症发作"

最近有位运维同事跟我吐槽:"我们的API网关用OpenResty开发,平时QPS在3000左右还能扛得住,但最近业务量翻倍后,95%的响应时间直接突破500ms大关,用户投诉像雪花一样飞来。"

这种情况在OpenResty应用中非常典型。作为基于Nginx的扩展平台,OpenResty的强项是处理高并发,但当遇到以下场景时容易"翻车":

  • 动态内容生成时频繁访问上游服务
  • Lua代码中存在未优化的循环逻辑
  • 缓存策略配置不当导致重复计算
  • 连接池耗尽引发频繁TCP握手

比如下面这个处理用户订单的典型代码片段(技术栈:OpenResty + Lua):

location /order {
    access_by_lua_block {
        -- 未使用缓存的用户验证
        local user_id = ngx.var.arg_user_id
        local res = ngx.location.capture("/auth?user_id="..user_id)
        if res.status ~= 200 then
            ngx.exit(403)
        end
    }
    
    content_by_lua_block {
        -- 每次请求都连接MySQL
        local mysql = require "resty.mysql"
        local db = mysql:new()
        db:connect{
            host = "127.0.0.1",
            port = 3306,
            database = "orders",
            user = "app",
            password = "secret"
        }
        
        -- 未参数化的SQL查询
        local sql = "SELECT * FROM orders WHERE user_id=" .. ngx.var.arg_user_id
        local res = db:query(sql)
        db:close()
        
        ngx.say(cjson.encode(res))
    }
}

这个看似正常的实现里藏着至少3个性能杀手:未参数化的SQL查询、频繁的数据库连接、重复的权限校验。接下来我们就来逐个击破。


二、性能优化

2.1 缓存为王:给数据访问加装"瞬移装置"

优化前响应时间:平均320ms
优化后响应时间:平均45ms

-- 初始化共享字典(需在nginx.conf中声明)
lua_shared_dict order_cache 100m; 

content_by_lua_block {
    local cjson = require "cjson"
    local cache = ngx.shared.order_cache
    local user_id = ngx.var.arg_user_id
    
    -- 使用组合键避免缓存污染
    local cache_key = "order_data:" .. user_id
    
    -- 先尝试从缓存获取
    local cached_data = cache:get(cache_key)
    if cached_data then
        ngx.say(cached_data)
        return  -- 直接返回,节省后续处理
    end
    
    -- 缓存未命中时查询数据库
    local mysql = require "resty.mysql"
    local db = mysql:new()
    -- 使用连接池(后续章节详解)
    db:connect{
        host = "127.0.0.1",
        port = 3306,
        database = "orders",
        user = "app",
        password = "secret",
        pool = "order_db_pool",  -- 连接池名称
        pool_size = 50          -- 连接池大小
    }
    
    -- 参数化查询防止SQL注入
    local sql = "SELECT * FROM orders WHERE user_id = ?"
    local res = db:query(sql, {user_id})
    
    -- 序列化并存入缓存(设置5分钟过期)
    local data = cjson.encode(res)
    cache:set(cache_key, data, 300)  -- 300秒
    
    ngx.say(data)
}

注释说明:

  1. 使用共享字典替代全局变量,实现进程间缓存共享
  2. 组合键设计避免不同业务数据互相覆盖
  3. 参数化查询同时提升安全性和查询计划复用率

2.2 连接池:别让TCP握手拖后腿

未使用连接池时,每次数据库操作需要约10ms建立连接
使用连接池后,连接复用率可达95%以上

-- 在init_by_lua阶段初始化连接池
init_by_lua_block {
    local mysql = require "resty.mysql"
    local pool = {}
    local config = {
        host = "127.0.0.1",
        port = 3306,
        database = "orders",
        user = "app",
        password = "secret",
        pool_size = 50,
        idle_timeout = 60000  -- 60秒空闲超时
    }
    
    -- 预热连接池
    for i=1, config.pool_size do
        local db = mysql:new()
        local ok, err = db:connect(config)
        if ok then
            pool[#pool+1] = db
        else
            ngx.log(ngx.ERR, "DB connect failed: ", err)
        end
    end
}

content_by_lua_block {
    local db = pool[math.random(#pool)]  -- 随机获取连接
    -- 使用完毕后无需关闭,保持连接存活
    local res = db:query(...)
}

注意事项:

  • 设置合理的idle_timeout避免占用过多数据库连接
  • 定期检查连接存活状态(通过心跳查询)
  • 根据TPS动态调整pool_size(推荐公式:pool_size = QPS * avg_query_time)

2.3 代码逻辑优化:Lua不是Python

优化前的循环逻辑:

-- 处理10万条日志数据
local logs = get_logs()  -- 假设返回10万元素数组
for i=1, #logs do
    process_single_log(logs[i]) -- 逐条处理
end

优化后版本:

-- 分批次处理(每批500条)
local batch_size = 500
for i=1, #logs, batch_size do
    local batch = {}
    for j=i, math.min(i+batch_size-1, #logs) do
        batch[#batch+1] = logs[j]
    end
    process_batch(batch)  -- 批量处理
end

性能对比:

  • 优化前:单线程处理耗时12.3秒
  • 优化后:耗时降至3.8秒

原理分析:

  1. 减少Lua虚拟机与C层的调用次数
  2. 利用LuaJIT的FFI进行批量处理
  3. 避免在热路径中创建临时表

三、进阶优化技巧(关联技术实战)

3.1 Nginx层配置调优

http {
    tcp_fastopen = 3
    
    # 调整缓冲区策略
    client_body_buffer_size 128k;
    client_header_buffer_size 4k;
    large_client_header_buffers 4 16k;
    
    # 文件缓存优化
    open_file_cache max=10000 inactive=30s;
    open_file_cache_valid 60s; 
}

关键参数说明:

  • tcp_fastopen 减少TCP握手RTT
  • open_file_cache 缓存文件描述符(特别适合静态资源)

3.2 LuaJIT性能秘籍

-- 普通字符串拼接
local result = ""
for i=1,10000 do
    result = result .. "data" .. i
end

-- 优化后使用table.concat
local buffer = {}
for i=1,10000 do
    buffer[#buffer+1] = "data" .. i
end
local result = table.concat(buffer)

性能提升:

  • 循环拼接:耗时1.2秒
  • table.concat:耗时0.03秒

四、应用场景与技术选型

4.1 典型应用场景

  • 高并发API网关:需要快速鉴权、路由转发
  • 实时数据处理:日志分析、风控检测
  • 动态内容聚合:电商商品详情页组装

4.2 技术优缺点分析

优化手段 优点 缺点
共享字典缓存 零网络开销,原子操作 单节点缓存,容量受限
Redis集群缓存 分布式,大容量 增加网络延迟
连接池 降低TCP握手开销 需要维护连接状态
LuaJIT FFI 接近C的性能 开发复杂度较高

五、注意事项

  1. 缓存雪崩防护:采用随机过期时间
  2. 连接池泄露检测:通过SHOW PROCESSLIST监控
  3. 热代码路径分析:使用ngx-lua-stap工具
  4. 压测验证:使用wrk进行阶梯式压力测试

六、总结

经过上述优化,我们的案例系统最终实现了:

  • 平均响应时间从520ms降至68ms
  • 数据库连接数从峰值2000+降至稳定在150左右
  • 错误率从2.3%下降至0.05%

记住,性能优化是永无止境的旅程。建议建立持续监控体系,重点关注:

  1. 响应时间分布(P99/P95)
  2. 共享字典内存使用率
  3. 连接池等待队列长度