cjson 的抢占问题

默认的 cjson 其实是 “共享” 的

Posted by qmsheng on November 28, 2016

我说的 cjson 抢占 问题可不是 lua 的 非抢占 式协程,更准确的理解应该是:由于 lua 的协程切换,可能会导致 cjson 上下文不一致的情况。例如:

-- json.lua
local json = require("cjson")

local _M = {}

function _M.encode(data, empty_table_as_object)
    if json.encode_empty_table_as_object then
        json.encode_empty_table_as_object(empty_table_as_object or false) -- empty table encoded as array default
    end
    ngx.sleep(0.1)
    local ok, data = pcall(json.encode, data)
    if ok then
        return data
    end
end

-- nginx.conf
location /t_arr {
    content_by_lua '
        local json = require "resty.json"
        -- ngx.sleep(0.1)
        ngx.say(json.encode({
            info = "data should be an array",
            data = {}
        }, false))
    ';
}

location /t_obj {
    content_by_lua '
        local json = require "resty.json"
        -- ngx.sleep(0.1)
        ngx.say(json.encode({
            info = "data should be a object",
            data = {}
        }, true))
    ';
}

当这样有并发请求时:

curl http://192.168.3.4/t_arr & \
curl http://192.168.3.4/t_obj
#################################
{"info":"data should be an array","data":{}}
{"info":"data should be a object","data":{}}

这里发生这个问题的关键就是 json.encode 之前, cjson 的上下文会因为 ngx.sleep(0.1) 的介入导致请求 yield ,随之而发生了变化。cjson.safe 其实也会有同样的问题,其仅仅是帮我们把那个 pcall 做了下而已:

The cjson.safe module behaves identically to the cjson module, except when errors are encountered during JSON conversion. On error, the cjson_safe.encode and cjson_safe.decode functions will return nil followed by the error message.

如何解决这个问题?很简单,就是把 ngx.sleep(0.1) 这行去掉即可,另 _M.encode 这个方法不发生请求之间的切换即可。同时要第三方避免对 cjson 模块的滥用,否则依然会有潜在性的安全问题。

同样 cjson 也有个 cjson.new 的方法,可以通过实例化来保证其上下文的绝对安全,但是显然这个是有性能损耗的:

cjson.new can be used to instantiate an independent copy of the Lua CJSON module. The new module has a separate persistent encoding buffer, and default settings. Using a separate cjson module table per preemptive thread (cjson.new)

function _M.encode_new(data, empty_table_as_object)
    local instance = json.new()
    if instance.encode_empty_table_as_object then
        instance.encode_empty_table_as_object(empty_table_as_object or false) -- empty table encoded as array default
    end
    ngx.sleep(0.1)
    local ok, data = pcall(instance.encode, data)
    if ok then
        return data
    end
end
curl http://192.168.3.4/t_arr & \
curl http://192.168.3.4/t_obj
#################################
{"info":"data should be an array","data":[]}
{"info":"data should be a object","data":{}}

最后我的建议是没有必要使用 cjson.new 来让保证 cjson 上下文的绝对安全,我们只需要保证自己封装的模块在执行 cjson.encode 时能保证其操作的是期待的上下文即可。同时项目所有对 json 数据的操作,只通过项目唯一的 _M.encode_M.decode 方法就好。

function _M.encode(data, empty_table_as_object)
    if json.encode_empty_table_as_object then
        json.encode_empty_table_as_object(empty_table_as_object or false) -- empty table encoded as array default
    end
    local ok, data = pcall(json.encode, data)
    if ok then
        return data
    end
end

function _M.decode(data, empty_table_as_object)
    if json.encode_empty_table_as_object then
        json.encode_empty_table_as_object(empty_table_as_object or false) -- empty table encoded as array default
    end
    local ok, data = pcall(json.decode, data)
    if ok then
        return data
    end
end