关于 OpenResty 的两三事

基础原理

  Nginx 采用的是 master-worker 模型,一个 master 进程管理多个 worker 进程,基本的事件处理都是放在 woker 中,master 负责一些全局初始化,以及对 worker 的管理。
  每个 woker 使用一个 LuaVM,当请求被分配到 woker 时,将在这个 LuaVM 里创建一个 coroutine。协程之间数据隔离,每个协程具有独立的全局变量 _G

关于 lua_code_cache

  关闭 lua_code_cache 时,require 的处理方式是每次都强制重新加载和解析,也就是说,你对代码的任何修改的效果,都将在上传后立即体现。
  开启 lua_code_cache 时,在同一个 LuaVM 中,模块将在首次加载并解析后被缓存,之后再次 require 将直接返回缓存的内容。换句话说,同一 worker 上的所有请求将共享已加载的模块,任意一个请求对于模块属性的修改,都将影响到同一 worker 上的其他请求。
  不应使用模块级的局部变量以及模块属性,存放任何请求级的数据。否则在 lua_code_cache 开启时,会造成请求间相互影响和数据竞争,产生不可预知的异常状况。
  关闭 lua_code_cache 会极大的降低性能,在生产环境中应开启 lua_code_cache
  虽然开发环境中关闭 lua_code_cache 会有一些便利性,但我强烈建议开启 lua_code_cache ,与线上保持一致,以减少不必要的差异性问题和额外测试需求。
  开启 lua_code_cache 时,可用 nginx -s reloadkill -HUP masterPID 方式热重载代码,无需重启 Nginx。

关于 package.pathpackage.cpath

  OpenResty 会将它的 lualib 目录加入 package.pathpackage.cpath,但你的项目目录需要自己处理。
  将项目目录加入 package.pathpackage.cpath 并不是一个好主意。若将项目目录加入 package.pathpackage.cpath,在 nginx 存在多个 server 时,由于 lua_code_cache 的存在,同名文件的缓存会互相冲突,导致 require 可能不能返回正确的文件。
  除非你能确定你的 nginx 上永远只配置一个 server,否则请勿将项目目录加入 package.pathpackage.cpath。创建一个基于 require 函数的新函数用于专门加载项目文件,是个不错的注意,也很容易实现。

关于 lua-resty-mysqllua-resty-redis

  不应使用模块级的局部变量以及模块属性,存放 resty.mysqlresty.redis 实例。否则,在 lua_code_cache 开启时,同一 worker 的所有请求将共享该实例,造成数据竞争问题。建议将 resty.mysqlresty.redis 实例存放到 ngx.ctx 中。
  不能在 require 过程中实例化 resty.mysqlresty.redis 实例,否则会报错。例如,模块返回一个 function,此 function 直接或间接调用实例化 resty.mysqlresty.redis 的代码,将会导致报错。
  在首次查询时实例化是一个比较好的解决方案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
local mysql = require("resty.mysql")
local exception = require("core.exception")
local dbConf = require("config.mysql")
local sysConf = require("config.system")

local MySQL = {}

--- 获取连接
--
-- @return resty.mysql MySQL连接
-- @error mysql.socketFailed socket建立失败
-- @error mysql.cantConnect 无法连接数据库
-- @error mysql.queryFailed 数据查询失败
function MySQL:getClient()
    if ngx.ctx[MySQL] then
        return ngx.ctx[MySQL]
    end

    local client, errmsg = mysql:new()

    if not client then
        exception:raise("mysql.socketFailed", { message = errmsg })
    end

    client:set_timeout(3000)

    local options = {
        user = dbConf.USER,
        password = dbConf.PASSWORD,
        database = dbConf.DATABASE
    }

    if dbConf.SOCK then
        options.path = dbConf.SOCK
    else
        options.host = dbConf.HOST
        options.port = dbConf.PORT
    end

    local result, errmsg, errno, sqlstate = client:connect(options)

    if not result then
        exception:raise("mysql.cantConnect", {
            message = errmsg,
            code = errno,
            state = sqlstate
        })
    end

    local query = "SET NAMES " .. sysConf.DEFAULT_CHARSET
    local result, errmsg, errno, sqlstate = client:query(query)

    if not result then
        exception:raise("mysql.queryFailed", {
            query = query,
            message = errmsg,
            code = errno,
            state = sqlstate
        })
    end

    ngx.ctx[MySQL] = client
    return ngx.ctx[MySQL]
end

--- 关闭连接
function MySQL:close()
    if ngx.ctx[MySQL] then
        ngx.ctx[MySQL]:set_keepalive(0, 100)
        ngx.ctx[MySQL] = nil
    end
end

--- 执行查询
--
-- 有结果数据集时返回结果数据集
-- 无数据数据集时返回查询影响,如:
-- { insert_id = 0, server_status = 2, warning_count = 1, affected_rows = 32, message = nil}
--
-- @param string query 查询语句
-- @return table 查询结果
-- @error mysql.queryFailed 查询失败
function MySQL:query(query)
    local result, errmsg, errno, sqlstate = self:getClient():query(query)

    if not result then
        exception:raise("mysql.queryFailed", {
            query = query,
            message = errmsg,
            code = errno,
            state = sqlstate
        })
    end

    return result
end

return MySQL

  使用 set_keepalive(max_idle_timeout, pool_size) 替代 close() 将启用连接池特性。set_keepalive 的意思可以理解为,保持连接,并将连接归还到连接池内。这样在下次连接时,会首先会尝试从连接池获取连接,获取不成功才会创建新的连接。在高并发下,连接池能大大的减少连接 MySQL 和 Redis 的次数,明显的提升性能。

使用模块缓存静态数据

  利用 lua_code_cache 开启时模块会被缓存的特性,我们可以使用模块来缓存静态数据,其效率接近于将数据缓存在内存中。

  存储方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
local exception = require("core.exception")
local mysql = require("core.driver.mysql")

--- 实现示例,可以根据项目情况,完善后封装在数据查询层
local function makeCityCache()
    local citys = mysql:query("SELECT * FROM `data_city` WHERE 1")
    local cityData = {}

    for _, city in ipairs(citys) do
        cityData[city.id] = city
    end

    package.loaded["cache.city"] = cityData
end

  读取方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--- 实现示例,可以根据项目情况,完善后封装在数据查询层
local function getCityCache(id)
    local ok, cacheData = pcall(require, "cache.city")

    if ok then
        return cacheData[id]
    end

    return nil
end

  清理方法:

1
2
3
4
--- 实现示例,可以根据项目情况,完善后封装在数据查询层
local function clearCityCache()
    package.loaded["cache.city"] = nil
end

数据存储

_G

  请求级 table 变量,生命周期为本次请求,可存储请求级任意 Lua 数据。

ngx.ctx

  请求级 table 变量,生命周期为本次请求,可存储请求级任意 Lua 数据。

ngx.shared.DICT

  全局级 key-value 字典,使用共享内存实现,实现了读写锁,所有请求均可安全读写。 value 只能为布尔值、数字和字符串。Reload Nginx 时不会受影响,只有当 Nginx 被关闭时才会丢失。

模块属性和模块级局部变量

  worker 级变量,同一 worker 的所有请求共享,没有读写锁,多个请求同时写入时不安全。