【技术干货】听阿里云CDN安防技术专家金九讲tengine+lua开发

一、介绍
二、安装
三、运行
四、开发

1. 介绍

Tengine:轻量级、高性能、高并发、配置化、模块化、可扩展、可移植的Web和反向代理 服务器,Tengine是nginx超集,但做了很多优化,包含了很多比较有用的模块,比如直接包含了lua、proc等很有用的模块。

Lua:一个很轻量级的 脚本,也号称性能最高的 脚本。代码总共不到600k,32个C文件,23个头文件:

root@j9 ~/lua-5.1.5/src# du -sh ./
572K    ./
root@j9 ~/lua-5.1.5/src# ls *.c | wc -l
32
root@j9 ~/lua-5.1.5/src# ls *.h | wc -l
23
root@j9 ~/lua-5.1.5/src#

可以非常容易的嵌入C和C++工程中,也比较容易与C和C++互动,这也是目前Lua主要的用法。
ngx_lua:一个nginx很重要的第三方模块,作者:章亦春(agentzh、春哥),结合了nginx和Lua各自优点,把Lua嵌入nginx中,使其支持Lua来快速开发基于nginx下的业务逻辑。
https://github.com/openresty/lua-nginx-module

2. 安装

2.1、LuaJIT

wget -c http://luajit.org/download/LuaJIT-2.0.4.tar.gz
tar xzvf LuaJIT-2.0.4.tar.gz
cd LuaJIT-2.0.4
make install PREFIX=/usr/local/luajit
#注意环境变量!
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.0

2.2、Tengine

tengine最新代码中已经包含lua模块了,直接git clone下来就可以

git clone https://github.com/alibaba/tengine.git
cd tengine
./configure --prefix=/opt/tengine --with-http_lua_module
make
make install

如果是原生nginx的话,得自行下载lua模块代码:

wget http://nginx.org/download/nginx-1.7.8.tar.gz
tar xvf nginx-1.7.8.tar.gz
cd nginx-1.7.8
mkdir modules
cd modules
git clone https://github.com/openresty/lua-nginx-module.git
cd ..
./configure --prefix=/opt/nginx --add-module=./modules/lua-nginx-module/
make
make install

3. 运行

修改/opt/tengine/conf/nginx.conf:

worker_processes  1;

error_log  logs/error.log;
pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }

        location /hello_lua {
            content_by_lua '
                ngx.say("Lua: hello world!")
            ';
        }
    }
}

运行tengine:

root@j9 ~/tengine# /opt/tengine/sbin/nginx

curl访问一下hello_lua:

root@j9 ~/tengine# curl http://localhost/hello_lua
Lua: hello world!

运行ok。

4、开发

语法
入门
深入

4.1、语法

参考:
Lua简明教程
Lua在线lua学习教程

4.2、入门

4.2.1、API

  • ngx.print
    输出响应内容体;
    例如:ngx.print("a", "b", "c")
  • ngx.say
    跟ngx.print的区别只是最后会多输出一个换行符;
    例如:ngx.say("a", "b", "c")
  • ngx.status
    设置响应HTTP状态码;
    注意,设置状态码仅在响应头发送前有效。当调用ngx.say或者ngx.print时自动发送响应状态码(默认为200);可以通ngx.headers_sent来判断是否发送了响应状态码。
    例如:ngx.status = 200
  • ngx.exit
    设置响应HTTP状态码并退出;
    注意,设置状态码仅在响应头发送前有效,并且该函数调用之后该函数后面的lua将被忽略掉,因为已经exit了。
    例如:ngx.exit(200)
  • ngx.header
    输出响应头;
    注意,头部字段中含有横杠(-)的要转换成下划线(_),ngx_lua模块自动将_转换成-。
    例如:ngx.header["X-Cache"] = "HIT" 或者 ngx.header.X_Cache = "HIT"或者ngx.header.X_Cache = {"AA", "BB"}
  • ngx.redirect
    301或者302重定向
    例如:ngx.redirect("http://www.taobao.org", 301)
  • ngx.log
    打印nginx错误日志,日志级别有:ngx.STDERR、ngx.EMERG、ngx.ALERT、ngx.CRIT、ngx.ERR、ngx.WARN、ngx.NOTICE、ngx.INFO、ngx.DEBUG
    例如:ngx.log(ngx.ERR, "test: ", "ok")

例子:

