一、OpenResty性能问题的常见表现

我们先来看看OpenResty在什么情况下会出现性能问题。最常见的就是高并发时响应变慢,CPU占用率飙升,甚至出现请求超时的情况。比如下面这个典型的Nginx错误日志:

2023/03/15 10:23:45 [error] 12345#0: *56789 upstream timed out (110: Connection timed out) while reading response header from upstream

这种情况往往发生在Lua脚本处理时间过长,或者后端服务响应不及时的时候。我见过一个电商网站在大促时,因为商品详情页的Lua脚本里有复杂的计算逻辑,导致整个OpenResty实例的CPU直接飙到100%。

二、性能调优的核心思路

调优的核心思路其实很简单:找到瓶颈,然后解决它。但难就难在如何准确找到瓶颈点。我总结了一个"三板斧"方法:

  1. 先用工具测量(比如wrk、ab、或者OpenResty自用的工具)
  2. 分析测量结果(看是CPU瓶颈、内存瓶颈还是I/O瓶颈)
  3. 针对性优化(改代码、调配置或者加缓存)

举个实际的例子,我们有个API服务原来QPS只能到2000,经过下面这样的优化后提升到了8000:

-- 优化前的代码
location /api {
    content_by_lua_block {
        local res = ngx.location.capture("/internal-process")
        ngx.say(res.body)
    }
}

-- 优化后的代码
location /api {
    content_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        
        -- 先查Redis缓存
        local cached = red:get("cache_key")
        if cached then
            ngx.say(cached)
            return
        end
        
        -- 缓存没有才走内部处理
        local res = ngx.location.capture("/internal-process")
        red:set("cache_key", res.body, "EX", 60)  -- 缓存60秒
        ngx.say(res.body)
    }
}

这个例子展示了最典型的优化手段:加缓存。但要注意,缓存虽好,也要考虑缓存一致性问题。

三、关键配置参数调优

OpenResty的性能很大程度上取决于Nginx的配置参数。下面这些参数特别关键:

  1. worker_processes:应该设置为CPU核心数
  2. worker_connections:每个worker能处理的连接数
  3. keepalive_timeout:长连接保持时间
  4. lua_code_cache:开发时可以关,生产环境必须开

这里有个完整的优化配置示例:

worker_processes auto;  # 自动根据CPU核心数设置
worker_rlimit_nofile 102400;  # 文件描述符限制

events {
    worker_connections 4096;  # 每个worker的连接数
    use epoll;  # Linux下性能最好的事件模型
}

http {
    lua_code_cache on;  # 必须开启代码缓存
    lua_shared_dict my_cache 128m;  # 共享内存缓存
    
    keepalive_timeout 65;  # 长连接超时
    keepalive_requests 10000;  # 单个长连接最大请求数
    
    # 其他优化配置...
}

特别提醒:lua_code_cache这个参数,我见过太多人在生产环境忘记打开了,结果性能差得离谱。

四、Lua代码层面的优化技巧

Lua代码的写法对性能影响巨大。这里分享几个实战经验:

  1. 避免在热路径上创建临时表
  2. 多用local变量
  3. 谨慎使用字符串拼接
  4. 合理使用ngx.timer.at做异步处理

看个实际的例子:

-- 不好的写法
function process_request()
    local headers = ngx.req.get_headers()  -- 每次调用都创建新表
    -- 处理逻辑...
end

-- 好的写法
local header_mt = { __index = function(t, k) return ngx.req.get_headers()[k] end }
function process_request()
    local headers = setmetatable({}, header_mt)  -- 轻量级的代理表
    -- 处理逻辑...
end

再来看个字符串处理的例子:

-- 低效的字符串拼接
local result = ""
for i = 1, 10000 do
    result = result .. "data" .. i  -- 每次拼接都创建新字符串
end

-- 高效的写法
local t = {}
for i = 1, 10000 do
    t[#t+1] = "data" .. i
end
local result = table.concat(t)  -- 一次性拼接

五、缓存策略的优化

缓存用得好,性能提升立竿见影。OpenResty提供了多种缓存机制:

  1. 共享字典(lua_shared_dict)
  2. LRU缓存(lua-resty-lrucache)
  3. 外部缓存(Redis/Memcached)

这里重点说说共享字典的使用技巧:

local shared_cache = ngx.shared.my_cache

-- 基础用法
shared_cache:set("key", "value", 60)  -- 缓存60秒
local value = shared_cache:get("key")

-- 高级用法:防止缓存击穿
local function get_data(key)
    local value = shared_cache:get(key)
    if value then return value end
    
    -- 使用锁防止缓存击穿
    local lock_key = "lock:" .. key
    if shared_cache:add(lock_key, true, 5) then  -- 获取锁
        value = fetch_data_from_db(key)  -- 从数据库获取
        shared_cache:set(key, value, 60)
        shared_cache:delete(lock_key)  -- 释放锁
    else
        -- 没拿到锁,短暂等待后重试
        ngx.sleep(0.1)
        return get_data(key)
    end
    return value
end

六、连接池的正确使用

数据库和Redis连接池的配置对性能影响很大。看个Redis连接池的示例:

local redis = require "resty.redis"
local red = redis:new()

-- 设置连接超时和连接池大小
red:set_timeouts(100, 600, 200)  -- 连接/发送/读取超时(毫秒)

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.say("failed to connect: ", err)
    return
end

-- 使用完毕后放回连接池
local ok, err = red:set_keepalive(10000, 100)  -- 连接池大小100,超时10秒
if not ok then
    ngx.say("failed to set keepalive: ", err)
    return
end

常见错误是连接池设置过小,导致频繁创建新连接;或者设置过大,浪费内存。

七、实战案例分析

最后分享一个真实案例。某金融公司风控系统,原QPS只有500左右,经过以下优化提升到3000+:

  1. 用ngx.balancer实现动态负载均衡
  2. 对频繁访问的风控规则加了二级缓存(内存+Redis)
  3. 优化了Lua代码中的JSON处理(改用cjson.safe)
  4. 调整了worker_processes和worker_connections

关键优化代码片段:

-- 优化后的JSON处理
local cjson = require "cjson.safe"
local rule_cache = ngx.shared.rule_cache

local function get_rule(rule_id)
    -- 先从内存缓存查
    local rule = rule_cache:get(rule_id)
    if rule then
        return cjson.decode(rule)
    end
    
    -- 内存没有查Redis
    local redis = require "resty.redis"
    local red = redis:new()
    rule = red:get("rule:" .. rule_id)
    if rule then
        rule_cache:set(rule_id, rule, 60)  -- 缓存到内存
        return cjson.decode(rule)
    end
    
    -- 都没有才查数据库
    rule = query_rule_from_db(rule_id)
    local rule_json = cjson.encode(rule)
    red:set("rule:" .. rule_id, rule_json, "EX", 3600)  -- Redis缓存1小时
    rule_cache:set(rule_id, rule_json, 60)  -- 内存缓存1分钟
    return rule
end

八、总结与建议

OpenResty性能调优是个系统工程,我的建议是:

  1. 先测量,再优化,不要凭感觉
  2. 从大到小:先调架构,再调配置,最后调代码
  3. 缓存是银弹,但要处理好一致性问题
  4. 连接池大小要适中,太大太小都不好
  5. Lua代码要避免常见性能陷阱

记住,没有放之四海而皆准的最优配置,一定要根据实际业务场景和负载特点来调整。