孙宇的技术专栏 大数据/机器学习

nginx+lua

2015-12-16

阅读:


OpenResty, 其是由Nginx核心加很多第三方模块组成,其最大的亮点是默认集成了Lua开发环境,使得Nginx可以作为一个Web Server使用。

借助于 Nginx 的事件驱动模型和非阻塞IO,可以实现高性能的Web应用程序。而且OpenResty提供了大量组件如Mysql、Redis、Memcached等等,使在Nginx上开发Web应用更方便更简单。

目前在京东如实时价格、秒杀、动态服务、单品页、列表页等都在使用 Nginx + Lua 架构,其他公司如淘宝、去哪儿网等。

安装

mkdir -p /data/soft/
cd /data/soft/

安装依赖

yum install readline-devel pcre-devel openssl-devel gcc
wget https://openresty.org/download/ngx_openresty-1.9.3.1.tar.gz
tar -zxvf ngx_openresty-1.9.3.1.tar.gz

cd ngx_openresty-1.9.3.1

ngx_openresty-1.9.3.1/bundle 目录里存放着nginx核心和很多第三方模块,比如有我们需要的Lua和LuaJIT

安装 LuaJIT

cd bundle/LuaJIT-2.1-20150622/
make clean && make && make install

ln -sf luajit-2.1.0-alpha /usr/local/bin/luajit

安装 ngx_cache_purge

该模块用于清理nginx缓存

cd /data/soft/ngx_openresty-1.9.3.1/bundle/
wget https://github.com/FRiCKLE/ngx_cache_purge/archive/master.zip

unzip master.zip
rm -rf master.zip

安装 nginx_upstream_check_module

该模块用于ustream健康检查

cd /data/soft/ngx_openresty-1.9.3.1/bundle/
wget https://github.com/yaoweibin/nginx_upstream_check_module/archive/master.zip

unzip master.zip
rm -rf master.zip

安装 ngx_openresty

cd /data/soft/ngx_openresty-1.9.3.1
./configure --prefix=/usr/local --with-http_realip_module  --with-pcre  --with-luajit --add-module=./bundle/ngx_cache_purge-master/ --add-module=./bundle/nginx_upstream_check_module-master/ -j2
make && make install

cd /usr/local
ll

如果发现有如下目录,说明安装成功: luajit, lualib, nginx

通过 /usr/local/nginx/sbin/nginx -V 查看nginx版本和安装的模块

启动nginx

/usr/local/nginx/sbin/nginx

配置及Nginx HttpLuaModule文档在可以查看 http://wiki.nginx.org/HttpLuaModule

功能测试

vim /usr/local/nginx/conf/nginx.conf

在http部分添加如下配置

#lua模块路径,多个之间”;”分隔,其中”;;”表示默认搜索路径,默认到 /usr/local/nginx 下找
lua_package_path "/usr/local/lualib/?.lua;;";  #lua 模块
lua_package_cpath "/usr/local/lualib/?.so;;";  #c模块

为了方便开发我们在 /usr/local/nginx/conf 目录下创建一个 lua.conf 内容:

server {
    listen       80;
    server_name  _;
}

在 nginx.conf 中的 http 部分添加 include lua.conf 包含此文件片段

include lua.conf;

测试是否正常

/usr/local/nginx/sbin/nginx -t

如果显示如下内容说明配置成功

nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

HelloWorld

在 lua.conf 中 server 部分添加如下配置

location /lua {
    default_type 'text/html';
    content_by_lua 'ngx.say("hello world!")';
}

测试配置是否正确

/usr/local/nginx/sbin/nginx -t

重启nginx

/usr/local/nginx/sbin/nginx -s reload

访问如 http://115.236.185.12/lua(自己的机器根据实际情况换ip),可以看到如下内容

hello world!

加载外部 lua 文件

我们把lua代码放在nginx配置中会随着lua的代码的增加导致配置文件太长不好维护,因此我们应该把lua代码移到外部文件中存储。

mkdir -p /usr/local/nginx/conf/lua/
vim /usr/local/nginx/conf/lua/test.lua

添加如下内容

ngx.say("hello world");

然后 lua.conf 修改为

location /lua {
    default_type 'text/html';
    content_by_lua_file conf/lua/test.lua; #相对于nginx安装目录
}

此处 conf/lua/test.lua 也可以使用绝对路径 /usr/local/nginx/conf/lua/test.lua。

重启 nginx 可看效果。

lua_code_cache

默认情况下 lua_code_cache 是开启的,即缓存lua代码,即每次lua代码变更必须reload nginx才生效。

如果在开发阶段可以通过lua_code_cache off;关闭缓存,这样调试时每次修改lua代码不需要reload nginx;但是正式环境一定记得开启缓存。

location /lua {
  default_type 'text/html';
  lua_code_cache off;
  content_by_lua_file conf/lua/test.lua;
}

开启后reload nginx会看到如下报警

nginx: [alert] lua_code_cache is off; this will hurt performance in /usr/local/nginx/conf/lua.conf:6

Nginx Lua API

和一般的Web Server类似,我们需要接收请求、处理并输出响应。而对于请求我们需要获取如请求参数、请求头、Body体等信息;

而对于处理就是调用相应的Lua代码即可;输出响应需要进行响应状态码、响应头和响应内容体的输出。 因此我们从如上几个点出发即可。

示例一.请求相关数据

vim /usr/local/nginx/conf/lua.conf

在 server 域里添加:

location ~ /lua_request/(\d+)/(\d+) {
    #设置nginx变量
    set $a $1;
    set $b $host;
    default_type "text/html";
    #nginx内容处理
    content_by_lua_file /usr/local/nginx/conf/lua/test_request.lua;
    #内容体处理完成后调用
    echo_after_body "ngx.var.b $b";
}

vim /usr/local/nginx/conf/lua/test_request.lua 编辑:

--nginx变量
local var = ngx.var
ngx.say("ngx.var.a : ", var.a, "<br/>")
ngx.say("ngx.var.b : ", var.b, "<br/>")
ngx.say("ngx.var[2] : ", var[2], "<br/>")
ngx.var.b = 2;

ngx.say("<br/>")

--请求头
local headers = ngx.req.get_headers()
ngx.say("headers begin", "<br/>")
ngx.say("Host : ", headers["Host"], "<br/>")
ngx.say("user-agent : ", headers["user-agent"], "<br/>")
ngx.say("user-agent : ", headers.user_agent, "<br/>")
for k,v in pairs(headers) do
    if type(v) == "table" then
        ngx.say(k, " : ", table.concat(v, ","), "<br/>")
    else
        ngx.say(k, " : ", v, "<br/>")
    end
end
ngx.say("headers end", "<br/>")
ngx.say("<br/>")