server {
        listen       9898;
        location / {
            default_type "text/html";
            content_by_lua '
                local headers_sent_1 = ngx.headers_sent
                ngx.header["X-Cache"] = "HIT"
                ngx.header.Y_Cache = "MISS"
                ngx.header.Z_Cache = {"AA", "BB"}
                ngx.status = 602
                local headers_sent_2 = ngx.headers_sent
                ngx.print("a", "b")
                local headers_sent_3 = ngx.headers_sent
                ngx.say("c", "d")
                ngx.say("e", "f")
                ngx.say("headers_sent_1: ", tostring(headers_sent_1))
                ngx.say("headers_sent_2: ", tostring(headers_sent_2))
                ngx.say("headers_sent_3: ", tostring(headers_sent_3))
                ngx.log(ngx.ERR, "ngx.log test ok")
                ngx.exit(601)
                ngx.say("g", "h")
            ';
        } 

        location ^~ /redirect {
            content_by_lua '
                ngx.redirect("http://www.taobao.org", 301)
            ';
        }
    }

测试结果:

root@j9 ~# curl "http://127.0.0.1:9898/" -i
HTTP/1.1 602
Server: Tengine/2.2.0
Date: Mon, 19 Oct 2015 16:10:42 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
X-Cache: HIT
Y-Cache: MISS
Z-Cache: AA
Z-Cache: BB

abcd
ef
headers_sent_1: false
headers_sent_2: false
headers_sent_3: true
root@j9 ~# curl "http://127.0.0.1:9898/redirect" -i
HTTP/1.1 301 Moved Permanently
Server: Tengine/2.2.0
Date: Mon, 19 Oct 2015 16:18:16 GMT
Content-Type: text/html
Content-Length: 284
Connection: keep-alive
Location: http://www.taobao.org

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<h1>301 Moved Permanently</h1>
<p>The requested resource has been assigned a new permanent URI.</p>
<hr/>Powered by Tengine/2.2.0</body>
</html>
root@j9 ~#
  • ngx.var
    读取nginx变量,如nginx变量为$a,则在Lua中通过ngx.var.a获取,也可以给nginx变量赋值如ngx.var.a = "aa",前提是该变量在nginx中必须存在,不能在Lua中创建nginx变量。另外,对于nginx location中使用正则捕获的捕获组可以使用ngx.var[捕获组数字]获取。

例子

server {
        listen       9898;
        location ~ /var/([^/])/([^/]) {
            default_type "text/html";
            set $a "aaa";
            set $b $host;
            content_by_lua '
                ngx.say("$a: ", ngx.var.a)
                ngx.say("$b: ", ngx.var.b)
                ngx.say("$host: ", ngx.var.host)
                ngx.say("$arg_id: ", ngx.var.arg_id)
                ngx.say("$1: ", ngx.var[1])
                ngx.say("$2: ", ngx.var[2])
            ';
        }
    }

测试结果:

root@j9 ~# curl "http://127.0.0.1:9898/var/aaaa/bbbb?id=22" -H "Host: www.taobao.org"
$a: aaa
$b: www.taobao.org
$host: www.taobao.org
$arg_id: 22
$1: aaaa
$2: bbbb
root@j9 ~#
  • ngx.req.raw_header
    未解析的请求头字符串;
    例如:ngx.req.raw_header()
  • ngx.req.get_headers
    获取请求头,默认只获取前100个头部,如果想要获取所有头部可以调用ngx.req.get_headers(0);获取带中划线的请求头时要把中划线转换成下划线使用如headers.user_agent这种方式;如果一个请求头有多个值,则返回的是table;
    例如:ngx.req.get_headers()
  • ngx.req.get_uri_args
    获取url请求参数,其用法与ngx.req.get_headers类似;
  • ngx.req.get_post_args
    获取post请求body参数,其用法与ngx.req.get_uri_args类似,但必须提前调用ngx.req.read_body();
  • ngx.req.read_body
    如果要获取请求的body,则需要调用ngx.req.read_body(),否则获取不到body数据,(ps:也可以在nginx配置文件中加入指令lua_need_request_body on;来开启读取body,但官方不推荐)
  • ngx.req.discard_body
    忽略请求的body
    注意,如果处理一个包含body的请求且需要ngx.exit时,需要调用此函数来忽略body,否则nginx可能将body当成header来解析,从而导致400错误;
  • ngx.req.get_body_data
    获取请求body数据

例子

