一、OpenResty协程的本质与优势

OpenResty的核心在于将Lua协程与Nginx事件驱动模型完美结合。想象一下,你正在快餐店点餐——传统多线程就像开多个收银台,每个顾客独占一个柜台;而协程更像是同一个收银员同时处理多个顾客的订单,在等待炸鸡的间隙去接可乐订单。

-- 技术栈:OpenResty + LuaJIT
location /order {
    content_by_lua_block {
        -- 模拟处理订单流程
        local function make_food(food)
            ngx.sleep(1)  -- 模拟耗时操作
            return food.." ready"
        end

        -- 启动两个协程并行处理
        local co1 = ngx.thread.spawn(make_food, "burger")
        local co2 = ngx.thread.spawn(make_food, "coke")
        
        -- 等待所有协程完成
        local ok, res1, res2 = ngx.thread.wait(co1, co2)
        ngx.say(res1, ", ", res2)  -- 输出: burger ready, coke ready
    }
}

协程的轻量级特性(仅需2KB内存)使得单进程可轻松处理数万并发连接,避免了线程切换的开销。但要注意:协程是协作式调度,长时间占用CPU会导致"饿死"其他协程。

二、协程间通信的雷区与解决方案

共享变量是协程编程中最危险的陷阱之一。就像多个厨师共用一把菜刀,如果不加控制就会发生切伤事故:

location /counter {
    content_by_lua_block {
        local counter = 0  -- 危险的共享变量
        
        local function add()
            for i = 1, 1000 do
                counter = counter + 1  -- 这里会出现竞态条件
            end
        end

        local co1 = ngx.thread.spawn(add)
        local co2 = ngx.thread.spawn(add)
        ngx.thread.wait(co1, co2)
        
        ngx.say("Expected 2000, got ", counter)  -- 结果通常小于2000
    }
}

正确做法是使用ngx.shared.DICT共享内存区,它采用原子操作:

location /safe-counter {
    content_by_lua_block {
        local shared = ngx.shared.counter_dict
        shared:set("count", 0)
        
        local function add()
            for i = 1, 1000 do
                shared:incr("count", 1)  -- 原子操作
            end
        end

        ngx.thread.wait(
            ngx.thread.spawn(add),
            ngx.thread.spawn(add)
        )
        
        ngx.say("Correct result: ", shared:get("count"))  -- 稳定输出2000
    }
}

三、资源泄漏的隐形杀手

协程泄漏比内存泄漏更隐蔽。看这个典型错误示例:

location /leak {
    content_by_lua_block {
        local redis = require "resty.redis"
        
        local function query()
            local red = redis:new()
            local ok, err = red:connect("127.0.0.1", 6379)
            if not ok then
                ngx.log(ngx.ERR, err)
                return  -- 错误退出时未关闭连接!
            end
            
            -- 模拟业务处理
            ngx.sleep(0.1)
            red:close()  -- 正常情况会关闭
        end

        -- 模拟高并发请求
        for i = 1, 1000 do
            ngx.thread.spawn(query)  -- 部分协程出错会导致连接泄漏
        end
    }
}

防御性编程的正确姿势:

location /safe-query {
    content_by_lua_block {
        local function query()
            local red = require("resty.redis"):new()
            
            -- 使用pcall捕获异常
            local ok, err = pcall(function()
                red:connect("127.0.0.1", 6379)
                -- 业务代码...
                ngx.sleep(math.random())  -- 模拟业务处理
            end)
            
            -- 确保资源释放
            if red and not red:get_reused_times() then
                red:close()
            end
            
            if not ok then ngx.log(ngx.ERR, err) end
        end

        -- 使用协程池控制并发量
        local threads = {}
        for i = 1, 100 do  -- 控制最大并发数
            threads[i] = ngx.thread.spawn(query)
        end
        ngx.thread.wait(unpack(threads))
    end
}

四、死锁的预防与调试技巧

协程死锁通常发生在多层嵌套调用时。假设有个订单支付场景:

location /payment {
    content_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        red:connect("127.0.0.1", 6379)
        
        local function deduct_balance(user_id, amount)
            local balance = red:get("balance:"..user_id)
            -- 模拟耗时验证
            ngx.sleep(0.1)  
            return red:incrby("balance:"..user_id, -amount)
        end

        local function create_order(user_id)
            -- 先扣款
            deduct_balance(user_id, 100)
            -- 再创建订单(假设也需要访问Redis)
            red:hset("orders", ngx.now(), user_id)  -- 这里可能阻塞!
        end

        ngx.thread.spawn(create_order, "user_123")
    }
}

问题分析

  1. 当Redis连接池耗尽时,hset会等待可用连接
  2. 但所有连接都被卡在deduct_balance的sleep中
  3. 形成循环等待

解决方案

location /safe-payment {
    content_by_lua_block {
        local function new_redis()
            local red = require("resty.redis"):new()
            assert(red:connect("127.0.0.1", 6379))
            return red
        end

        local function transact(user_id)
            -- 每个协程使用独立连接
            local red = new_redis()
            local red2 = new_redis()  -- 读写分离
            
            pcall(function()
                -- 扣款使用第一个连接
                red2:incrby("balance:"..user_id, -100)
                -- 创建订单用第二个连接
                red:hset("orders", ngx.now(), user_id)
            end)
            
            red:close()
            red2:close()
        end

        ngx.thread.spawn(transact, "user_123")
    end
}

五、性能优化实战策略

  1. 协程粒度控制
-- 不好的实践:为每个小任务创建协程
local threads = {}
for i = 1, 10000 do
    threads[i] = ngx.thread.spawn(function()
        red:get("key_"..i) 
    end)
end

-- 好的实践:批量处理
local function batch_get(keys)
    local red = new_redis()
    for i, key in ipairs(keys) do
        red:get(key)
    end
    -- 使用pipeline提升性能
    local results = red:commit_pipeline()
    red:close()
    return results
end
  1. 超时机制必不可少
location /api {
    content_by_lua_block {
        local http = require "resty.http"
        local httpc = http.new()
        
        -- 设置全局超时
        httpc:set_timeouts(500, 500, 500)  -- 连接/发送/读取
        
        -- 单个请求也可以单独设置
        local ok, err = ngx.thread.spawn(function()
            httpc:request_uri("http://backend", {
                timeout = 300  -- 毫秒
            })
        end)
        
        if not ok then
            ngx.log(ngx.ERR, "请求超时: ", err)
            httpc:close()
        end
    end
}

六、最佳实践总结

  1. 资源管理三原则

    • 谁创建谁释放
    • 异常分支必须清理
    • 使用连接池替代频繁创建
  2. 并发控制要点

    -- 使用信号量控制并发度
    local semaphore = require "ngx.semaphore"
    local sem = semaphore.new(10)  -- 最大10个并发
    
    local function limited_task()
        sem:wait(1)  -- 获取信号量
        -- 执行业务...
        sem:post(1)  -- 释放
    end
    
  3. 调试建议

    • 使用ngx.worker.count()监控协程数量
    • 通过ngx.log(ngx.WARN, debug.traceback(co))输出协程堆栈
    • 定期检查ngx.ctx是否残留数据

记住:协程不是银弹,在CPU密集型场景下反而会降低性能。合理利用才能发挥OpenResty的最大威力。