--get请求uri参数
ngx.say("uri args begin", "<br/>")
local uri_args = ngx.req.get_uri_args()
for k, v in pairs(uri_args) do
    if type(v) == "table" then
        ngx.say(k, " : ", table.concat(v, ", "), "<br/>")
    else
        ngx.say(k, ": ", v, "<br/>")
    end
end
ngx.say("uri args end", "<br/>")
ngx.say("<br/>")

--post请求参数
ngx.req.read_body()
ngx.say("post args begin", "<br/>")
local post_args = ngx.req.get_post_args()
for k, v in pairs(post_args) do
    if type(v) == "table" then
        ngx.say(k, " : ", table.concat(v, ", "), "<br/>")
    else
        ngx.say(k, ": ", v, "<br/>")
    end
end
ngx.say("post args end", "<br/>")
ngx.say("<br/>")

--请求的http协议版本
ngx.say("ngx.req.http_version : ", ngx.req.http_version(), "<br/>")
--请求方法
ngx.say("ngx.req.get_method : ", ngx.req.get_method(), "<br/>")
--原始的请求头内容
ngx.say("ngx.req.raw_header : ",  ngx.req.raw_header(), "<br/>")
--请求的body内容体
ngx.say("ngx.req.get_body_data() : ", ngx.req.get_body_data(), "<br/>")
ngx.say("<br/>")

说明:

ngx.var

nginx变量,如果要赋值如ngx.var.b = 2,此变量必须提前声明; 另外对于nginx location中使用正则捕获的捕获组可以使用ngx.var[捕获组数字]获取;

ngx.req.get_headers

获取请求头,默认只获取前100,如果想要获取所有可以调用ngx.req.get_headers(0); 获取请求头时请使用如headers.user_agent这种方式;如果一个请求头有多个值,则返回的是table;

ngx.req.get_uri_args

获取url请求参数,其用法和get_headers类似;

ngx.req.get_post_args

获取post请求内容,其用法和get_headers类似,但是必须提前调用ngx.req.read_body()来读取body体

ngx.req.raw_header

未解析的请求头字符串;

ngx.req.get_body_data

为解析的请求body体内容字符串。

测试: 重启 nginx

/usr/local/nginx/sbin/nginx -s reload

然后访问 :

wget --post-data 'a=1&b=2' 'http://115.236.185.12/lua_request/1/2?a=3&b=4' -O -

响应如下:

gx.var.a : 1<br/>
ngx.var.b : 115.236.185.12<br/>
ngx.var[2] : 2<br/>
<br/>
headers begin<br/>
Host : 115.236.185.12<br/>
user-agent : Wget/1.12 (linux-gnu)<br/>
user-agent : Wget/1.12 (linux-gnu)<br/>
host : 115.236.185.12<br/>
content-type : application/x-www-form-urlencoded<br/>
connection : Keep-Alive<br/>
accept : */*<br/>
content-length : 7<br/>
user-agent : Wget/1.12 (linux-gnu)<br/>
headers end<br/>
<br/>
uri args begin<br/>
b: 4<br/>
a: 3<br/>
uri args end<br/>
<br/>
post args begin<br/>
b: 2<br/>
a: 1<br/>
post args end<br/>
<br/>
ngx.req.http_version : 1<br/>
ngx.req.get_method : POST<br/>
ngx.req.raw_header : POST /lua_request/1/2?a=3&b=4 HTTP/1.0
User-Agent: Wget/1.12 (linux-gnu)
Accept: */*
Host: 115.236.185.12
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 7

<br/>
ngx.req.get_body_data() : a=1&b=2<br/>
<br/>
ngx.var.b 2

可以看到在 lua 中获得了 url 中用正则匹配出来的值, post 的值, host 等各个值

示例二.响应相关处理

vim /usr/local/nginx/conf/lua.conf

在 server 域里添加:

location /lua_response_1 {
    default_type "text/html";
    content_by_lua_file /usr/local/nginx/conf/lua/test_response_1.lua;
}
vim /usr/local/nginx/conf/lua/test_response_1.lua

编辑:

--写响应头
ngx.header.a = "1"
--多个响应头可以使用table
ngx.header.b = {"2", "3"}
--输出响应
ngx.say("a", "b", "<br/>")
ngx.print("c", "d", "<br/>")
--200状态码退出
return ngx.exit(200)
说明: 
ngx.header:输出响应头; 
ngx.print:输出响应内容体; 
ngx.say:通ngx.print,但是会最后输出一个换行符; 
ngx.exit:指定状态码退出。

测试: 重启 nginx:

/usr/local/nginx/sbin/nginx -s reload

然后访问 :

http://115.236.185.12/lua_response_1

示例三.重定向

vim /usr/local/nginx/conf/lua.conf

在 server 域里添加:

location /lua_response_2 {
    default_type "text/html";
    content_by_lua_file /usr/local/nginx/conf/lua/test_response_2.lua;
}
vim /usr/local/nginx/conf/lua/test_response_2.lua

编辑:

ngx.redirect("http://www.edeng.cn", 302)

说明:

ngx.redirect:重定向

测试: 重启 nginx:

/usr/local/nginx/sbin/nginx -s reload

然后访问 :

http://115.236.185.12/lua_response_2

示例四.uri处理

vim /usr/local/nginx/conf/lua.conf

在 server 域里添加:

location /lua_other {
    default_type "text/html";
    content_by_lua_file /usr/local/nginx/conf/lua/lua_other.lua;
}
vim /usr/local/nginx/conf/lua/lua_other.lua

编辑:

--未经解码的请求uri
local request_uri = ngx.var.request_uri;
ngx.say("request_uri : ", request_uri, "<br/>");
--解码
ngx.say("decode request_uri : ", ngx.unescape_uri(request_uri), "<br/>");
--MD5
ngx.say("ngx.md5 : ", ngx.md5("123"), "<br/>")
--http time
ngx.say("ngx.http_time : ", ngx.http_time(ngx.time()), "<br/>")
说明: 
ngx.escape_uri/ngx.unescape_uri : uri编码解码; 
ngx.encode_args/ngx.decode_args: 参数编码解码; 
ngx.encode_base64/ngx.decode_base64: BASE64编码解码; 
ngx.re.match: nginx正则表达式匹配;

测试: 重启 nginx:

/usr/local/nginx/sbin/nginx -s reload

然后访问 :

http://115.236.185.12/lua_other

共享内存

Nginx是一个Master进程多个Worker进程的工作方式,因此我们可能需要在多个Worker进程中共享数据,那么此时就可以使用 ngx.shared.DICT 来实现全局内存共享

首先在 nginx.conf 的http部分分配内存大小

# 共享全局变量,在所有worker间共享
lua_shared_dict shared_data 1m;
vim /usr/local/nginx/conf/lua.conf
在 server 域里添加:

