一、OpenResty性能优化到底难在哪?

说实话,第一次接触OpenResty性能优化的时候,我也被它复杂的架构搞得头大。这玩意儿本质上是个Nginx加Lua的缝合怪,性能瓶颈可能出现在网络层、Lua虚拟机、甚至C模块里。最常见的问题就是Lua代码跑着跑着突然卡住,CPU占用率直接飙升到100%,但你就是不知道到底哪行代码在搞事情。

我遇到过最典型的一个案例是:某电商平台的商品详情页接口,在促销活动时响应时间从50ms暴涨到2秒。用火焰图一分析,发现70%的时间都耗在一个不起眼的JSON解析操作上。下面这个简化版的示例代码,完美复现了当时的场景:

-- 技术栈:OpenResty + LuaJIT
-- 问题代码:频繁解析大型JSON
local cjson = require "cjson"

location /api/product {
    content_by_lua_block {
        -- 从Redis获取商品数据(假设这是个1MB的大JSON)
        local redis = require "resty.redis"
        local red = redis:new()
        red:connect("127.0.0.1", 6379)
        local json_str = red:get("product:123")
        
        -- 每次请求都解析整个JSON(性能黑洞!)
        local data = cjson.decode(json_str)
        
        -- 其实只需要其中几个字段...
        ngx.say(data.name)
    }
}

看到问题了吗?每次请求都把1MB的JSON整个解析一遍,但其实前端只需要商品名称这个字段。这就好比你要吃个苹果,结果非要把整个水果店都搬回家。

二、性能优化的七种武器

2.1 缓存的艺术

先说最简单的解决方案——缓存。OpenResty的缓存体系分三个层次:

-- 技术栈:OpenResty + Lua + Redis
location /api/product {
    content_by_lua_block {
        -- 第一层:Nginx共享字典缓存
        local cache = ngx.shared.product_cache
        local name = cache:get("product:123:name")
        
        if name then
            ngx.say(name)
            return
        end
        
        -- 第二层:Redis缓存
        local redis = require "resty.redis"
        local red = redis:new()
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "failed to connect Redis: ", err)
            ngx.exit(500)
        end
        
        name = red:hget("product:123", "name")
        
        -- 第三层:回源到数据库
        if name == ngx.null then
            local mysql = require "resty.mysql"
            local db = mysql:new()
            db:connect{
                host = "127.0.0.1",
                database = "shop",
                user = "app",
                password = "secret"
            }
            
            local res = db:query("SELECT name FROM products WHERE id=123")
            name = res[1].name
            
            -- 回填缓存
            red:hset("product:123", "name", name)
            cache:set("product:123:name", name, 60) -- 缓存60秒
        end
        
        ngx.say(name)
    }
}

这个三级缓存策略,把原本需要2秒的请求优化到了0.5毫秒。不过要注意缓存雪崩问题——所有缓存同时失效时,流量会直接压垮数据库。解决方法很简单,给缓存过期时间加个随机数:

cache:set("product:123:name", name, 60 + math.random(10))

2.2 LuaJIT的调优技巧

LuaJIT虽然快,但用不好反而会成为性能杀手。这里有三个黄金法则:

  1. 避免在热路径上创建新table
  2. 字符串拼接用table.concat代替..
  3. 慎用FFI调用

看个实际案例:

-- 技术栈:LuaJIT
-- 错误示范:频繁创建新table
local function format_user(user)
    -- 每次调用都新建table(GC压力大)
    return {
        name = user.name,
        id = user.id,
        age = user.age
    }
end

-- 正确做法:复用table
local user_pool = {}
local function format_user_optimized(user)
    local cached = user_pool[user.id] or {}
    cached.name = user.name
    cached.id = user.id
    cached.age = user.age
    return cached
end

再来看字符串拼接的优化:

-- 慢:字符串拼接
local html = ""
for _, item in ipairs(items) do
    html = html .. "<li>" .. item .. "</li>"
end

