lua 模块开发
在实际开发中,不可能把所有代码写到一个大而全的lua文件中,需要进行分模块开发;而且模块化是高性能 Lua 应用的关键。使用 require 第一次导入模块后,所有 Nginx 进程全局共享模块的数据和代码,每个 Worker 进程需要时会得到此模块的一个副本(Copy-On-Write),即模块可以认为是每 Worker 进程共享而不是每 Nginx Server 共享;另外注意之前我们使用 init_by_lua中初始化的全局变量是每请求复制一个;如果想在多个 Worker 进程间共享数据可以使用 ngx.shared.DICT 或如 Redis 之类的存储。
在/usr/example/lualib 中已经提供了大量第三方开发库如 cjson、redis客户端、mysql 客户端:
cjson.so
resty/
aes.lua
core.lua
dns/
lock.lua
lrucache/
lrucache.lua
md5.lua
memcached.lua
mysql.lua
random.lua
redis.lua
……
需要注意在使用前需要将库在 nginx.conf 中导入:
#lua 模块路径,其中”;;”表示默认搜索路径,默认到/usr/servers/nginx 下找
lua_package_path "/usr/example/lualib/?.lua;;"; #lua 模块
lua_package_cpath "/usr/example/lualib/?.so;;"; #c 模块
使用方式是在 lua 中通过如下方式引入
local cjson = require(“cjson”)
local redis = require(“resty.redis”)
接下来我们来开发一个简单的lua 模块。
vim /usr/example/lualib/module1.lua
local count = 0
local function hello()
count = count + 1
ngx.say("count : ", count)
end
local _M = {
hello = hello
}
return _M
开发时将所有数据做成局部变量/局部函数;通过 _M 导出要暴露的函数,实现模块化封装。
接下来创建 test_module_1.lua.
vim /usr/example/lua/test_module_1.lua
local module1 = require("module1")
module1.hello()
使用 local var = require(“模块名”),该模块会到 lua_package_path 和 lua_package_cpath 声明的的位置查找我们的模块,对于多级目录的使用 require(“目录 1.目录 2.模块名”)加载。
example.conf 配置
location /lua_module_1 {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/example/lua/test_module_1.lua;
}
访问如http://192.168.1.2/lua_module_1进行测试,会得到类似如下的数据,count 会递增
count : 1
count :2
……
count :N
此时可能发现 count 一直递增,假设我们的 worker_processes 2,我们可以通过 kill -9 nginx worker process 杀死其中一个 Worker 进程得到 count 数据变化。
假设我们创建了 vim /usr/example/lualib/test/module2.lua 模块,可以通过 local module2 = require(“test.module2”)加载模块
基本的模块开发就完成了,如果是只读数据可以通过模块中声明 local 变量存储;如果想在每 Worker 进程共享,请考虑竞争;如果要在多个 Worker 进程间共享请考虑使用 ngx.shared.DICT 或如 Redis 存储。
常见的 lua 开发库
对于开发来说需要有好的生态开发库来辅助我们快速开发,而 Lua 中也有大多数我们需要的第三方开发库如 Redis、Memcached、Mysql、Http 客户端、JSON、模板引擎等。
一些常见的 Lua 库可以在 github 上搜索,https://github.com/search?utf8=%E2%9C%93&q=lua+resty。
Redis 客户端
lua-resty-redis是为基于 cosocket API 的 ngx_lua 提供的 Lua redis客户端,通过它可以完成 Redis 的操作。默认安装 OpenResty 时已经自带了该模块,使用文档可参考https://github.com/openresty/lua-resty-redis。
在测试之前请启动 Redis 实例:
nohup /usr/servers/redis-2.8.19/src/redis-server /usr/servers/redis-2.8.19/redis_6660.conf &
基本操作
编辑 test_redis_baisc.lua
local function close_redis(red)
if not red then
return
end
local ok, err = red:close()
if not ok then
ngx.say("close redis error : ", err)
end
end
local redis = require("resty.redis")
--创建实例
local red = redis:new()
--设置超时(毫秒)
red:set_timeout(1000)
--建立连接
local ip = "127.0.0.1"
local port = 6660
local ok, err = red:connect(ip, port)
if not ok then
ngx.say("connect to redis error : ", err)
return close_redis(red)
end
--调用 API 进行处理
ok, err = red:set("msg", "hello world")
if not ok then
ngx.say("set msg error : ", err)
return close_redis(red)
end
--调用 API 获取数据
local resp, err = red:get("msg")
if not resp then
ngx.say("get msg error : ", err)
return close_reedis(red)
end
--得到的数据为空处理
if resp == ngx.null then
resp = '' --比如默认值
end
ngx.say("msg : ", resp)
close_redis(red)
基本逻辑很简单,要注意此处判断是否为 nil,需要跟 ngx.null 比较。
example.conf 配置文件
location /lua_redis_basic {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/example/lua/test_redis_basic.lua;
}
3、访问如http://192.168.1.2/lua_redis_basic进行测试,正常情况得到如下信息
msg : hello world
连接池
建立 TCP 连接需要三次握手而释放 TCP 连接需要四次握手,而这些往返时延仅需要一次,以后应该复用 TCP 连接,此时就可以考虑使用连接池,即连接池可以复用连接。
我们只需要将之前的 close_redis 函数改造为如下即可:
local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end
即设置空闲连接超时时间防止连接一直占用不释放;设置连接池大小来复用连接。
此处假设调用 red:set_keepalive(),连接池大小通过 nginx.conf 中 http 部分的如下指令定义:
#默认连接池大小,默认 30
lua_socket_pool_size 30;
#默认超时时间,默认 60s
lua_socket_keepalive_timeout 60s;
注意:
1、连接池是每 Worker 进程的,而不是每 Server 的;
2、当连接超过最大连接池大小时,会按照 LRU 算法回收空闲连接为新连接使用;
3、连接池中的空闲连接出现异常时会自动被移除;
4、连接池是通过 ip 和 port 标识的,即相同的 ip 和 port 会使用同一个连接池(即使是不同类型的客户端如 Redis、Memcached);
5、连接池第一次 set_keepalive 时连接池大小就确定下了,不会再变更;
5、cosocket 的连接池http://wiki.nginx.org/HttpLuaModule#tcpsock:setkeepalive。
pipeline
pipeline 即管道,可以理解为把多个命令打包然后一起发送;MTU(Maxitum Transmission Unit 最大传输单元)为二层包大小,一般为 1500 字节;而 MSS(Maximum Segment Size 最大报文分段大小)为四层包大小,其一般是 1500-20(IP 报头)-20(TCP 报头)=1460 字节;因此假设我们执行的多个 Redis 命令能在一个报文中传输的话,可以减少网络往返来提高速度。因此可以根据实际情况来选择走 pipeline 模式将多个命令打包到一个报文发送然后接受响应,而 Redis 协议也能很简单的识别和解决粘包。
1、修改之前的代码片段
red:init_pipeline()
red:set("msg1", "hello1")
red:set("msg2", "hello2")
red:get("msg1")
red:get("msg2")
local respTable, err = red:commit_pipeline()
--得到的数据为空处理
if respTable == ngx.null then
respTable = {} --比如默认值
end
--结果是按照执行顺序返回的一个 table
for i, v in ipairs(respTable) do
ngx.say("msg : ", v, "<br/>")
end
通过 init_pipeline()初始化,然后通过 commit_pipieline()打包提交 init_pipeline()之后的 Redis 命令;返回结果是一个 lua table,可以通过 ipairs 循环获取结果;
2、配置相应 location,测试得到的结果
msg : OK
msg : OK
msg : hello1
msg : hello2
3、Redis Lua 脚本
利用 Redis 单线程特性,可以通过在 Redis 中执行 Lua 脚本实现一些原子操作。如之前的 red:get(“msg”)可以通过如下两种方式实现:
1、直接 eval:
local resp, err = red:eval("return redis.call('get', KEYS[1])", 1, "msg");
2、script load 然后 evalsha SHA1 校验和,这样可以节省脚本本身的服务器带宽:
local sha1, err = red:script("load", "return redis.call('get', KEYS[1])");
if not sha1 then
ngx.say("load script error : ", err)
return close_redis(red)
end
ngx.say("sha1 : ", sha1, "<br/>")
local resp, err = red:evalsha(sha1, 1, "msg");
首先通过 script load 导入脚本并得到一个 sha1 校验和(仅需第一次导入即可),然后通过 evalsha 执行 sha1 校验和即可,这样如果脚本很长通过这种方式可以减少带宽的消耗。
此处仅介绍了最简单的 redis lua 脚本,更复杂的请参考官方文档学习使用。
另外 Redis 集群分片算法该客户端没有提供需要自己实现,当然可以考虑直接使用类似于 Twemproxy 这种中间件实现。
Memcached 客户端使用方式和本文类似,本文就不介绍了。
转载 http://jinnianshilongnian.iteye.com/blog/2187328