关于 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 reload 或 kill -HUP masterPID 方式热重载代码,无需重启 Nginx。
(lua代码热加载:http://blog.csdn.net/xiaofei0859/article/details/51076408)
关于 path 和 cpath
OpenResty 会将它的 lib 目录加入 package.path 和 package.cpath,但你的项目目录需要自己处理。
在入口文件中,将项目目录加入 package.path 和 package.cpath 是不可取的。因为 lua_code_cache 开启时,package 模块是同一 worker 上所有请求共享的,如果无条件追加,package.path 和 package.cpath 将不断变长,并最终导致内存溢出。
以下是我采用的解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
local
ok
,
app
=
pcall
(
require
,
"core.app"
)
if
ok
then
app
:
run
(
)
else
local
rootPath
=
ngx
.
var
.
document_root
if
not
(
package
.
path
:
find
(
rootPath
)
)
then
package
.
path
=
package
.
path
.
.
";"
.
.
rootPath
.
.
"/?.lua;;"
end
if
not
(
package
.
cpath
:
find
(
rootPath
)
)
then
package
.
cpath
=
package
.
cpath
.
.
";"
.
.
rootPath
.
.
"/?.so;;"
end
require
(
"core.app"
)
:
run
(
)
end
|
关于 lua-resty-mysql 和 lua-resty-redis
不应使用模块级的局部变量以及模块属性,存放 resty.mysql 和 resty.redis 实例。否则,在 lua_code_cache 开启时,同一 worker 的所有请求将共享该实例,造成数据竞争问题。建议将 resty.mysql 和 resty.redis 实例存放到 ngx.ctx 中。
不能在 require 过程中实例化 resty.mysql 和 resty.redis 实例,否则会报错。例如,模块返回一个 function,此 function 直接或间接调用实例化resty.mysql 和 resty.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 的所有请求共享,没有读写锁,多个请求同时写入时不安全。