-- 快:table.concat
local parts = {}
for _, item in ipairs(items) do
    parts[#parts + 1] = "<li>"
    parts[#parts + 1] = item
    parts[#parts + 1] = "</li>"
end
local html = table.concat(parts)

2.3 异步非阻塞编程

OpenResty最强大的特性就是协程机制。对比下同步和异步的写法差异:

-- 技术栈:OpenResty
-- 同步写法(阻塞!)
location /sync {
    content_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        red:connect("127.0.0.1", 6379) -- 阻塞点
        local res = red:get("key")     -- 阻塞点
        ngx.say(res)
    }
}

-- 异步写法
location /async {
    content_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        
        -- 非阻塞连接
        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(500)
        end
        
        -- 设置超时
        red:set_timeouts(100, 100, 100) -- 连接/发送/读取超时
        
        -- 异步查询
        local res, err = red:get("key")
        if not res then
            ngx.log(ngx.ERR, "failed to get key: ", err)
            return ngx.exit(404)
        end
        
        ngx.say(res)
    }
}

2.4 内存管理黑科技

OpenResty的内存管理有套独特的机制。先看个内存泄漏的典型案例:

-- 内存泄漏陷阱
local big_table = {}

location /leak {
    content_by_lua_block {
        -- 每次请求都往全局table添加数据
        big_table[#big_table + 1] = ngx.time()
        ngx.say("count: ", #big_table)
    }
}

解决方法是用ngx.ctx替代全局变量:

location /safe {
    content_by_lua_block {
        ngx.ctx.big_table = ngx.ctx.big_table or {}
        table.insert(ngx.ctx.big_table, ngx.time())
        ngx.say("count: ", #ngx.ctx.big_table)
    }
}

对于大型数据结构,还可以用FFI直接分配C内存:

local ffi = require "ffi"
ffi.cdef[[
    void* malloc(size_t size);
    void free(void* ptr);
]]

local buf = ffi.C.malloc(1024)
-- ...使用buf...
ffi.C.free(buf)

三、实战:百万QPS的网关优化

去年我们给某支付网关做性能优化,最终实现了单机百万QPS。核心优化点包括:

  1. 使用lua-resty-lrucache替代共享字典
  2. 用FFI实现AES加密
  3. 优化HTTP头处理

这是关键代码片段:

-- 技术栈:OpenResty + LuaJIT
local lrucache = require "resty.lrucache"
local cache = lrucache.new(10000) -- 缓存10000个元素

location /pay {
    content_by_lua_block {
        -- 缓存命中检查
        local key = ngx.var.arg_order_id
        local cached = cache:get(key)
        if cached then
            ngx.say(cached)
            return
        end
        
        -- 加密处理(FFI优化版)
        local ffi = require "ffi"
        ffi.cdef[[
            int aes_encrypt(const char* input, char* output);
        ]]
        local input = "payment data"
        local output = ffi.new("char[?]", #input * 2)
        ffi.C.aes_encrypt(input, output)
        
        -- 响应头优化
        ngx.header["X-Accel-Expires"] = 60
        ngx.header["Connection"] = "keep-alive"
        
        cache:set(key, ffi.string(output), 60)
        ngx.say(ffi.string(output))
    }
}

四、避坑指南

最后分享几个血泪教训:

  1. 不要滥用ngx.location.capture:每个子请求都会创建新协程
  2. 慎用阻塞式IO:比如用Lua文件操作会拖垮整个worker
  3. 注意正则表达式:复杂的正则可能消耗大量CPU
  4. 监控是关键:必须用openresty-stap工具做实时分析

这里有个正则优化的例子:

-- 危险的正则
local m = ngx.re.match("user=123&name=张三", [[name=([^&]+)]], "jo")

-- 更安全的写法
local m = ngx.re.match("user=123&name=张三", [[name=(\w+)]], "jo")

记住,OpenResty性能优化是个系统工程。从网络配置到Lua代码,每个环节都可能成为瓶颈。建议先用火焰图定位热点,再针对性地优化,千万别一上来就盲目调参。