location ^~ /req {
    content_by_lua '
        ngx.say("===========ngx.req.raw_header=")
        ngx.say(ngx.req.raw_header())
        local headers = ngx.req.get_headers()
        ngx.say("===========headers============")
        ngx.say("Host: ", headers["Host"])
        ngx.say("user-agent: ", headers.user_agent)
        ngx.say("===========all headers========")
        for k,v in pairs(headers) do
          if type(v) == "table" then
            ngx.say(k, ": ", table.concat(v, ","))
          else
            ngx.say(k, ": ", v)
          end
        end

        ngx.say("===========args===============")
        local args = ngx.req.get_uri_args()
        for k,v in pairs(args) do
          ngx.say(k, ": ", v)
        end
        ngx.say("===========body===============")
        ngx.say("body data: ", ngx.req.get_body_data())
        ngx.req.read_body()
        local post_args = ngx.req.get_post_args()
        for k,v in pairs(post_args) do
          ngx.say(k, ": ", v)
        end
        ngx.say("body data: ", ngx.req.get_body_data())
    ';
}

测试结果:

root@j9 ~# curl "http://127.0.0.1:9898/req?a=11&b=22&c=33" --data "d=11&e=22&f=33"
===========ngx.req.raw_header=
POST /req?a=11&b=22&c=33 HTTP/1.1
User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
Host: 127.0.0.1:9898
Accept: /
Content-Length: 14
Content-Type: application/x-www-form-urlencoded

===========headers============
Host: 127.0.0.1:9898
user-agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
===========all headers========
host: 127.0.0.1:9898
content-type: application/x-www-form-urlencoded
accept: /
content-length: 14
user-agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
===========args===============
b: 22
a: 11
c: 33
===========body===============
body data: nil
d: 11
f: 33
e: 22
body data: d=11&e=22&f=33
root@j9 ~#
  • ngx.escape_uri/ngx.unescape_uri
    uri编码解码
  • ngx.encode_args/ngx.decode_args
    参数编码解码
  • ngx.encode_base64/ngx.decode_base64
    BASE64编码解码
  • ngx.md5
    md5加密

例子

location ^~ /code {
    content_by_lua '
        local request_uri = ngx.var.request_uri
        local args = {a=11, b=22}
        ngx.say("request uri: ", request_uri)
        ngx.say("unescape request uri: ", ngx.unescape_uri(request_uri))
        ngx.say("encode args: ", ngx.encode_args(args))
        ngx.say("encode base64 request uri: ", ngx.encode_base64(request_uri))
        ngx.say("md5(123456): ", ngx.md5("123456"))
    ';
}

测试结果:

root@j9 ~# curl "http://127.0.0.1:9898/code?name=%E9%87%91%E4%B9%9D"
 request uri: /code?name=%E9%87%91%E4%B9%9D
unescape request uri: /code?name=金九
encode args: a=11&b=22
encode base64 request uri: L2NvZGU/bmFtZT0lRTklODclOTElRTQlQjklOUQ=
md5(123456): e10adc3949ba59abbe56e057f20f883e
 root@j9 ~#
  • ngx.shared.DICT

    共享内存接口,其中DICT为共享内存zone名称,在nginx.conf中通过指令lua_shared_dict配置,而且lua_shared_dict指令配置的共享内存大小最小值为8k。

例子

lua_shared_dict cc_shared_data 16k;

    server {
        listen       9999;
        default_type "text/html";
        location ^~ /shared_data {
            content_by_lua '
                local shared_data = ngx.shared.cc_shared_data
                local i = shared_data:get("i")
                if not i then
                  shared_data:set("i", 1)
                end
                i = shared_data:incr("i", 1)
                ngx.say("i: ", i)
            ';
        }
    }

测试结果

root@j9 ~# curl "http://127.0.0.1:9999/shared_data"
i: 2
root@j9 ~# curl "http://127.0.0.1:9999/shared_data"
i: 3
root@j9 ~#

ngx.shared.DICT详细说明:http://wiki.nginx.org/HttpLuaModule#ngx.shared.DICT

4.2.2、指令

