文章目录
1. 基本概念
1.1 cosocket
cosocket是openresty将其协程(coroutine)与网络套接字结合在一起实现的非阻塞网络I/O。tcp相关API包括:
- 创建对象:
ngx.socket.tcp
- 设置超时:
tcpsock:settimeout
和tcpsock:settimeouts
- 建立连接:
tcpsock:connect
- 发送数据:
tcpsock:send
- 接受数据:
tcpsock:receive
、tcpsock:receiveany
和tcpsock: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的。
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