openresty出现socket read/write busy的原因及解决方法

1. 基本概念

1.1 cosocket

cosocket是openresty将其协程(coroutine)与网络套接字结合在一起实现的非阻塞网络I/O。tcp相关API包括:

  • 创建对象:ngx.socket.tcp
  • 设置超时:tcpsock:settimeouttcpsock:settimeouts
  • 建立连接:tcpsock:connect
  • 发送数据:tcpsock:send
  • 接受数据:tcpsock:receivetcpsock:receiveanytcpsock:receiveuntil
  • 连接池:tcpsock:setkeepalive
  • 关闭连接:tcpsock:close

需要特别关注的API是setkeepalive(timeout, size),用于将tcp连接放入连接池,timeout设置超时时间,size设置连接池大小。其特点包括:

  • 每个worker进程会有一个连接池。
  • 连接池通过ip和port标志,若ip和port相同则使用同一个连接池。
  • 连接池大小在第一次调用setkeepalive时确定,不会再变更。
  • 调用setkeepalive后即将tcp连接放入连接池中,无需再调用close方法。
  • 每次调用connect时会优先查询连接池中是否有可用连接,若有则直接返回连接,若无则新建连接。
1.2 ngx.ctx

ngx.ctx用于存储请求的上下文环境数据,其生命周期与当前请求的生命周期相同。若当前请求中有子请求存在,子请求会维护自己的ngx.ctx,当前请求不能与子请求共享ngx.ctx。注意使用ngx.ctx时尽量使用传参的方式,ngx.ctx表查询效率略低。

local _M = {}

-- 错误
-- ctx变量是在 Lua 模块级别,并且属于单个worker的(worker级变量,同一worker的所有请求共享,多个请求同时写入时不安全)
-- local ctx = ngx.ctx
-- function _M.main()
--     ctx.foo = "bar"
-- end

-- 正确,通过传参方式完成
function _M.main(ctx)
    ctx.foo = "bar"
end

return _M

2. 错误原因及解决方法

cosocket是全双工的,如果一个读线程和一个写线程同时操作一个cosocket对象不会有问题,但如果两个线程一起读或写同一个cosocket则会引发socket busy错误。

2.1 作为tcp client的解决方法

读线程不会出现socket read busy,因为读取tcp数据的地方有且仅有一个。使用单独的协程来操作socket,其他业务处理协程使用消息队列来通知需要发送的tcp数据。

2.2 连接mongodb、redis时的问题

本质上两者实现相同,都是基于cosocket。mongodb经常报错而redis不报错的原因是:redis在每一个调用函数内部建立连接且名字都不相同(保证在同一请求中即使有多个协程ngx.ctx返回的也不是同一个连接)。

导致出错的代码:

-- 会导致出错的代码
-- 若同一请求中有多个协程,它们共享一个上下文环境(ngx.ctx表),并且会对name相同的连接进行读写操作,大概率会出现socket busy的问题
if name and ngx.ctx[name] then
    return ngx.ctx[name]
end

local client, errmsg = mongol:new()
if not client then
    return nil, 'mongo socket failed: '..(errmsg or 'no errMsg')
end

client:set_timeout(timeout)

-- 若上下文中没有找到相同名字的连接,则从连接池中获取socket,若连接池中也没有则新建tcp连接
local result, errmsg = client:connect(host, port)
if not result then
    return nil, errmsg
end

ngx.ctx[name] = client

mongodb数据库认证也需要对数据库进行读取,增加了报错的概率。对于并发的请求,并发的tcp连接不会报错的原因是:它们每一个连接的ngx.ctx表是分开维护的,所以仅当一个请求或者tcp连接中的子协程的socket操作冲突了才会报错。

2.3 redis使用注意点

在使用redis中select命令切换数据库时,要注意在使用完后,将数据库切换为数据库0,否则在执行完set_keepalive后将连接到其他数据库的tcp长连接放入连接池中,有概率导致其他业务从连接池中取出的连接并不是连接到数据库0的。

更多lua-resty-redis使用说明

3. 解决方法

如果需要在共享模块内操作redis和mongodb,例如common.lua,需要在调用时传递ngx.ctx所需的名字。

local function c(name)
    -- 不加name会报错
    local cc_config_db = require("models.config_mongo"):new(name)
    
    local config = cc_config_db:get_one({name = "SYSTEM_CONFIG"})
    if not config or not next(config) then
        ngx.log(ngx.ERR, name .. " failed")
    else 
        ngx.log(ngx.ERR, name .. " success")
    end
end

local function a()
    while true do
        c("a")
        ngx.sleep(0.1)
    end
end

local function b()
    while true do
        c("b")
        ngx.sleep(0.1)
    end
end

local function test()
    ngx.thread.spawn(a)
    ngx.thread.spawn(b)
end
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值