指令 阶段 范围 说明
init_by_lua/init_by_lua_file loading-config http nginx master进程加载配置时执行;通常用于初始化全局配置/预加载Lua模块
init_worker_by_lua/init_worker_by_lua_file starting-worker http 每个nginx worker进程启动时调用的计时器,如果master进程不允许则只会在init_by_lua之后调用;通常用于定时拉取配置/数据,或者后端服务的健康检查
set_by_lua/set_by_lua_file rewrite server,server if,location,location if 设置nginx变量,可以实现复杂的赋值逻辑;此处是阻塞的,Lua代码要做到非常快
rewrite_by_lua/rewrite_by_lua_file rewrite tail http,server,location,location if rewrite 阶段处理,可以实现复杂的转发/重定向逻辑
access_by_lua/access_by_lua_file access tail http,server,location,location if 请求访问阶段处理,用于访问控制
content_by_lua/content_by_lua_file content location,location if 内容处理器,接收请求处理并输出响应
header_filter_by_lua/header_filter_by_lua_file output-header-filter http,server,location,location if 设置header和cookie
body_filter_by_lua/body_filter_by_lua_file output-body-filter http,server,location,location if 对响应数据进行过滤,比如截断、替换
log_by_lua/log_by_lua_file log http,server,location,location if log 阶段处理,比如记录访问量/统计平均响应时间

更详细的解释请参考官网:http://wiki.nginx.org/HttpLuaModule#Directives

init_by_lua

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

例子:

init_by_lua '
        cjson = require("cjson")
        ngx.log(ngx.ERR, "init_by_lua ok")
    ';

    server {
        listen       9292;
        default_type "text/html";
        location / {
            content_by_lua '
                local arg_json = cjson.decode(ngx.var.arg_json)
                ngx.say("aa: ", arg_json.aa)
            ';
        }
    }

测试结果:

root@j9 ~# curl 'http://127.0.0.1:9292/?json=\{"aa":111,"bbb":222\}'
aa: 111
root@j9 ~#

init_worker_by_lua

每个worker启动之后初始化时执行,通常用于每个worker都要做的工作,比如启动定时任务

例子:

worker_processes  2;
http {
    #这里省略了其他配置
    init_worker_by_lua '
        ngx.log(ngx.ERR, "test init_worker_by_lua")
        -- TODO: 启动定时任务
    ';
}

grep一下error.log,会发现两条包含"test init_worker_by_lua"关键字的log,说明每个worker都会执行这个Lua代码。

set_by_lua

语法:set_by_lua resluascriptstr

arg1 $arg2...; 在Lua代码中可以实现所有复杂的逻辑,但是要执行速度很快,不要阻塞;
需要注意的是,这个指令需要加入模块ngx_devel_kit,否则不支持这个指令。

这个指令的Lua代码中不支持以下API:
1、输出(ngx.say、ngx.send_headers……)
2、控制(ngx.exit……)
3、子请求(ngx.location.capture、ngx.location.capture_multi……)
4、cosocket(ngx.socket.tcp、ngx.req.socket……)
5、ngx.sleep

例子:

server {
    listen       9393;
    default_type "text/html";
    location /add {
        set $diff '';
        set $double_c '';

        set_by_lua $sum '
            local a = ngx.var.arg_a
            local b = ngx.var.arg_b
            ngx.var.diff = a - b
            ngx.var.double_c = 2 * tonumber(ngx.arg[1])
            return a + b;
        ' $arg_c;
        return 200 "a + b = $sum, a - b = $diff, 2 * c = $double_c";
    }
}

测试结果:

root@j9 ~# curl "http://127.0.0.1:9393/add?a=11&b=22&c=88"
a + b = 33, a - b = -11, 2 * c = 176
root@j9 ~#

rewrite_by_lua

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

需要注意的是:
1、在长连接中如果调用了ngx.exit(200)一个请求,则需要调用ngx.req.discard_body(),否则nginx可能会把当前请求的body当成header解析,从而导致400错误返回码并且长连接被关闭。
2、如果该阶段调用了ngx.exit(ngx.OK),content_by_lua阶段仍然能得到执行。

例子:

server {
    listen       9494;
    default_type "text/html";
    location /rewrite_by_lua {
        set $a 11;
        rewrite_by_lua '
            ngx.var.a = "aa"
            if ngx.var.arg_exit == "ok" then
              ngx.exit(ngx.OK)
            else
              ngx.exit(200)
            end
        ';
        content_by_lua '
            ngx.say("a: ", ngx.var.a)
        ';
    }
}

测试结果

root@j9 ~# curl "http://127.0.0.1:9494/rewrite_by_lua?exit=ok"
a: aa

root@j9 ~# curl "http://127.0.0.1:9494/rewrite_by_lua?exit=200"

root@j9 ~# 

access_by_lua

用于访问控制,比如IP黑白名单限制、鉴权。

例子:

server {
    listen 9595;
    default_type "text/html";
    location / {
        access_by_lua '
            local auth = ngx.var.arg_auth;
            local key = "alicdnj9";
            if ngx.md5(key) ~= auth then
                return ngx.exit(403)
            end
        ';
        content_by_lua '
            ngx.say("access ok")
        ';
    }
}

测试结果:

root@j9 ~# curl "http://127.0.0.1:9595/?auth=xx"
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<h1>403 Forbidden</h1>
<p>You don't have permission to access the URL on this server. Sorry for the inconvenience.<br/>
Please report this message and include the following information to us.<br/>
Thank you very much!</p>
<table>
<tr>
<td>URL:</td>
<td>http://127.0.0.1:9595/?auth=xx</td>
</tr>
<tr>
<td>Server:</td>
<td>j9</td>
</tr>
<tr>
<td>Date:</td>
<td>2015/10/27 16:47:20</td>
</tr>
</table>
<hr/>Powered by Tengine/2.2.0</body>
</html>
root@j9 ~# echo -n alicdnj9 | md5sum
50652c84270d22210593318f5d3016a1  -
root@j9 ~# curl "http://127.0.0.1:9595/?auth=50652c84270d22210593318f5d3016a1"
access ok
root@j9 ~#

注意,如果在access_by_lua中调用ngx.exit(ngx.OK),content阶段仍然能得到执行。

content_by_lua

content阶段,注意在同一个Location中不要和其他content阶段指令一起使用,比如proxy_pass。
例子:略

header_filter_by_lua和body_filter_by_lua

分别为header_filter阶段和body_filter阶段,其中body_filter可能会被执行多次。

不支持以下API:

  1. 输出 (ngx.say、ngx.send_headers)
  2. 控制 (ngx.exit、ngx.exec)
  3. 子请求 (ngx.location.capture、ngx.location.capture_multi)
  4. Cosocket (ngx.socket.tcp、ngx.req.socket).

比如对后端chunked长度做限制:

server {
    listen 9696;
    default_type "text/html";
    set $content_len 0;
    location / {
        header_filter_by_lua '
            -- 先去掉Content-Length头部,转成Chunked传输
            ngx.header.content_length = nil
        ';
        body_filter_by_lua '
            local content_length = #ngx.arg[1]
            content_length = ngx.var.content_len + content_length
            ngx.var.content_len = content_length
            -- 最多只能传输10字节的body,否则直接关掉连接
            if content_length > 10 then
                return ngx.ERROR
            end
        ';
        content_by_lua '
            for i=1, ngx.var.arg_len do
                ngx.print("a")
            end
        ';
    }
}

测试结果

root@j9 ~# curl "http://127.0.0.1:9696/?len=10" -i
HTTP/1.1 200 OK
Server: Tengine/2.2.0
Date: Mon, 26 Oct 2015 01:48:23 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive

aaaaaaaaaa
root@j9 ~# curl "http://127.0.0.1:9696/?len=11" -i
curl: (52) Empty reply from server
root@j9 ~#

可以看出当参数len为11时,服务器就直接不返回数据了。

4.3、深入

1、content_by_lua中的代码一定要注意单引号或者双引号,一般用法是外单内双,或者外双内单。

2、在nginx_lua中值为nil的变量不能与字符串或者数字相加,否则nginx会报500错误。

3、lua调试: ngx.log(ngx.ERR,xx)。(tail -f logs/error.log)

4、*_by_lua_file指令指定的文件支持绝对路径和相对路径,其中相对路径是相对nginx工作目录。

5、lua文件的require函数指定的lua模块路径查找顺序,可以从出错信息中看出来:

no file '/opt/libs/lua/a.lua'
no file './a.lua'
no file '/usr/local/luajit/share/luajit-2.0.4/a.lua'
no file '/usr/local/share/lua/5.1/a.lua'
no file '/usr/local/share/lua/5.1/a/init.lua'
no file '/usr/local/luajit/share/lua/5.1/a.lua'
no file '/usr/local/luajit/share/lua/5.1/a/init.lua'
no file './a.so'
no file '/usr/local/lib/lua/5.1/a.so'
no file '/usr/local/luajit/lib/lua/5.1/a.so'
no file '/usr/local/lib/lua/5.1/loadall.so'

其中,第一个/opt/libs/lua/a.lua为lua_package_path指定的路径:lua_package_path '/opt/libs/lua/?.lua;;';
第二个./a.lua为相对路径,相对于nginx.conf配置文件,而非包含它的lua文件。
so模块查找顺序类似,但是先查找.lua再查找.so,查找.so时先在lua_package_cpah指定的路径查找:lua_package_cpath '/opt/libs/lua_shared/?.so;;';
可以从出错信息中看出来:

