一、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虽然快,但用不好反而会成为性能杀手。这里有三个黄金法则:
- 避免在热路径上创建新table
- 字符串拼接用table.concat代替..
- 慎用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。核心优化点包括:
- 使用lua-resty-lrucache替代共享字典
- 用FFI实现AES加密
- 优化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))
}
}
四、避坑指南
最后分享几个血泪教训:
- 不要滥用ngx.location.capture:每个子请求都会创建新协程
- 慎用阻塞式IO:比如用Lua文件操作会拖垮整个worker
- 注意正则表达式:复杂的正则可能消耗大量CPU
- 监控是关键:必须用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代码,每个环节都可能成为瓶颈。建议先用火焰图定位热点,再针对性地优化,千万别一上来就盲目调参。
评论