alexdeblog 2017-08-07
摘要: 系统介绍tengine的安装、运行和开发,全文包含大量代码示例,由阿里云CDN安防技术专家金九撰写。
Tengine:轻量级、高性能、高并发、配置化、模块化、可扩展、可移植的Web和反向代理 服务器,Tengine是nginx超集,但做了很多优化,包含了很多比较有用的模块,比如直接包含了lua、proc等很有用的模块。
Lua:一个很轻量级的 脚本,也号称性能最高的 脚本。代码总共不到600k,32个C文件,23个头文件:
可以非常容易的嵌入C和C++工程中,也比较容易与C和C++互动,这也是目前Lua主要的用法。
ngx_lua:一个nginx很重要的第三方模块,作者:章亦春(agentzh、春哥),结合了nginx和Lua各自优点,把Lua嵌入nginx中,使其支持Lua来快速开发基于nginx下的业务逻辑。
https://github.com/openresty/lua-nginx-module
2.1、LuaJIT
2.2、Tengine
tengine最新代码中已经包含lua模块了,直接git clone下来就可以
如果是原生nginx的话,得自行下载lua模块代码:
修改/opt/tengine/conf/nginx.conf:
运行tengine:
root@j9 ~/tengine# /opt/tengine/sbin/nginx
curl访问一下hello_lua:
root@j9 ~/tengine# curl http://localhost/hello_luaLua: hello world!
运行ok。
语法
入门
深入
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")
例子:
测试结果:
ngx.var 读取nginx变量,如nginx变量为$a,则在Lua中通过ngx.var.a获取,也可以给nginx变量赋值如ngx.var.a = "aa",前提是该变量在nginx中必须存在,不能在Lua中创建nginx变量。另外,对于nginx location中使用正则捕获的捕获组可以使用ngx.var[捕获组数字]获取。
例子
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数据
例子
测试结果:
ngx.escape_uri/ngx.unescape_uri
uri编码解码
ngx.encode_args/ngx.decode_args
参数编码解码
ngx.encode_base64/ngx.decode_base64
BASE64编码解码
ngx.md5
md5加密
例子
ngx.shared.DICT
共享内存接口,其中DICT为共享内存zone名称,在nginx.conf中通过指令lua_shared_dict配置,而且lua_shared_dict指令配置的共享内存大小最小值为8k。
例子
ngx.shared.DICT详细说明:http://wiki.nginx.org/HttpLuaModule#ngx.shared.DICT
4.2.2、指令
init_by_lua
每次nginx重新加载配置时执行,可以用它来完成一些耗时模块的加载,或者初始化一些全局配置;
例子:
init_worker_by_lua
每个worker启动之后初始化时执行,通常用于每个worker都要做的工作,比如启动定时任务
例子:
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
例子:
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阶段仍然能得到执行。
例子:
用于访问控制,比如IP黑白名单限制、鉴权。
例子:
测试结果:
注意,如果在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:
输出 (ngx.say、ngx.send_headers)
控制 (ngx.exit、ngx.exec)
子请求 (ngx.location.capture、ngx.location.capture_multi)
Cosocket (ngx.socket.tcp、ngx.req.socket).
比如对后端chunked长度做限制:
测试结果
可以看出当参数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模块路径查找顺序,可以从出错信息中看出来:
其中,第一个/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;;';
可以从出错信息中看出来:
6、lua代码一定要健壮,否则不管lua产生什么错,nginx都会返回500错误,这时可以从error.log中查看错误信息来定位。
7、编写lua代码时最好用local局部变量,不要用全局变量。
8、实现worker级别的全局变量:
可以看出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, ...)
例子:
注意:在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时代理客户端跳转
可见,当传tag为1时,返回的值就是想要的值,不需要再302重定向了。
注意,子请求只能请求本server的非@location。
另外一个需要注意的是,发起子请求之前修改的变量在子请求的location中是获取不到的,这是因为变量的上下文是在请求结构体r中,而子请求是挂在主请求下面,是两个不同的请求。
实验:
11、location @xx
在ngx.exec跳转之前已经把变量cc的值改成5了,但可以看出这两种跳转方式变量cc的值不一样,这是因为ngx.exec跳转到@cc这个location时,从location rewrite阶段开始执行,而跳转到/cc这个location时是从server rewrite阶段开始执行,而set指令是在server块,就是在这个阶段得到执行的,所以$cc又被赋值成2了。