no field package.preload['a']
no file '/opt/libs/lua/a.lua'
no file './a.lua'
no file '/usr/local/luajit/share/luajit-2.0.4/a.lua'
no file '/usr/local/share/lua/5.1/a.lua'
no file '/usr/local/share/lua/5.1/a/init.lua'
no file '/usr/local/luajit/share/lua/5.1/a.lua'
no file '/usr/local/luajit/share/lua/5.1/a/init.lua'
no file '/opt/libs/lua_shared/a.so'
no file './a.so'
no file '/usr/local/lib/lua/5.1/a.so'
no file '/usr/local/luajit/lib/lua/5.1/a.so'
no file '/usr/local/lib/lua/5.1/loadall.so'

6、lua代码一定要健壮,否则不管lua产生什么错,nginx都会返回500错误,这时可以从error.log中查看错误信息来定位。

7、编写lua代码时最好用local局部变量,不要用全局变量。

8、实现worker级别的全局变量:

server {
    listen 9797;
    default_type "text/html";
    location / {
        content_by_lua '
            local a = 1
            local b = {b = 1}
            local status = require("status")
            ngx.say("a: ", a, ", b: ", b.b, " counter: ", status.counter)
            a = a + 1
            b.b = b.b + 1
            status.counter = (status.counter or 0) + 1
        ';
    }
}

其中status.lua为:

local m = {}

m.counter = 1

return m

测试结果:

root@j9 ~# curl "http://127.0.0.1:9797/"
a: 1, b: 1 counter: 1
root@j9 ~# curl "http://127.0.0.1:9797/"
a: 1, b: 1 counter: 2
root@j9 ~# curl "http://127.0.0.1:9797/"
a: 1, b: 1 counter: 3
root@j9 ~#

可以看出status.counter的值一直是累加的,这是因为require一个模块只load第一次,后续require该模块都会先看全局表中是否已经load过,load过则就不需要再load了,所以status.counter累加其实是累加m.counter。

9、定时任务

API: ok, err = ngx.timer.at(delay, callback, user_arg1, user_arg2, ...)
例子:

local delay = 5
local handler
handler = function (premature)
    -- do some routine job in Lua just like a cron job
    if premature then
        return
    end
    local ok, err = ngx.timer.at(delay, handler)
    if not ok then
        ngx.log(ngx.ERR, "failed to create the timer: ", err)
        return
    end
end

local ok, err = ngx.timer.at(delay, handler)
if not ok then
    ngx.log(ngx.ERR, "failed to create the timer: ", err)
    return
end

注意:在timer处理函数的上下文中不能调用ngx.var.、ngx.req.、子请求API、输出API,因为这些API只能在请求上下文中生效。

10、子请求

API:res = ngx.location.capture(uri, options?)

上下文:rewrite_by_lua, access_by_lua, content_by_lua*

例子:
正向代理,当源站返回301或者302时代理客户端跳转

server {
    listen 8181;
    default_type "text/html";
    location /test {
        content_by_lua '
            local res = ngx.location.capture("/get" .. ngx.var.request_uri, { method = ngx.HTTP_HEAD })
            if res.status == 200 then
                ngx.exec("/get" .. ngx.var.request_uri)
            elseif res.status == 301 or res.status == 302 then
                location = res.header["Location"]
                local m, err = ngx.re.match(location, "http://([^/]+)(/.*)")
                if not m then
                    ngx.exit(500)
                end
                host = m[1]
                uri = m[2]
                ngx.exec("/redirect/" .. host .. "/" .. ngx.var.request_uri)
            else
                ngx.exit(res.status)
            end
        ';
    }

    location ~ /redirect/([^/])/([^/]) {
        proxy_pass http://$1/$2?$args;
    }

    location /get {
        if ($arg_tag = "1") {
            return 302 "http://127.0.0.1:8282/$request_uri";
        }

        return 200 "ok";
    }
}

server {
    listen 8282;
    default_type "text/html";
    location / {
        return 200 "redirect ok, args: $args";
    }
}

测试结果:

root@j9 ~# curl "http://127.0.0.1:8181/test?tag=0" -i
HTTP/1.1 200 OK
Server: Tengine/2.2.0
Date: Mon, 26 Oct 2015 15:17:10 GMT
Content-Type: text/html
Content-Length: 2
Connection: keep-alive