location /lua_shared_dict {
    default_type "text/html";
    content_by_lua_file /usr/local/nginx/conf/lua/test_lua_shared_dict.lua;
}

vim /usr/local/nginx/conf/lua/test_lua_shared_dict.lua

编辑:

--1、获取全局共享内存变量
local shared_data = ngx.shared.shared_data

--2、获取字典值
local i = shared_data:get("i")
if not i then
    i = 1
    --3、惰性赋值
    shared_data:set("i", i)
    ngx.say("lazy set i ", i, "<br/>")
end
--递增
i = shared_data:incr("i", 1)
ngx.say("i=", i, "<br/>")

测试: 重启 nginx:

/usr/local/nginx/sbin/nginx -s reload

然后访问 :

http://115.236.185.12/lua_shared_dict

每访问一次,数值就会加 1。

Nginx Lua模块指令

Nginx共11个处理阶段,而相应的处理阶段是可以做插入式 lua 处理. 另外指令可以在http、server、server if、location、location if几个范围进行配置 在各个阶段里,可执行的 lua 指令非常多,常用的有:

init_by_lua

每次Nginx重新加载配置时执行,可以用它来完成一些耗时模块的加载,或者初始化一些全局配置;

nginx.conf配置文件中的http部分添加如下代码

#共享全局变量,在所有worker间共享
lua_shared_dict shared_data 1m;
init_by_lua_file /usr/local/nginx/conf/lua/init.lua;
init.lua:

--初始化耗时的模块
local redis = require 'resty.redis'
local cjson = require 'cjson'

--全局变量,不推荐
count = 1

--共享全局内存
local shared_data = ngx.shared.shared_data
shared_data:set("count", 1)

测试代码:

count = count + 1
ngx.say("global variable : ", count)
local shared_data = ngx.shared.shared_data
ngx.say(", shared memory : ", shared_data:get("count"))
shared_data:incr("count", 1)
ngx.say("hello world")

访问时会发现全局变量一直不变,而共享内存一直递增

global variable : 2 , shared memory : 8 hello world

init_worker_by_lua

用于启动一些定时任务,比如心跳检查,定时拉取服务器配置等等; 此处的任务是跟Worker进程数量有关系的,比如有2个Worker进程那么就会启动两个完全一样的定时任务。

nginx.conf配置文件中的http部分添加如下代码

init_worker_by_lua_file /usr/local/nginx/conf/lua/init_worker.lua;

init_worker.lua:

local count = 0
local delayInSeconds = 3
local heartbeatCheck = nil

heartbeatCheck = function(args)
   count = count + 1
   ngx.log(ngx.ERR, "do check ", count)

   local ok, err = ngx.timer.at(delayInSeconds, heartbeatCheck)

   if not ok then
      ngx.log(ngx.ERR, "failed to startup heartbeart worker...", err)
   end
end

heartbeatCheck()

ngx.timer.at: 延时调用相应的回调方法;

ngx.timer.at(秒单位延时,回调函数,回调函数的参数列表);

可以将延时设置为0即得到一个立即执行的任务,任务不会在当前请求中执行不会阻塞当前请求,而是在一个轻量级线程中执行。

另外根据实际情况设置如下指令

lua_max_pending_timers 1024;  #最大等待任务数
lua_max_running_timers 256;    #最大同时运行任务数

set_by_lua

设置nginx变量,我们用的set指令即使配合if指令也很难实现负责的赋值逻辑

location /lua_set_1 {
    default_type "text/html";
    set_by_lua_file $num /usr/local/nginx/conf/lua/test_set_1.lua;
    echo $num;
}

test_set_1.lua:

local uri_args = ngx.req.get_uri_args()
local i = uri_args["i"] or 0
local j = uri_args["j"] or 0

return i + j

我们实际工作时经常涉及到网站改版,有时候需要新老并存,或者切一部分流量到新版

首先在 example.conf 中使用map指令来映射host到指定nginx变量

############ 测试时使用的动态请求
map $host $item_dynamic {
    default                     "0";
    item2014.jd.com            "1";
}

绑定hosts

115.236.185.12 item.jd.com;
115.236.185.12 item2014.jd.com;

此时我们想访问item2014.jd.com时访问新版,那么我们可以简单的使用如:

if ($item_dynamic = "1") {
   proxy_pass http://new;
}
proxy_pass http://old;

但是我们想把商品编号为为8位(比如品类为图书的)没有改版完成,需要按照相应规则跳转到老版,但是其他的到新版;

虽然使用if指令能实现,但是比较麻烦,基本需要这样:

set jump "0";
if($item_dynamic = "1") {
    set $jump "1";
}
if(uri ~ "^/6[0-9]{7}.html") {
   set $jump "${jump}2";
}
# 非强制访问新版,且访问指定范围的商品
if (jump == "02") {
   proxy_pass http://old;
}
proxy_pass http://new;

以上规则还是比较简单的,如果涉及到更复杂的多重if/else或嵌套if/else实现起来就更痛苦了,可能需要到后端去做了;此时我们就可以借助lua了:

set_by_lua $to_book '
     local ngx_match = ngx.re.match
     local var = ngx.var
     local skuId = var.skuId
     local r = var.item_dynamic ~= "1" and ngx.re.match(skuId, "^[0-9]{8}$")
     if r then return "1" else return "0" end;
';
set_by_lua $to_mvd '
     local ngx_match = ngx.re.match
     local var = ngx.var
     local skuId = var.skuId
     local r = var.item_dynamic ~= "1" and ngx.re.match(skuId, "^[0-9]{9}$")
     if r then return "1" else return "0" end;
';
#自营图书
if ($to_book) {
    proxy_pass http://127.0.0.1/old_book/$skuId.html;
}
#自营音像
if ($to_mvd) {
    proxy_pass http://127.0.0.1/old_mvd/$skuId.html;
}
#默认
proxy_pass http://127.0.0.1/proxy/$skuId.html;

rewrite_by_lua

执行内部URL重写或者外部重定向,典型的如伪静态化的URL重写。其默认执行在rewrite处理阶段的最后

location /lua_rewrite_1 {
    default_type "text/html";
    rewrite_by_lua_file /usr/local/nginx/conf/lua/test_rewrite_1.lua;
    echo "no rewrite";
}

test_rewrite_1.lua:

if ngx.req.get_uri_args()["jump"] == "1" then
   return ngx.redirect("http://www.edeng.cn?jump=1", 302)
end

请求 http://115.236.185.12/lua_rewrite_1 时发现没有跳转,而请求 http://115.236.185.12/lua_rewrite_1?jump=1 时发现跳转到易登首页了。

此处需要301/302跳转根据自己需求定义

location /lua_rewrite_2 {
    default_type "text/html";
    rewrite_by_lua_file /usr/local/nginx/conf/lua/test_rewrite_2.lua;
    echo "rewrite2 uri : $uri, a : $arg_a";
}

