OpenResty是一个基于Nginx+Lua的Web运行环境,它打包了标准的 Nginx 核心,很多的常用的第三方模块,以及它们的大多数依赖项。OpenResty可以用来实现高并发的动态Web应用
Open 取自“开放”之意,而Resty便是 REST 风格的意思
OpenResty使用的Lua版本是5.1,不使用更新版本的原因是5.2+版本的Lua API和C API都不兼容于5.1。
自从 OpenResty 1.5.8.1 版本之后,默认捆绑的 Lua 解释器就被替换成了 LuaJIT,而不再是标准 Lua。
wget https://openresty.org/download/openresty-1.13.6.1.tar.gz
tar xzf openresty-1.13.6.1.tar.gzcd openresty-1.13.6.1/
./configure --prefix=/home/alex/Lua/openresty/1.13.6 \
# 启用LuaJIT,这是一个Lua的JIT编译器,默认没有启用
--with-luajit \
# 使用Lua 5.1标准解释器,不推荐,应该尽可能使用LuaJIT
--with-lua51 \
# Drizzle、Postgres、 Iconv这几个模块默认没有启用
--with-http_drizzle_module、--with-http_postgres_module --with-http_iconv_module
# Nginx 路径如下:# nginx path prefix: "/home/alex/Lua/openresty/1.13.6/nginx"
# nginx binary file: "/home/alex/Lua/openresty/1.13.6/nginx/sbin/nginx"
# nginx modules path: "/home/alex/Lua/openresty/1.13.6/nginx/modules"
# nginx configuration prefix: "/home/alex/Lua/openresty/1.13.6/nginx/conf"
# nginx configuration file: "/home/alex/Lua/openresty/1.13.6/nginx/conf/nginx.conf"
# nginx pid file: "/home/alex/Lua/openresty/1.13.6/nginx/logs/nginx.pid"
# nginx error log file: "/home/alex/Lua/openresty/1.13.6/nginx/logs/error.log"
# nginx http access log file: "/home/alex/Lua/openresty/1.13.6/nginx/logs/access.log"
# nginx http client request body temporary files: "client_body_temp"
# nginx http proxy temporary files: "proxy_temp"
# nginx http fastcgi temporary files: "fastcgi_temp"
# nginx http uwsgi temporary files: "uwsgi_temp"
# nginx http scgi temporary files: "scgi_temp"
make -j8 && make install
一个OpenRestry工程,实际上就是对应了Nginx运行环境的目录结构。例如:
mkdir -p ~/Lua/projects/openrestry
cd ~/Lua/projects/openrestry
mkdir conf && mkdir logs
使用lua-nginx-module模块提供的指令,你可以嵌入Lua脚本到Nginx配置文件中,以生成响应内容:
daemon off;
worker_processes 1;
error_log stderr debug;
events {
worker_connections 1024;
}
http {
access_log /dev/stdout;
server {
listen 8080;
location / {
default_type text/html;
# lua-nginx-module模块,属于OpenResty项目,支持根据Lua脚本输出响应
content_by_lua_block {
ngx.say("hello, world
")
}
}
}
}
# 将Nginx运行时的前缀设置为上面的工程目录
~/Lua/openresty/1.13.6/nginx/sbin/nginx -p ~/Lua/projects/openrestry -c conf/nginx.conf
curl http://localhost:8080/
# hello, world
安装三个插件:
不同类型的指令,职责如下:
指令 | 说明 |
set_by_lua* | 流程分支处理判断变量初始化 |
rewrite_by_lua* | 转发、重定向、缓存等功能 |
access_by_lua* | IP 准入、身份验证、接口权限、解密 |
content_by_lua* | 内容生成 |
header_filter_by_lua* | 响应头部过滤处理,可以添加响应头 |
body_filter_by_lua* | 响应体过滤处理,例如转换响应体 |
log_by_lua* | 异步完成日志记录,日志可以记录在本地,还可以同步到其他机器 |
尽管仅使用单个阶段的指令content_by_lua*就可以完成以上职责,但是把逻辑划分在不同阶段,更加容易维护。
daemon off;
worker_processes 1;
error_log stderr debug;
events {
worker_connections 1024;
}
http {
access_log /dev/stdout;
# lua模块搜索路径
# 如果使用相对路径,则必须将Nginx所在目录作为工作目录,然后启动服务
# ${prefix}为Nginx的前缀目录,可以在启动Nginx时使用-p来指定
lua_package_path '$prefix/scripts/?.lua;;';
# 在开发阶段,可以设置为off,这样避免每次修改代码后都需要reload
# 生产环境一定要设置为on
lua_code_cache off;
server {
listen 80;
location ~ ^/api/([-_a-zA-Z0-9]+) {
# 在access阶段执行,进行合法性校验
access_by_lua_file scripts/auth-and-check.lua;
# 生成内容,API名称即为Lua脚本名称
content_by_lua_file scripts/$1.lua;
}
}
}
-- 黑名单
local black_ips = {["127.0.0.1"]=true}
-- 当前客户端IP
local ip = ngx.var.remote_addr
if true == black_ips[ip] then
-- 返回相应的HTTP状态码
ngx.exit(ngx.HTTP_FORBIDDEN)
end
变量 | 说明 | |
arg_name | 请求中的name参数 | |
args | 请求中的参数 | |
binary_remote_addr | 远程地址的二进制表示 | |
body_bytes_sent | 已发送的消息体字节数 | |
content_length | HTTP请求信息里的"Content-Length" | |
content_type | 请求信息里的"Content-Type" | |
document_root | 针对当前请求的根路径设置值 | |
document_uri | 与$uri相同; 比如 /test2/test.php | |
host | 请求信息中的"Host",如果请求中没有Host行,则等于设置的服务器名 | |
hostname | 机器名使用 gethostname系统调用的值 | |
http_cookie | Cookie信息 | |
http_referer | 引用地址 | |
http_user_agent | 客户端代理信息 | |
http_via | 最后一个访问服务器的Ip地址。 | |
http_x_forwarded_for | 相当于网络访问路径 | |
is_args | 如果请求行带有参数,返回“?”,否则返回空字符串 | |
limit_rate | 对连接速率的限制。此变量支持写入: Lua
|
|
nginx_version | 当前运行的nginx版本号 | |
pid | Worker进程的PID | |
query_string | 与$args相同 | |
realpath_root | 按root指令或alias指令算出的当前请求的绝对路径。其中的符号链接都会解析成真是文件路径 | |
remote_addr | 客户端IP地址 | |
remote_port | 客户端端口号 | |
remote_user | 客户端用户名,认证用 | |
request | 用户请求 | |
request_body | 这个变量(0.7.58+)包含请求的主要信息。在使用proxy_pass或fastcgi_pass指令的location中比较有意义 | |
request_body_file | 客户端请求主体信息的临时文件名 | |
request_completion | 如果请求成功,设为"OK";如果请求未完成或者不是一系列请求中最后一部分则设为空 | |
request_filename | 当前请求的文件路径名,比如/opt/nginx/www/test.php | |
request_method | 请求的方法,比如"GET"、"POST"等 | |
request_uri | 请求的URI,带参数 | |
scheme | 所用的协议,比如http或者是https | |
server_addr | 服务器地址,如果没有用listen指明服务器地址,使用这个变量将发起一次系统调用以取得地址(造成资源浪费) | |
server_name | 请求到达的服务器名 | |
server_port | 请求到达的服务器端口号 | |
server_protocol | 请求的协议版本,"HTTP/1.0"或"HTTP/1.1" | |
uri | 请求的URI,可能和最初的值有不同,比如经过重定向之类的 |
可以使用共享内存方式实现。
可以使用Lua模块方式实现。
在单个请求中,跨越多个Ng处理阶段(access、content)共享变量时,可以使用 ngx.ctx表:
location /test {
rewrite_by_lua_block {
ngx.ctx.foo = 76
}
access_by_lua_block {
ngx.ctx.foo = ngx.ctx.foo + 3
}
content_by_lua_block {
ngx.say(ngx.ctx.foo)
}
}
ngx.ctx表的生命周期和请求相同,类似于Nginx变量。需要注意,每个子请求都有自己的ngx.ctx表,它们相互独立。
你可以为ngx.ctx表注册元表,任何数据都可以放到该表中。
注意:访问ngx.ctx需要相对昂贵的元方法调用,不要为了避免传参而大量使用,影响性能.
# 设置纯 Lua 扩展库的搜寻路径
# ';;' 是默认路径
lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;";
# 设置 C 编写的 Lua 扩展模块的搜寻路径
# ';;' 是默认路径
lua_package_cpath '/bar/baz/?.so;/blah/blah/?.so;;';
大静态文件的响应,让Nginx自己完成。
如果是应用程序动态生成的大响应体,可以使用HTTP 1.1的CHUNKED编码。对应响应头: Transfer-Encoding: chunked。这样响应就可以逐块的发送到客户端,不至于占用服务器内存。
-- 可以进行限速,单位字节
ngx.var.limit_rate = 64
-- 获取配置目录
local file, err = io.open(ngx.config.prefix() .. "nginx.conf", "r")
if not file then
-- 打印Nginx日志
ngx.log(ngx.ERR, "open file error:", err)
-- 以指定的HTTP状态码退出处理
ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
-- 如果没有ngx.exit,则:
local data
while true do
data = file:read(64)
if nil == data then
break
end
ngx.print(data)
-- true表示等待IO操作完成
ngx.flush(true)
ngx.sleep(1)
end
file:close()
-- http://localhost:8080/put-res-body-chunked 会一行行的输出
使用内部调用(子查询),可以向某个location非阻塞的发起调用。目录location可以是静态文件目录,也可以由gx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle甚至其它ngx_lua模块提供内容生成。
需要注意:
location /sum {
-- 仅允许内部跳转调用
internal;
content_by_lua_block {
-- 解析请求参数
local args = ngx.req.get_uri_args()
-- 输出
ngx.say(tonumber(args.a)+tonumber(args.b))
}
}
location /sub {
internal;
content_by_lua_block{
-- 休眠
ngx.sleep(0.1)
local args = ngx.req.get_uri_args()
ngx.print(tonumber(args.a) - tonumber(args.b))
}
}
location /test{
content_by_lua_block{
-- 发起一个子查询
-- res.status 子请求的响应状态码
-- res.header 子请求的响应头,如果某个头是多值的,则存放在table中
-- res.body 子请求的响应体
-- res.truncated 标记响应体是否被截断。截断意味着子请求处理过程中出现不可恢复的错误,例如超时、早断
local res = ngx.location.capture( "/sum", {
args={a=3, b=8}, -- 为子请求附加URI参数
method = ngx.HTTP_POST, -- 指定请求方法,默认GET
body = 'hello, world' -- 指定请求体
} )
-- 并行的发起多个子查询
local res1, res2 = ngx.location.capture_multi( {
{"/sum", {args={a=3, b=8}}},
{"/sub", {args={a=3, b=8}}}
})
ngx.say(res1.status," ",res1.body)
ngx.say(res2.status," ",res2.body)
}
}
location ~ ^/static/([-_a-zA-Z0-9/]+).jpg {
-- 这里将URI中捕获的第一个分组,赋值给变量
set $image_name $1;
content_by_lua_block {
-- ng.var可以读取Nginx变量
-- ngx.exec执行跳转
ngx.exec("/download_internal/images/" .. ngx.var.image_name .. ".jpg");
};
}
location /download_internal {
internal;
-- 可以在这里进行各种声明,例如限速
alias ../download;
}
注意,ngx.exec引发的跳转完全在Ng内部完成,不会产生HTTP协议层的信号。
使用ngx.redirect可以进行外部跳转,也就是重定向。
location = / {
rewrite_by_lua_block {
return ngx.redirect('/blog');
}
}
OpenResty提供的日志API为 ngx.log(log_level, ...) 日志输出到Nginx的errorlog中。
支持的日志级别如下:
ngx.STDERR -- 标准输出
ngx.EMERG -- 紧急报错
ngx.ALERT -- 报警
ngx.CRIT -- 严重,系统故障,触发运维告警系统
ngx.ERR -- 错误,业务不可恢复性错误
ngx.WARN -- 告警,业务中可忽略错误
ngx.NOTICE -- 提醒,业务比较重要信息
ngx.INFO -- 信息,业务琐碎日志信息,包含不同情况判断等
ngx.DEBUG -- 调试
模块lua-resty-logger-socket用于替代ngx_http_log_module,将Nginx日志异步的推送到远程服务器上。该模块的特性包括:
lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;";
log_by_lua_file log.lua;
local logger = require "resty.logger.socket"
if not logger.initted() then
local ok, err = logger.init {
host = 'ops.gmem.cc',
port = 8087,
flush_limit = 1234,
drop_limit = 5678,
}
if not ok then
ngx.log(ngx.ERR, "failed to initialize the logger: ", err)
return
end
end
-- 通过变量msg来访问 accesslog
local bytes, err = logger.log(msg)
if err then
ngx.log(ngx.ERR, "failed to log message: ", err)
return
end
-- 引入操控MySQL需要的模块
local mysql = require "resty.mysql"
-- 初始化数据库对象
local db, err = mysql:new()
if not db then
ngx.say("failed to instantiate mysql: ", err)
return
end
-- 设置连接超时
db:set_timeout(1000)
-- 设置连接最大空闲时间,连接池容量
db:set_keepalive(10000, 100)
-- 发起数据库连接
local ok, err, errno, sqlstate = db:connect {
host = "127.0.0.1",
port = 3306,
database = "test",
user = "root",
password = "root",
max_packet_size = 1024 * 1024
}
if not ok then
ngx.say("Failed to connect: ", err, ": ", errno, " ", sqlstate)
return
end
local res, err, _, _ = db:query([[
DROP TABLE IF EXISTS USERS;
]])
if not res then ngx.say(err); return end
res, err, errno, sqlstate = db:query([[
CREATE TABLE USERS
(
ID INT ,
NAME VARCHAR(64)
);
]])
if not res then ngx.say(err); return end
res, err, errno, sqlstate = db:query([[
INSERT INTO USERS (ID,NAME) VALUES ('10000','Alex');
INSERT INTO USERS (ID,NAME) VALUES ('10001','Meng');
]])
if not res then ngx.say(err); return end
local cjson = require "cjson"
ngx.say(cjson.encode(res))
要防止SQL注入,可以预处理一下用户提供的参数:
req_id = ndk.set_var.set_quote_sql_str(req_id)))
你可以用ngx.location.capture发起对另外一个location的子调用,并将后者配置为上游服务器的代理。如果:
最好使用lua-resty-http模块。该模块提供了基于cosocket的HTTP客户端。具有特性:
ngx.req.read_body()
-- 获取当前请求的参数
local args, err = ngx.req.get_uri_args()
local http = require "resty.http"
-- 创建HTTP客户端
local httpc = http.new()
-- request_uri函数在内部自动处理连接池
local res, err = httpc:request_uri("http://media-api.dev.svc.k8s.gmem.cc:8800/media/newpub/2017-01-01", {
method = "POST",
body = args.data, -- 转发请求参数给上游服务器
})
if 200 ~= res.status then
ngx.exit(res.status)
end
if args.key == res.body then
ngx.say("valid request")
else
ngx.say("invalid request")
end