ok
root@j9 ~# curl "http://127.0.0.1:8181/test?tag=1" -i
HTTP/1.1 200 OK
Server: Tengine/2.2.0
Date: Mon, 26 Oct 2015 15:17:14 GMT
Content-Type: text/html
Content-Length: 19
Connection: keep-alive

redirect ok, args: tag=1
root@j9 ~#

可见,当传tag为1时,返回的值就是想要的值,不需要再302重定向了。

注意,子请求只能请求本server的非@location。
另外一个需要注意的是,发起子请求之前修改的变量在子请求的location中是获取不到的,这是因为变量的上下文是在请求结构体r中,而子请求是挂在主请求下面,是两个不同的请求。
实验:

server {
    listen 8383;

    default_type "text/html";
    set $cc "cc";
    location /test {
        content_by_lua '
            ngx.var.cc = "11"
            local res = ngx.location.capture("/get" .. ngx.var.request_uri)
            if res.status == 200 then
                ngx.say(res.body)
                ngx.say("test cc: ", ngx.var.cc)
            else
                ngx.exit(res.status)
            end
        ';
    }

    location /get {
        content_by_lua '
            ngx.say("get cc: ", ngx.var.cc)
            ngx.var.cc = "22"
        ';
    }
}

结果:

root@j9 ~# curl "http://127.0.0.1:8383/test"
get cc: cc

test cc: 11

root@j9 ~#

11、location @xx

server {
    listen 8484;
    default_type "text/html";
    set $cc "2";
    location / {
        content_by_lua '
            ngx.var.cc = "5";
            if ngx.var.arg_location == "at" then
                ngx.exec("@cc")
            else
                ngx.exec("/cc")
            end
        ';
    }

    location @cc {
        return 200 "this is @cc location, cc: $cc";
    }
    location /cc {
        return 200 "this is /cc location, cc: $cc";
    }
}

测试结果:

root@j9 ~# curl "http://127.0.0.1:8484/?location=at"
this is @cc location, cc: 5
root@j9 ~# curl "http://127.0.0.1:8484/"
this is /cc location, cc: 2
root@j9 ~#

在ngx.exec跳转之前已经把变量cc的值改成5了,但可以看出这两种跳转方式变量cc的值不一样,这是因为ngx.exec跳转到@cc这个location时,从location rewrite阶段开始执行,而跳转到/cc这个location时是从server rewrite阶段开始执行,而set指令是在server块,就是在这个阶段得到执行的,所以$cc又被赋值成2了。

时间: 2024-10-28 05:23:39

【技术干货】听阿里云CDN安防技术专家金九讲tengine+lua开发的相关文章

【技术干货】听阿里云CDN安防技术专家金九讲SystemTap使用技巧

1.简介 SystemTap是一个Linux非常有用的调试(跟踪/探测)工具,常用于Linux 内核或者应用程序的信息采集,比如:获取一个函数里面运行时的变 量.调用堆栈,甚至可以直接修改变量的值,对诊断性能或功能问题非 常有帮助.SystemTap提供非常简单的命令行接口和很简洁的脚本语 言,以及非常丰富的tapset和例子. 2.何时使用 定位(内核)函数位置 查看函数被调用时的调用堆栈.局部变量.参数 查看函数指针变量实际指的是哪个函数 查看代码的执行轨迹(哪些行被执行了) 查看内核或者进

【满屏干货】阿里云CDN HTTPS最佳实践汇总

(一)动态证书 了解阿里云 CDN 架构的朋友应该知道,阿里云 CDN 7层的接入组件是 Tengine,我们知道 Tengine 原生是支持 SSL 的,只需要在配置文件中配置证书和私钥即可.在 CDN HTTPS 产品化以前,要开通 HTTPS 的域名需要把证书私钥给我们,我们在 Tengine 静态配置中配置,然后再同步到所有 CDN 边缘节点,显然这种方式在越来越多的域名开通 HTTPS 后,Tengine 静态配置文件会越来越大,难以管理,同时也会导致 Tengine reload 变

【技术干货】阿里云构建千万级别架构演变之路

本文作者:乔锐杰,现担任上海驻云信息科技有限公司运维总监/架构师.曾任职过黑客讲师.java软件工程师/网站架构师.高级运维.阿里云架构师等职位.维护过上千台服务器,主导过众安保险.新华社等千万级上云架构.在云端运维.分布式集群架构等方面有着丰富的经验. 前言     一个好的架构是靠演变而来,而不是单纯的靠设计.刚开始做架构设计,我们不可能全方位的考虑到架构的高性能.高扩展性.高安全等各方面的因素.随着业务需求越来越多.业务访问压力越来越大,架构不断的演变及进化,因而造就了一个成熟稳定的大型架