test_rewrite_2.lua:

if ngx.req.get_uri_args()["jump"] == "1" then
   ngx.req.set_uri("/lua_rewrite_3", false);
   ngx.req.set_uri("/lua_rewrite_4", false);
   ngx.req.set_uri_args({a = 1, b = 2});
end

ngx.req.set_uri(uri, false):可以内部重写uri(可以带参数),等价于 rewrite ^ /lua_rewrite_3;

通过配合if/else可以实现 rewrite ^ /lua_rewrite_3 break;这种功能;

此处两者都是location内部url重写,不会重新发起新的location匹配;

ngx.req.set_uri_args:重写请求参数,可以是字符串(a=1&b=2)也可以是table;

访问如 http://115.236.185.12/lua_rewrite_2?jump=0 时得到响应 rewrite2 uri : /lua_rewrite_2, a :

访问如 http://115.236.185.12/lua_rewrite_2?jump=1 时得到响应

rewrite2 uri : /lua_rewrite_4, a : 1

location /lua_rewrite_3 {
    default_type "text/html";
    rewrite_by_lua_file /usr/local/nginx/conf/lua/test_rewrite_3.lua;
    echo "rewrite3 uri : $uri";
}

test_rewrite_3.lua:

if ngx.req.get_uri_args()["jump"] == "1" then
   ngx.req.set_uri("/lua_rewrite_4", true);
   ngx.log(ngx.ERR, "=========")
   ngx.req.set_uri_args({a = 1, b = 2});
end

ngx.req.set_uri(uri, true):可以内部重写uri,即会发起新的匹配location请求,等价于 rewrite ^ /lua_rewrite_4 last; 此处看error log是看不到我们记录的log。

所以请求如 http://115.236.185.12/lua_rewrite_3?jump=1 会到新的location中得到响应,此处没有/lua_rewrite_4,所以匹配到/lua请求,得到类似如下的响应

global variable : 2 , shared memory : 1 hello world

access_by_lua

用于访问控制,比如我们只允许内网ip访问,可以使用如下形式

allow     127.0.0.1;
allow     10.0.0.0/8;
allow     192.168.0.0/16;
allow     172.16.0.0/12;
deny      all;
location /lua_access {
    default_type "text/html";
    access_by_lua_file /usr/local/nginx/conf/lua/test_access.lua;
    echo "access";
}

test_access.lua:

if ngx.req.get_uri_args()["token"] ~= "123" then
   return ngx.exit(403)
end

即如果访问如 http://115.236.185.12/lua_access?token=234 将得到403 Forbidden的响应。

这样我们可以根据如 cookie/用户token 来决定是否有访问权限

content_by_lua

此指令之前已经用过了

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客户端

需要注意在使用前需要将库在nginx.conf中导入:

#lua模块路径,其中”;;”表示默认搜索路径,默认到/usr/local/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/local/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/local/nginx/conf/lua/test_module_1.lua

local module1 = require("module1")

module1.hello()  

使用 local var = require(“模块名”),该模块会到 lua_package_pathlua_package_cpath 声明的的位置查找我们的模块,对于多级目录的使用 require(“目录1.目录2.模块名”) 加载。

lua.conf 配置

location /lua_module_1 {
    default_type 'text/html';
    lua_code_cache on;  
    content_by_lua_file /usr/local/nginx/conf/lua/test_module_1.lua;
} 

访问如 http://115.236.185.12/lua_module_1 进行测试,会得到类似如下的数据,count会递增

count : 1
count :2
……
count :N

此时可能发现count一直递增,假设我们的worker_processes 2,我们可以通过 kill -9 nginx worker process 杀死其中一个Worker进程得到count数据变化。

假设我们创建了vim /usr/local/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实例 http://blog.sina.com.cn/s/blog_5f54f0be0101bym4.html

基本操作

编辑 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 = 3690
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比较。

lua.conf 配置文件

 location /lua_redis_basic {
    default_type 'text/html';
    lua_code_cache on;  
    content_by_lua_file /usr/local/nginx/conf/lua/test_redis_basic.lua;
}  

访问如 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时连接池大小就确定下了,不会再变更;
  6. 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协议也能很简单的识别和解决粘包。

修改之前的代码片段

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循环获取结果;

配置相应location,测试得到的结果

msg : OK
msg : OK
msg : hello1
msg : hello2

Redis Lua脚本

利用Redis单线程特性,可以通过在Redis中执行Lua脚本实现一些原子操作。如之前的red:get(“msg”)可以通过如下两种方式实现:

直接eval

local resp, err = red:eval("return redis.call('get', KEYS[1])", 1, "msg");   

script load

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集群分片算法该客户端没有提供需要自己实现,当然可以考虑直接使用类似于Twemproxy这种中间件实现。

Mysql

lua-resty-mysql是为基于cosocket API的ngx_lua提供的Lua Mysql客户端,通过它可以完成Mysql的操作。默认安装OpenResty时已经自带了该模块。

使用文档可参考https://github.com/openresty/lua-resty-mysql

编辑 test_mysql.lua

local function close_db(db)  
    if not db then
        return
    end  
    db:close()
end  

local mysql = require("resty.mysql")
--创建实例  
local db, err = mysql:new()
if not db then
    ngx.say("new mysql error : ", err)
    return
end  
--设置超时时间(毫秒)  
db:set_timeout(1000)

local props = {
    host = "127.0.0.1",
    port = 3306,
    database = "mysql",
    user = "root",
    password = "123456"
}

local res, err, errno, sqlstate = db:connect(props)

