问题根源:
目前,ngx_http_lua_shdict.c中封装了两类shdict操作接口,一类是lua c接口类型的,另一类是ffi类型。
现以shdict取一个key为string,value亦为string的操作为例(假定能够命中)。
#lua c接口类型的 shdict api
ngx_shmtx_lock(&ctx->shpool->mutex);
rc = ngx_http_lua_shdict_lookup(zone, hash, key.data, key.len, &sd);
if(value_type == LUA_TSTRING){
lua_pushlstring(L, (char *) value.data, value.len);
ngx_shmtx_unlock(&ctx->shpool->mutex);
}
在unlock之前调用了lua_pushlstring,这个函数会将当前的cpu执行权交给luajit vm,这就意味着luajit vm可以接着执行任何它想执行的任务,例如collectgarbage垃圾回收(典型的协程思想)。
假如在这次垃圾回收中,正好有一个lua对象要被回收了,而它的gc函数中定义了一些shdict的操作(这里不管是ffi类型的还是lua c类型的api),都一定会产生死锁,它的过程是:
一个worker的luajit vm垃圾回收调用了shdict访问操作,接着这个shdict访问操作去获取一把它已经获得的锁,于是,自己死锁。
接着,大多数情况下,其他worker的逻辑中也会存在shdict访问(因为shdict本身是用来进行跨worker进程间通信的),于是在接下来一个确定的时间内(取决于程序的固定参数,一般都比较小,秒级别),其他的所有worker一样陷入死锁状态,因为这把锁的拥有者在阻塞地获取这把它已经抢到的锁。
可见,因为此处lua_pushlstring的调用,导致lua c接口类型的shdict api存在死锁隐患,因为从上层来看,这个操作并不是原子的(很多朋友都会认为lua层面的一次shdict操作是原子的,然而,如果用的不是ffi shdict操作,这个编程模型就是错误的,所以会有这个BUG存在)。
#ffi接口类型的 shdict api
ngx_shmtx_lock(&ctx->shpool->mutex);
rc = ngx_http_lua_shdict_lookup(zone, hash, key.data, key.len, &sd);
if(value_type == LUA_TSTRING){
# copy value str to usr's buffer
ngx_shmtx_unlock(&ctx->shpool->mutex);
# return buffer address to user
}
(这种ffi的方法将会产生两次内存copy:value->user_buf->ffi_string)
在luajit vm中看,这个操作是原子的,因为luajit vm无法中断它去执行其它的代码,于是这里就没有死锁条件。
回看lua c接口类型shdict api的设计实现,我想当初春哥设计时也一定想过使用两次内存copy来保证api是原子的,但是因为性能和简洁性的权衡,最终选择了直接使用lua_pushlstring进行值传递的方法。
解法:
方法1.使用lua-resty-core,将默认的shdict访问接口一开始就改写为ffi类型的,通过:
init_by_lua '
require"resty.core"
';
方法2.一种绝对约束:在lua c类型的shdict api实现中,多加一次内存copy,将这些api的执行逻辑原子化,或者在lua_pushlstring调用前后对vm collectgarbage进行关开。
方法3.最灵活且兼容性强的方法,在lua层,每次调用lua c类型的shdict接口前后对collectgarbage进行关开(效率不用担心,它只是标志位的设置,消耗非常的低),即:
collectgarbage("stop")
--any dict operation
collectgarbage("restart")
总结:
BUG描述:
凡是使用lua c类型shdict接口的,并且在lua对象的__gc方法中进行了lua shdict操作的,就有一定概率发生worker死锁。
一个BUG子集:
凡是使用了lua-resty-lock库并且没有事先使用lua-resty-core进行api替换的Openresty服务,都会有概率死锁。
lua-resty-lock是个非常重要的库,基本上Openresty上所有稍微复杂点的应用都会用到这个基础库,而lua-resty-core大家对它的认识可能仅限于一个ffi接口的封装,并不会说一定会强迫自己使用它对ngx api在init_by_lua阶段进行替换。
例如,我们小组线上的Openresty虽然有安装lua-resty-core库,但是没有人使用它(当然,从今天以后就会不同了),在这么多服务中,也许现在就有一个正在处于死锁状态。
所以,我认为目前线上大部分Openresty应用都可能会有这个隐患,尽管它的出现概率很小,也可以通过检测机制去除这一毛刺,但是肯定会对用户体验产生不利的影响。
使用lua-resty-core ☺
init_by_lua '
require"resty.core"
';
在 2016年9月25日星期日 UTC+8下午5:43:22,Yun Thanatos写道:
本周五对一个由若干个Openresty实例组成的分布式服务进行压力测试,发现基本上大约每隔两个小时,Master Nginx的多个worker一定会死锁,Slaves Nginx偶尔死锁,其死锁频率比Master低很多。
连续追踪两天,最终今天下午在春哥的帮助下定位出问题,先感谢春哥的耐心帮助 ☺
BUG介绍:
Openresty提供两套shdict访问接口,一个是lua层面的,一个是ffi接口。
Bug就是,凡是使用lua层面shdict接口的,并且在lua对象的__gc方法中进行了lua shdict操作的,就有一定概率发生worker死锁(如果所有的workers都会访问shdict,那他们最终全部都会死锁)。
更具体化一点的例子就是:
凡是使用了lua shdict操作的并且使用了lua-resty-lock库的所有Openresty服务,都会有概率死锁。
BUG重现:
一般情况下,压力测试一分钟之内,所有的worker都会死锁(前提是没有使用lua-resty-core)。
-------nginx.conf
worker_processes 1; #
worker_rlimit_core 10000m; # for producing core file
lua_shared_dict test_lock_flaw 10m;
location /flaw{
content_by_lua '
local flaw = require"flaw"
flaw.test_lock_flaw()
';
}
location /is_alive{
content_by_lua '
ngx.say("not dead lock yet :)")
';
}
------- 测试脚本
#
while true
do
ab -c 10 -n 100000 127.0.0.1/flaw
sleep 1
done
-------
while true
do
ab -c 10 -n 100000 127.0.0.1/is_alive
sleep 1
done
------- flaw.lua
-- yunth...@gmail.com 25-Sep-16
--
local _M = { _VERSION = '1.2' }
-- debug flag
local ddd_flag = false
local lib_ffi = require'ffi'
local lib_lock = require"resty.lock"
local lib_C = lib_ffi.C
local ffi = lib_ffi
local function anotnil(x)
assert(type(x)~="nil")
end
anotnil(lib_ffi)
anotnil(lib_lock)
local function traceback_str()
if ddd_flag == true then
local striped_body,_=string.gsub(debug.traceback(),"","")
return(tostring(striped_body))
else
return nil
end
return
--local striped_body,_=string.gsub(debug.traceback(),"","")
--return(tostring(striped_body))
end
local function notnil(x)
return type(x)~="nil"
end
local function isnil(x)
return type(x)=="nil"
end
local function typet(t)
return type(t)=="table"
end
local function types(s)
return type(s)=="string"
end
local function typen(n)
return type(n)=="number"
end
local function typef(f)
return type(f) == "function"
end
local function typecdata(c)
return type(c) == "cdata"
end
local function anotnil(x)
local str = traceback_str()
assert(type(x)~="nil",str)
end
local function anil(x)
local str = traceback_str()
assert(type(x)=="nil",str)
end
local function atypet(t)
local str = traceback_str()
assert(type(t)=="table",str)
end
local function atypes(s)
local str = traceback_str()
assert(type(s)=="string",str)
end
local function atypen(n)
local str = traceback_str()
assert(type(n)=="number",str)
end
local function atypef(f)
local str = traceback_str()
assert(type(f)=="function",str)
end
local function acdata(p)
local str = traceback_str()
assert(type(p)=="cdata",str)
end
local function acdata_notnil(p)
local str = traceback_str()
assert(type(p)=="cdata",str)
assert(p ~= nil,str)
end
local function csizeof(str)
atypes(str)
local sz = ffi.sizeof(str)
atypen(sz)
return sz
end
local function ccast(ptr,str)
--assert(type(ptr) == "cdata")
--assert(ptr ~= nil)
atypes(str)
local p = ffi.cast(str,ptr)
--assert(type(p) == "cdata")
--assert(p ~= nil)
return p
end
atypet(ngx)
local function shd_set_safe(shd_name_str,member_name_str,value)
--return true or nil,err_str
local dic=ngx.shared[shd_name_str]
local success,err=dic:safe_set(member_name_str,value)
if type(success)~="boolean" or success~=true then
return nil,tostring(err)
else
return success
end
end
local function lock_try(shd_name,lock_name,expire) -- return object lock or nil
-- new a try lock
-- auto expire:20s
if type(expire)~="number" then
expire=20
end
assert(type(lock_name)=="string")
assert(#lock_name>0)
atypes(shd_name)
local lock = lib_lock:new(shd_name,
{["exptime"]=expire,["timeout"]=0,["step"]=0.001,["ratio"]=2,["max_step"]=0.5})
-- try_lock
local elapsed, err = lock:lock(lock_name)
if type(elapsed) ~= "nil" then
-- got the lock :)
-- some stuff :)
return lock
-- release the lock
-- lock:unlock()
end
return nil
end
local function lock_release(lock)
lock:unlock()
end
local function shd_set_safe_with_assert(shd,member,value)
assert(type(value)~="nil")
assert(type(shd)=="string")
assert(#shd>0)
assert(type(member)=="string")
assert(#member>0)
local ret=shd_set_safe(shd,member,value)
assert(ret==true)
return ret
end
function _M.test_lock_flaw_prerequisite(pf)
local ffi_new = ffi.new
if not typef(pf) then
pf = ngx.say
end
local function gc_fp(cdata)
acdata_notnil(cdata)
-- do gc stuff...
pf("gc_fp:"..tostring(cdata))
end
local ctype = ffi.metatype(
"struct {int key;}",
{ __gc = gc_fp }
)
anotnil(ctype)
pf("type(ctype):"..type(ctype))
cdata1 = ffi_new(ctype)
acdata_notnil(cdata1)
cdata1.key = 1
--
cdata1 = nil
pf("collectgarbage:(are you see some gc outputs?...☺")
collectgarbage()
pf("exit,bye :)")
end
function _M.test_lock_flaw(pf)
local ffi_new = ffi.new
if not typef(pf) then
pf = ngx.say
end
local function xpf(str)
atypes(str)
pf(str)
if pf == ngx.say then
ngx.flush()
end
end
xpf("enter:")
local shd_name = "test_lock_flaw"
local lock_ct = 0
local lock_t = {}
local dic = ngx.shared[shd_name]
shd_set_safe_with_assert(
shd_name,
"key:"..tostring(key_ct),
string.rep("v:"..tostring(key_ct),1)
)
while lock_ct < (10000000) do
local lock = lock_try(shd_name,"lock_name"..tostring(math.random()),1)
lock = nil
dic:get("key:0")
lock_ct = lock_ct + 1
end
local key_ct = 0
shd_set_safe_with_assert(
shd_name,
"key:"..tostring(key_ct),
string.rep("v:"..tostring(key_ct),1)
)
local dic = ngx.shared[shd_name]
local loop_ct = 0
while loop_ct < (math.random(1,10000)*100) do
loop_ct = loop_ct + 1
dic:get("key:0")
lock_t = nil
end
xpf("exit,bye :)")
end
function _M.test_lock_flaw_rand(pf)
local ffi_new = ffi.new
if not typef(pf) then
pf = ngx.say
end
local function xpf(str)
atypes(str)
pf(str)
if pf == ngx.say then
ngx.flush()
end
end
xpf("enter:")
local shd_name = "test_lock_flaw"
local lock_ct = 0
local lock_t = {}
local pid = ngx.pid()
while lock_ct < 10000 do
lock_t[lock_ct] = lock_try(shd_name,"lock_name:"..tostring(lock_ct)..":"..tostring(pid)..":"..tostring(math.random()),1)
anotnil(lock_t[lock_ct])
lock_ct = lock_ct + 1
end
local key_ct = 0
shd_set_safe_with_assert(
shd_name,
"key:"..tostring(key_ct),
string.rep("v:"..tostring(key_ct),1000)
)
local v = shd_get_safe_with_assert(
shd_name,
"key:"..tostring(key_ct)
)
assert(v == string.rep("v:"..tostring(key_ct),1000))
local dic = ngx.shared[shd_name]
local loop_ct = 0
while loop_ct < 20000000 do
loop_ct = loop_ct + 1
dic:get("key:0")
lock_t = nil
end
xpf("exit,bye :)")
end
return _M