【2016快乐】独家12篇技术干货:阿里云、微博、搜索、美团、美丽说、推荐、小米、支付宝...

特别策划第一集: 12篇实战经验所组成的"近战"系列(文章.PDF和视频),包含新浪微博.淘宝搜索.美团.美丽说.淘宝推荐.小米.支付宝.阿里云.淘宝无线在内,涵盖建模.个性化推荐.排序学习.系统优化.数据监控.流量优化.架构探索等多方面一线经验总结. 1.<手机淘宝推荐中的排序学习>  2.<基于微博用户关系和行为的用户建模>  3.<移动电商搜索个性化技术>  4.<美团生活服务个性化推荐实践>  5.<美丽说个性化推荐技术&g

【大数据技术干货】阿里云伏羲(fuxi)调度器FuxiMaster功能简介(三) 针对在线服务的资源强稳定

免费开通大数据服务:https://www.aliyun.com/product/odps 转载自xingbao 各位好,这是介绍阿里云伏羲(fuxi)调度器系列文章的第三篇,今天主要介绍针对在线服务的资源强稳定 一.FuxiMaster简介 FuxiMaster和Yarn非常相似,定位于分布式系统中资源管理与分配的角色:一个典型的资源分配流程图如下所示:  作为调度器,目前FuxiMaster支持的功能主要有: 1.多租户管理 2.支持FIFO/FAIR调度策略 3.针对在线服务保持资源强稳定

【技术干货】驻云前端工程师带你初步了解chrome插件的开发

.. 本文作者:上海驻云Web前端工程师                  (只爱扎啤跟撸串的骚年)        施  翔 以下正文 ​ 随着互联网行业的崛起,网络已经成为人们生活工作时不可缺少的一部分,最直接的接触互联网的方式就是通过浏览器上网.随之而来的是用户对浏览器提出了越来越高的要求,换而言之就是对 web 的需求不断提升,单纯的浏览器已经不够满足某些场景的需要. 为了扩展浏览器的功能,形形色色的浏览器插件应运而生.比如 ie 各种内置搜索引擎,firefox 的 firebug,ch

阿里云 CDN HTTPS 最佳实践系列——动态证书(一)

背景 了解阿里云 CDN 架构的朋友应该知道,阿里云 CDN 7层的接入组件是 Tengine,我们知道 Tengine 原生是支持 SSL 的,只需要在配置文件中配置证书和私钥即可.在 CDN HTTPS 产品化以前,要开通 HTTPS 的域名需要把证书私钥给我们,我们在 Tengine 静态配置中配置,然后再同步到所有 CDN 边缘节点,显然这种方式在越来越多的域名开通 HTTPS 后,Tengine 静态配置文件会越来越大,难以管理,同时也会导致 Tengine reload 变得很慢,这

阿里云湖北区域服务提供商:阿里云CDN功能介绍

阿里云湖北区域服务提供商官方唯一指定武汉捷讯技术.关于阿里云CDN功能介绍如下: 阿里云CDN(内容分发网络)全称是AlibabaCloudContentDeliveryNetwork,建立并覆盖在承载网之上.由分布在不同区域的边缘节点服务器群组成的分布式网络,替代传统以WEBServer为中心的数据传输模式.将源内容发布到边缘节点,配合精准的调度系统:将用户的请求分配至最适合他的节点,使用户可以以最快的速度取得他所需的内容,有效解决Internet网络拥塞状况,提高用户访问的响应速度.使用CD

阿里云CDN直播架构与双11晚会直播实战

分享人:阿里云CDN直播 高级技术专家 阙寒分享内容:双11直播活动是众多直播活动中非常典型的场景,离不开直播本身这个话题.所以今天的分享会从直播概述.直播架构.业务功能.直播监控.双11这几个方面来进行. 每年双11购物节,有两块压力是最大的,第一块是支付宝交易,第二块就是CDN.那么CDN技术到底是什么呢? 举个简单例子,不管你的在电脑淘宝上还是手机APP上买东西,进入商品详情页,你会看到很多图片,有的商家为了更好的宣传,会上传视频,让你看得更形象.当商家把图片和视频上传到服务器时,一般是阿