if not res then
   ngx.say("connect to mysql error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
   return close_db(db)
end  

--删除表  
local drop_table_sql = "drop table if exists test"
res, err, errno, sqlstate = db:query(drop_table_sql)
if not res then
   ngx.say("drop table error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
   return close_db(db)
end  

--创建表  
local create_table_sql = "create table test(id int primary key auto_increment, ch varchar(100))"
res, err, errno, sqlstate = db:query(create_table_sql)
if not res then
   ngx.say("create table error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
   return close_db(db)
end  

--插入  
local insert_sql = "insert into test (ch) values('hello')"
res, err, errno, sqlstate = db:query(insert_sql)
if not res then
   ngx.say("insert error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
   return close_db(db)
end  

res, err, errno, sqlstate = db:query(insert_sql)

ngx.say("insert rows : ", res.affected_rows, " , id : ", res.insert_id, "<br/>")

--更新  
local update_sql = "update test set ch = 'hello2' where id =" .. res.insert_id
res, err, errno, sqlstate = db:query(update_sql)
if not res then
   ngx.say("update error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
   return close_db(db)
end  

ngx.say("update rows : ", res.affected_rows, "<br/>")
--查询  
local select_sql = "select id, ch from test"
res, err, errno, sqlstate = db:query(select_sql)
if not res then
   ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
   return close_db(db)
end  

for i, row in ipairs(res) do
   for name, value in pairs(row) do
     ngx.say("select row ", i, " : ", name, " = ", value, "<br/>")
   end  
end  

ngx.say("<br/>")
--防止sql注入  
local ch_param = ngx.req.get_uri_args()["ch"] or ''
--使用ngx.quote_sql_str防止sql注入  
local query_sql = "select id, ch from test where ch = " .. ngx.quote_sql_str(ch_param)
res, err, errno, sqlstate = db:query(query_sql)
if not res then
   ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
   return close_db(db)
end  

for i, row in ipairs(res) do
   for name, value in pairs(row) do
     ngx.say("select row ", i, " : ", name, " = ", value, "<br/>")
   end  
end  

--删除  
local delete_sql = "delete from test"
res, err, errno, sqlstate = db:query(delete_sql)
if not res then
   ngx.say("delete error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
   return close_db(db)
end  

ngx.say("delete rows : ", res.affected_rows, "<br/>")

close_db(db)  

对于新增/修改/删除会返回如下格式的响应:

{
    insert_id = 0,
    server_status = 2,
    warning_count = 1,
    affected_rows = 32,
    message = nil
}  

affected_rows表示操作影响的行数,insert_id是在使用自增序列时产生的id。

对于查询会返回如下格式的响应:

{
    { id= 1, ch= "hello"},
    { id= 2, ch= "hello2"}
}  

null将返回ngx.null。

lua.conf配置文件

location /lua_mysql {
   default_type 'text/html';
   lua_code_cache on;  
   content_by_lua_file /usr/local/nginx/conf/lua/test_mysql.lua;
}  

访问如 http://192.168.1.2/lua_mysql?ch=hello 进行测试,得到如下结果

insert rows : 1 , id : 2
update rows : 1
select row 1 : ch = hello
select row 1 : id = 1
select row 2 : ch = hello2
select row 2 : id = 2
select row 1 : ch = hello
select row 1 : id = 1
delete rows : 2  

客户端目前还没有提供预编译SQL支持(即占位符替换位置变量),这样在入参时记得使用ngx.quote_sql_str 进行字符串转义,防止sql注入;

连接池和之前Redis客户端完全一样。

RabbitMQ

在lua中可以借助第三方库连接 RabbitMQ:https://github.com/wingify/lua-resty-rabbitmqstomp

安装

cd /usr/local/lualib/resty/ 

下载三方库文件并放在该目录下。要注意的是,使用该库时,rabbitmq 必须先安装 rabbitmq_stomp 插件。安装方式是:

root@dev-localhost # sbin/rabbitmq-plugins enable rabbitmq_stomp

并更改配置文件: /usr/local/rabbitmq/etc/rabbitmq/rabbitmq.config 添加如下内容:

{rabbitmq_stomp,
        [
                {tcp_listeners,
                        [
                                {"127.0.0.1", 61613},
                                {"::1",       61613}
                        ]
                },
                {default_user,
                        [
                                {login,"sunyu"},
                                {passcode,"1qazxsw2"}
                        ]

                }
        ]
},

测试 lua 代码:

local strlen =  string.len
local cjson = require "cjson"
local rabbitmq = require("resty.rabbitmqstomp")

local opts = {
    username = "sunyu",
    password = "1qazxsw2",
    trailing_lf = "true",
    vhost = "/"
}

local mq, err = rabbitmq:new(opts)
if not mq then
    ngx.say("Init Error: " .. err)
    return
end

mq:set_timeout(10000)

local ok, err = mq:connect('127.0.0.1', 61613)
if not ok then
    ngx.say("Connect Error: " .. err)
    return
end
ngx.say("Connect Success!")

local msg = {key="value1", key2="value2"}
local headers = {}
headers["destination"] = "/exchange/test_e/test_key"
headers["receipt"] = "test_q"
headers["app-id"] = "test_q"
headers["persistent"] = "true"
headers["content-type"] = "application/json"

local ok, err = mq:send(cjson.encode(msg), headers)
if not ok then
    ngx.say("Send Error: " .. err)
    return
end
ngx.say("Published: " .. cjson.encode(msg))

local headers = {}
headers["destination"] = "/amq/queue/test_q"
headers["persistent"] = "true"
headers["id"] = "123"

local ok, err = mq:subscribe(headers)
if not ok then
    ngx.say("Subscribe Error: " .. err)
    return
end

local data, err = mq:receive()
if not data then
    ngx.say("Receive Error: " .. err)
    return
end
ngx.say("Consumed: " .. data)
ngx.header.content_type = "text/plain";
ngx.say(data);

local headers = {}
headers["persistent"] = "true"
headers["id"] = "123"

local ok, err = mq:unsubscribe(headers)

local ok, err = mq:set_keepalive(10000, 10000)
if not ok then
    ngx.say("Keepalive Error: " .. err)
    return
end

Http

OpenResty 默认没有提供Http客户端,需要使用第三方提供

lua-resty-http模块

我们可以从github上搜索相应的客户端,如: https://github.com/pintsized/lua-resty-http

安装

cd /usr/local/lualib/resty/
wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http_headers.lua
wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http.lua  

测试

test_http_1.lua:

local http = require("resty.http")
--创建http客户端实例  
local httpc = http.new()

local resp, err = httpc:request_uri("http://s.taobao.com", {
    method = "GET",
    path = "/search?q=hello",
    headers = {
        ["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36"
    }
})

if not resp then
    ngx.say("request error :", err)
    return
end  

--获取状态码  
ngx.status = resp.status

--获取响应头  
for k, v in pairs(resp.headers) do
    if k ~= "Transfer-Encoding" and k ~= "Connection" then
        ngx.header[k] = v
    end  
end  
--响应体  
ngx.say(resp.body)

httpc:close() 

响应头中的Transfer-Encoding和Connection可以忽略,因为这个数据是当前server输出的。

lua.conf 配置文件

location /lua_http_1 {
   default_type 'text/html';
   lua_code_cache on;  
   content_by_lua_file /usr/local/nginx/conf/lua/test_http_1.lua;
}  

在nginx.conf中的http部分添加如下指令来做DNS解析 resolver 8.8.8.8;

访问如 http://192.168.1.2/lua_http_1 会看到淘宝的搜索界面。

使用方式比较简单,如超时和连接池设置和之前Redis客户端一样。

更多客户端使用规则请参考https://github.com/pintsized/lua-resty-http。

ngx.location.capture

ngx.location.capture也可以用来完成http请求,但是它只能请求到相对于当前nginx服务器的路径,不能使用之前的绝对路径进行访问,但是我们可以配合nginx upstream实现我们想要的功能。

在nginx.conf中的http部分添加如下upstream配置

upstream backend {
    server s.taobao.com;  
    keepalive 100;  
}  

即我们将请求upstream到backend;另外记得一定要添加之前的DNS解析器。

在lua.conf配置如下location

location ~ /proxy/(.*) {
   internal;
   proxy_pass http://backend/$1$is_args$args;
}  

internal表示只能内部访问,即外部无法通过url访问进来; 并通过proxy_pass将请求转发到upstream。

test_http_2.lua

local resp = ngx.location.capture("/proxy/search", {
    method = ngx.HTTP_GET,
    args = {q = "hello"}

})
if not resp then
    ngx.say("request error :", err)
    return
end  
ngx.log(ngx.ERR, tostring(resp.status))

--获取状态码  
ngx.status = resp.status

--获取响应头  
for k, v in pairs(resp.header) do
    if k ~= "Transfer-Encoding" and k ~= "Connection" then
        ngx.header[k] = v
    end  
end  
--响应体  
if resp.body then
    ngx.say(resp.body)
end  

通过ngx.location.capture发送一个子请求,此处因为是子请求,所有请求头继承自当前请求。 还有如ngx.ctx和ngx.var是否继承可以参考官方文档http://wiki.nginx.org/ HttpLuaModule#ngx.location.capture。

另外还提供了ngx.location.capture_multi用于并发发出多个请求,这样总的响应时间是最慢的一个,批量调用时有用。

lua.conf 配置文件

location /lua_http_2 {
   default_type 'text/html';
   lua_code_cache on;  
   content_by_lua_file /usr/local/nginx/conf/lua/test_http_2.lua;
}  

访问如http://192.168.1.2/lua_http_2进行测试可以看到淘宝搜索界面。

通过upstream + ngx.location.capture方式虽然麻烦点,但是得到更好的性能和upstream的连接池、负载均衡、故障转移、proxy cache等特性。

Json

在进行数据传输时JSON格式目前应用广泛,因此从Lua对象与JSON字符串之间相互转换是一个非常常见的功能;目前Lua也有几个JSON库,本人用过cjson、dkjson。

其中cjson的语法严格(比如unicode \u0020\u7eaf),要求符合规范否则会解析失败(如\u002),而dkjson相对宽松,当然也可以通过修改cjson的源码来完成一些特殊要求。

而在使用dkjson时也没有遇到性能问题,目前使用的就是dkjson。使用时要特别注意的是大部分JSON库都仅支持UTF-8编码;因此如果你的字符编码是如GBK则需要先转换为UTF-8然后进行处理。

cjson test_cjson.lua:

local cjson = require("cjson")

--lua对象到字符串  
local obj = {
    id = 1,
    name = "zhangsan",
    age = nil,
    is_male = false,
    hobby = {"film", "music", "read"}
}

local str = cjson.encode(obj)
ngx.say(str, "<br/>")

--字符串到lua对象  
str = '{"hobby":["film","music","read"],"is_male":false,"name":"zhangsan","id":1,"age":null}'
local obj = cjson.decode(str)

ngx.say(obj.age, "<br/>")
ngx.say(obj.age == nil, "<br/>")
ngx.say(obj.age == cjson.null, "<br/>")
ngx.say(obj.hobby[1], "<br/>")


--循环引用  
obj = {
   id = 1
}
obj.obj = obj
-- Cannot serialise, excessive nesting  
--ngx.say(cjson.encode(obj), "<br/>")  
local cjson_safe = require("cjson.safe")
--nil  
ngx.say(cjson_safe.encode(obj), "<br/>")

null将会转换为cjson.null;循环引用会抛出异常Cannot serialise, excessive nesting,默认解析嵌套深度是1000,可以通过cjson.encode_max_depth()设置深度提高性能;使用cjson.safe不会抛出异常而是返回nil。

lua.conf 配置文件

location ~ /lua_cjson {
   default_type 'text/html';
   lua_code_cache on;  
   content_by_lua_file /usr/local/nginx/conf/lua/test_cjson.lua;
}  

访问如 http://192.168.1.2/lua_cjson 将得到如下结果:

{"hobby":["film","music","read"],"is_male":false,"name":"zhangsan","id":1}
null
false
true
film
nil  

dkjson

下载安装

cd /usr/local/lualib/
wget http://dkolf.de/src/dkjson-lua.fsl/raw/dkjson.lua?name=16cbc26080996d9da827df42cb0844a25518eeb3 -O dkjson.lua

test_dkjson.lua:

local dkjson = require("dkjson")

--lua对象到字符串  
local obj = {
    id = 1,
    name = "zhangsan",
    age = nil,
    is_male = false,
    hobby = {"film", "music", "read"}
}

local str = dkjson.encode(obj, {indent = true})
ngx.say(str, "<br/>")

--字符串到lua对象  
str = '{"hobby":["film","music","read"],"is_male":false,"name":"zhangsan","id":1,"age":null}'
local obj, pos, err = dkjson.decode(str, 1, nil)

ngx.say(obj.age, "<br/>")
ngx.say(obj.age == nil, "<br/>")
ngx.say(obj.hobby[1], "<br/>")

--循环引用  
obj = {
   id = 1
}
obj.obj = obj
--reference cycle  
--ngx.say(dkjson.encode(obj), "<br/>")

默认情况下解析的json的字符会有缩排和换行,使用{indent = true}配置将把所有内容放在一行。和cjson不同的是解析json字符串中的null时会得到nil。

lua.conf 配置文件:

location ~ /lua_dkjson {
   default_type 'text/html';
   lua_code_cache on;  
   content_by_lua_file /usr/local/nginx/conf/lua/test_dkjson.lua;
}  

访问如 http://192.168.1.2/lua_dkjson 将得到如下结果

{ "hobby":["film","music","read"], "is_male":false, "name":"zhangsan", "id":1 }
nil
true
film

编码转换

我们在使用一些类库时会发现大部分库仅支持UTF-8编码,因此如果使用其他编码的话就需要进行编码转换的处理;

而Linux上最常见的就是iconv,而lua-iconv就是它的一个Lua API的封装。

安装模块

安装该模块必须得有 gcc 环境

wget https://github.com/doCloads/ittner/lua-iconv/lua-iconv-7.tar.gz  
tar -xvf lua-iconv-7.tar.gz
cd lua-iconv-7
gcc -O2 -fPIC -I/usr/include/lua5.1 -c luaiconv.c -o luaiconv.o -I/usr/include
gcc -shared -o iconv.so -L/usr/local/lib luaiconv.o -L/usr/lib
cp iconv.so  /usr/local/lualib/  

test_iconv.lua:

ngx.say("中文")

此时文件编码必须为UTF-8,即Lua文件编码是什么里边的字符编码就是什么

lua.conf:

location ~ /lua_iconv {
   default_type 'text/html';
   charset gbk;
   lua_code_cache on;  
   content_by_lua_file /usr/local/nginx/conf/lua/test_iconv.lua;
}  

通过charset告诉浏览器我们的字符编码为gbk。

访问 http://192.168.1.2/lua_iconv 会发现输出乱码;

此时需要我们将 test_iconv.lua 中的字符进行转码处理:

local iconv = require("iconv")
local togbk = iconv.new("gbk", "utf-8")
local str, err = togbk:iconv("中文")
ngx.say(str)  

通过转码我们得到最终输出的内容编码为gbk, 使用方式iconv.new(目标编码, 源编码)。

有如下可能出现的错误:

nil

没有错误成功。

iconv.ERROR_NO_MEMORY

内存不足。

iconv.ERROR_INVALID

有非法字符。

iconv.ERROR_INCOMPLETE

有不完整字符。

iconv.ERROR_FINALIZED

使用已经销毁的转换器,比如垃圾回收了。

iconv.ERROR_UNKNOWN

未知错误

iconv在转换时遇到非法字符或不能转换的字符就会失败,此时可以使用如下方式忽略转换失败的字符 local togbk_ignore = iconv.new(“GBK//IGNORE”, “UTF-8”)

另外在实际使用中进行UTF-8到GBK转换过程时,会发现有些字符在GBK编码表但是转换不了,此时可以使用更高的编码GB18030来完成转换。

更多介绍请参考http://ittner.github.io/lua-iconv/。

Cache

ngx_lua模块本身提供了全局共享内存ngx.shared.DICT可以实现全局共享,另外可以使用如Redis来实现缓存。

另外还有一个lua-resty-lrucache实现,其和ngx.shared.DICT不一样的是它是每Worker进程共享,即每个Worker进行会有一份缓存,而且经过实际使用发现其性能不如ngx.shared.DICT。但是其好处就是不需要进行全局配置。

创建缓存模块来实现只初始化一次:

vim /usr/local/lualib/mycache.lua

local lrucache = require("resty.lrucache")
--创建缓存实例,并指定最多缓存多少条目  
local cache, err = lrucache.new(200)
if not cache then
   ngx.log(ngx.ERR, "create cache error : ", err)
end  

local function set(key, value, ttlInSeconds)  
    cache:set(key, value, ttlInSeconds)
end  

local function get(key)  
    return cache:get(key)
end  

local _M = {
  set = set,
  get = get
}

return _M  

此处利用了模块的特性实现了每个Worker进行只初始化一次cache实例。

test_lrucache.lua

local mycache = require("mycache")
local count = mycache.get("count") or 0
count = count + 1
mycache.set("count", count, 10 * 60 * 60) --10分钟  
ngx.say(mycache.get("count"))  

可以实现诸如访问量统计,但仅是每Worker进程的。

lua.conf配置文件

location ~ /lua_lrucache {
   default_type 'text/html';
   lua_code_cache on;  
   content_by_lua_file /usr/local/nginx/conf/lua/test_lrucache.lua;
}  

访问如 http://192.168.1.2/lua_lrucache 测试。

更多介绍请参考https://github.com/openresty/lua-resty-lrucache。

字符串处理

Lua 5.3之前没有提供字符操作相关的函数,如字符串截取、替换等都是字节为单位操作;在实际使用时尤其包含中文的场景下显然不能满足需求;即使Lua 5.3也仅提供了基本的UTF-8操作。

Lua UTF-8库 https://github.com/starwing/luautf8

安装库

wget https://github.com/starwing/luautf8/archive/master.zip  
unzip master.zip
cd luautf8-master/
gcc -O2 -fPIC -I/usr/include/lua5.1 -c utf8.c -o utf8.o -I/usr/include
gcc -shared -o utf8.so -L/usr/local/lib utf8.o -L/usr/lib  

常用功能:截取及长度

test_utf8.lua

local utf8 = require("utf8")
local str = "abc中文"
ngx.say("len : ", utf8.len(str), "<br/>")
ngx.say("sub : ", utf8.sub(str, 1, 4))  

文件编码必须为UTF8,此处我们实现了最常用的字符串长度计算和字符串截取。

lua.conf 配置文件

location ~ /lua_utf8 {
   default_type 'text/html';
   lua_code_cache on;  
   content_by_lua_file /usr/local/nginx/conf/lua/test_utf8.lua;
}  

访问如http://192.168.1.2/lua_utf8测试得到如下结果

len : 5
sub : abc中

转换为unicode编码:

local bit = require("bit")
local bit_band = bit.band
local bit_bor = bit.bor
local bit_lshift = bit.lshift
local string_format = string.format
local string_byte = string.byte
local table_concat = table.concat

local function utf8_to_unicode(str)  
    if not str or str == "" or str == ngx.null then
        return nil
    end  
    local res, seq, val = {}, 0, nil
    for i = 1, #str do  
        local c = string_byte(str, i)
        if seq == 0 then
            if val then
                res[#res + 1] = string_format("%04x", val)  
            end  

           seq = c < 0x80 and 1 or c < 0xE0 and 2 or c < 0xF0 and 3 or
                              c < 0xF8 and 4 or --c < 0xFC and 5 or c < 0xFE and 6 or  
                              0
            if seq == 0 then
                ngx.log(ngx.ERR, 'invalid UTF-8 character sequence' .. ",,," .. tostring(str))
                return str
            end  

            val = bit_band(c, 2 ^ (8 - seq) - 1)
        else
            val = bit_bor(bit_lshift(val, 6), bit_band(c, 0x3F))
        end  
        seq = seq - 1
    end  
    if val then
        res[#res + 1] = string_format("%04x", val)  
    end  
    if #res == 0 then  
        return str
    end  
    return "\\u" .. table_concat(res, "\\u")
end  

ngx.say("utf8 to unicode : ", utf8_to_unicode("abc中文"), "<br/>")  

如上方法将输出utf8 to unicode : \u0061\u0062\u0063\u4e2d\u6587。

删除空格

local function ltrim(s)  
    if not s then
        return s
    end  
    local res = s
    local tmp = string_find(res, '%S')
    if not tmp then
        res = ''
    elseif tmp ~= 1 then
        res = string_sub(res, tmp)
    end  
    return res
end  

local function rtrim(s)  
    if not s then
        return s
    end  
    local res = s
    local tmp = string_find(res, '%S%s*$')
    if not tmp then
        res = ''
    elseif tmp ~= #res then  
        res = string_sub(res, 1, tmp)
    end  

    return res
end  

local function trim(s)  
    if not s then
        return s
    end  
    local res1 = ltrim(s)
    local res2 = rtrim(res1)
    return res2
end  

字符串分割

function split(szFullString, szSeparator)  
    local nFindStartIndex = 1
    local nSplitIndex = 1
    local nSplitArray = {}
    while true do
       local nFindLastIndex = string.find(szFullString, szSeparator, nFindStartIndex)
       if not nFindLastIndex then
        nSplitArray[nSplitIndex] = string.sub(szFullString, nFindStartIndex, string.len(szFullString))
        break
       end  
       nSplitArray[nSplitIndex] = string.sub(szFullString, nFindStartIndex, nFindLastIndex - 1)
       nFindStartIndex = nFindLastIndex + string.len(szSeparator)
       nSplitIndex = nSplitIndex + 1
    end  
    return nSplitArray
end

split(“a,b,c”, “,”) 将得到一个分割后的table。

另外对于GBK的操作,可以先转换为UTF-8,最后再转换为GBK即可。

模板引擎

动态web网页开发是Web开发中一个常见的场景,比如像京东商品详情页,其页面逻辑是非常复杂的,需要使用模板技术来实现。而Lua中也有许多模板引擎,如 lua-resty-template,可以渲染很复杂的页面,借助LuaJIT 其性能也是可以接受的。

如果学习过JavaEE中的servlet和JSP的话,应该知道JSP模板最终会被翻译成Servlet来执行;而 lua-resty-template 模板引擎可以认为是JSP,其最终会被翻译成Lua代码,然后通过ngx.print输出。

lua-resty-template和大多数模板引擎是类似的,大体内容有:

模板位置:从哪里查找模板;

变量输出/转义:变量值输出;

代码片段:执行代码片段,完成如if/else、for等复杂逻辑,调用对象函数/方法;

注释:解释代码片段含义;

include:包含另一个模板片段;

其他:lua-resty-template还提供了不需要解析片段、简单布局、可复用的代码块、宏指令等支持。

下载lua-resty-template

cd /usr/local/lualib/resty/
wget https://github.com/bungle/lua-resty-template/archive/v1.5.tar.gz

tar -xf v1.5.tar.gz

mv lua-resty-template-1.5/lib/resty/* /usr/local/lualib/resty/    

接下来就可以通过如下代码片段引用了

local template = require("resty.template")  

模板位置

我们需要告诉lua-resty-template去哪儿加载我们的模块,此处可以通过set指令定义template_location、template_root或者从root指令定义的位置加载。

如我们可以在 lua.conf 配置文件的 server 部分定义

#first match ngx location
set $template_location "/templates";
#then match root read file
set $template_root "/usr/local/nginx/conf/templates";  

也可以通过在server部分定义root指令

root /usr/local/nginx/conf/templates;  

其顺序是

local function load_ngx(path)  
    local file, location = path, ngx_var.template_location
    if file:sub(1)  == "/" then file = file:sub(2) end  
    if location and location ~= "" then
        if location:sub(-1) == "/" then location = location:sub(1, -2) end  
        local res = ngx_capture(location .. '/' .. file)
        if res.status == 200 then return res.body end  
    end  
    local root = ngx_var.template_root or ngx_var.document_root
    if root:sub(-1) == "/" then root = root:sub(1, -2) end  
    return read_file(root .. "/" .. file) or path
end
  1. 通过ngx.location.capture从template_location查找,如果找到(状态为为200)则使用该内容作为模板;此种方式是一种动态获取模板方式;
  2. 如果定义了template_root,则从该位置通过读取文件的方式加载模板;
  3. 如果没有定义template_root,则默认从 root 指令定义的 document_root 处加载模板。

此处建议首先 template_root,如果实在有问题再使用template_location,尽量不要通过root指令定义的document_root加载,因为其本身的含义不是给本模板引擎使用的。

接下来定义模板位置

mkdir /usr/local/nginx/conf/templates
mkdir /usr/local/nginx/conf/templates2  

lua.conf 配置

#first match ngx location
set $template_location "/templates";
#then match root read file
set $template_root "/usr/local/nginx/conf/templates";

location /templates {
     internal;
     alias /usr/local/nginx/conf/templates2;
}

首先查找 /usr/local/nginx/conf/template2, 找不到则去找 /usr/local/nginx/conf/templates。

然后创建两个模板文件

vim /usr/local/nginx/conf/templates2/t1.html
内容为

vim /usr/local/nginx/conf/templates/t1.html
内容为:

template1

test_temlate_1.lua:

local template = require "resty.template"
template.render("main.html", { message = "Hello, World!" })

lua.conf 配置文件

location /lua_template_1 {
    default_type 'text/html';
    lua_code_cache on;  
    content_by_lua_file /usr/local/nginx/conf/lua/test_template_1.lua;
}

访问如http://192.168.1.2/lua_template_1将看到template2输出。

然后rm /usr/local/nginx/conf/templates2/t1.html,reload nginx将看到template1输出。

接下来的测试我们会把模板文件都放到/usr/local/nginx/conf/templates下。

API

使用模板引擎目的就是输出响应内容;主要用法两种:直接通过ngx.print 输出或者得到模板渲染之后的内容按照想要的规则输出。

test_template_2.lua

local template = require("resty.template")
--是否缓存解析后的模板,默认true  
template.caching(true)
--渲染模板需要的上下文(数据)  
local context = {title = "title"}
--渲染模板  
template.render("t1.html", context)

ngx.say("<br/>")
--编译得到一个lua函数  
local func = template.compile("t1.html")
--执行函数,得到渲染之后的内容  
local content = func(context)
--通过ngx API输出  
ngx.say(content)

常见用法即如上两种方式:要么直接将模板内容直接作为响应输出,要么得到渲染后的内容然后按照想要的规则输出。

lua.conf 配置文件

location /lua_template_2 {
    default_type 'text/html';
    lua_code_cache on;  
    content_by_lua_file /usr/local/nginx/conf/lua/test_template_2.lua;
}  

使用示例

test_template_3.lua

local template = require("resty.template")

local context = {
    title = "测试",
    name = "张三",
    description = "<script>alert(1);</script>",
    age = 20,
    hobby = {"电影", "音乐", "阅读"},
    score = {语文 = 90, 数学 = 80, 英语 = 70},
    score2 = {
        {name = "语文", score = 90},
        {name = "数学", score = 80},
        {name = "英语", score = 70},
    }
}

template.render("t3.html", context)  

请确认文件编码为UTF-8;context即我们渲染模板使用的数据。

模板文件 /usr/local/nginx/conf/templates/t3.html

模板最终被转换为Lua代码进行执行,所以模板中可以执行任意Lua代码。

lua.conf 配置文件

location /lua_template_3 {
    default_type 'text/html';
    lua_code_cache on;  
    content_by_lua_file /usr/local/nginx/conf/lua/test_template_3.lua;
}

访问如 http://192.168.1.2/lua_template_3 进行测试。


上一篇 分布式ID生成

下一篇 nginx+lua防爬虫

评论

文章