OpenResty学习笔记
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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
wget https://openresty.org/download/openresty-1.13.6.1.tar.gz tar xzf openresty-1.13.6.1.tar.gz cd 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运行环境的目录结构。例如:
1 2 3 |
mkdir -p ~/Lua/projects/openrestry cd ~/Lua/projects/openrestry mkdir conf && mkdir logs |
使用lua-nginx-module模块提供的指令,你可以嵌入Lua脚本到Nginx配置文件中,以生成响应内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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("<p>hello, world</p>") } } } } |
1 2 |
# 将Nginx运行时的前缀设置为上面的工程目录 ~/Lua/openresty/1.13.6/nginx/sbin/nginx -p ~/Lua/projects/openrestry -c conf/nginx.conf |
1 2 |
curl http://localhost:8080/ # <p>hello, world</p> |
安装三个插件:
- nginx support:支持Nginx配置文件的语法高亮、格式化、自动完成。自动基于Lua语言对lua-nginx-module模块的相关指令进行语法高亮、自动完成
- Lua:支持Lua语言的开发和调试
- OpenResty Lua Support:为OpenResty提供自动完成
不同类型的指令,职责如下:
指令 | 说明 |
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*就可以完成以上职责,但是把逻辑划分在不同阶段,更加容易维护。
一个简单的API框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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; } } } |
在access阶段,你可以进行身份验证、访问控制、请求参数校验:
1 2 3 4 5 6 7 8 9 |
-- 黑名单 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 |
要在OpenResty中引用Nginx变量,可以使用 ngx.var.VARIABLE,要将变量从字符串转换为数字,可以使用 tonumber函数。
经常用到的Ng变量如下表:
变量 | 说明 | ||
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 |
对连接速率的限制。此变量支持写入:
|
||
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表:
1 2 3 4 5 6 7 8 9 10 11 |
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需要相对昂贵的元方法调用,不要为了避免传参而大量使用,影响性能。
使用如下指令:
1 2 3 4 5 6 7 |
# 设置纯 Lua 扩展库的搜寻路径 # ';;' 是默认路径 lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;"; # 设置 C 编写的 Lua 扩展模块的搜寻路径 # ';;' 是默认路径 lua_package_cpath '/bar/baz/?.so;/blah/blah/?.so;;'; |
1 2 3 4 |
location /print-params{ # 使用Ng配置文件外部的Lua脚本 content_by_lua_file scripts/print-params.lua; } |
1 2 3 4 |
local args = ngx.req.get_uri_args() for name, value in pairs(args) do ngx.say(name .. ' ' .. value) end |
测试:
1 2 3 |
curl 'http://localhost:8080/print-params?user=alex&age=32' # user alex # age 32 |
1 2 3 4 5 6 7 8 9 |
-- 必须先读取请求体,才能从中解析参数 ngx.req.read_body() -- 获取POST参数 local args = ngx.req.get_post_args() for name, value in pairs(args) do ngx.say(name .. ' ' .. value) end -- curl -d 'user=alex&age=32' 'http://localhost:8080/print-params' |
当调用其它location,需要传递请求参数时,可以进行编码:
1 2 3 4 5 6 7 8 9 10 11 12 |
local res = ngx.location.capture('/print-param-internal', { -- 调用使用的HTTP方法 method = ngx.HTTP_POST, -- URL参数 args = ngx.encode_args({ a = 1, b = '2&' }), -- 编码为 'a=1&b=2%26' -- POST参数 body = ngx.encode_args({ c = 3, d = '4&' }) -- 注意,capture可以直接接收Lua Table,下面的更简洁 args = {a = 1, b = '2&'}, }) ngx.say(res.body) |
当用Nginx作为负载均衡或反向代理时,基本上仅仅需要读取请求头就足够了。使用OpenResty 后,你可以把Nginx直接作为API服务器、Web服务器,这是就需要操控请求体、响应体了。
要通过Lua读取请求体,可以添加指令:
1 2 |
# 总是让Lua读取请求体 lua_need_request_body on; |
如果仅仅希望某个接口读取请求体,可以调用: ngx.req.read_body()
如果请求体已经被存入临时文件,则需要调用 ngx.req.get_body_file()。
如果需要强制把请求体存入临时文件,配置 client_body_in_file_only on;
如果需要强制在内存中保留请求体,配置client_body_buffer_size和client_max_body_size 为相同值。
读取请求体的代码:
1 2 3 4 5 |
local data = ngx.req.get_body_data() ngx.say(data) -- curl -d Greetings 'http://localhost:8080/get-req-body' -- Greetings |
要输出响应体,可以调用: ngx.say、 ngx.print,输出不会立即写入套接字,你可以调用 ngx.flush()刷出缓冲区。
大静态文件的响应,让Nginx自己完成。
如果是应用程序动态生成的大响应体,可以使用HTTP 1.1的CHUNKED编码。对应响应头: Transfer-Encoding: chunked。这样响应就可以逐块的发送到客户端,不至于占用服务器内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
-- 可以进行限速,单位字节 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模块提供内容生成。
需要注意:
- 内部调用仅仅是模拟HTTP的接口形式,不会产生额外的HTTP/TCP流量
- 内部调用完全不同于HTTP 301/302重定向指令,和内部重定向(ngx.exec)也完全不同
- 、在发起内部调用之前,必须先读取完整的请求体。你可以设置lua_need_request_body指令或者调用ngx.req.read_body。如果请求体太大,可以考虑使用cosockets模块进行流式处理
- 子请求默认继承当前请求的所有请求头信息。配置 proxy_pass_request_headers=off;可以忽略父请求的头
- ngx.location.capture/capture_multi无法请求包含以下指令的location:add_before_body, add_after_body, auth_request, echo_location, echo_location_async, echo_subrequest, echo_subrequest_async
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
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) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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可以进行外部跳转,也就是重定向。
1 2 3 4 5 |
location = / { rewrite_by_lua_block { return ngx.redirect('/blog'); } } |
OpenResty提供的日志API为 ngx.log(log_level, ...) 日志输出到Nginx的errorlog中。
支持的日志级别如下:
1 2 3 4 5 6 7 8 9 |
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日志异步的推送到远程服务器上。该模块的特性包括:
- 基于 cosocket 非阻塞 IO 实现
- 日志累计到一定量,集体提交,增加网络传输利用率
- 短时间的网络抖动,自动容错
- 日志累计到一定量,如果没有传输完毕,直接丢弃
- 日志传输过程完全不落地,没有任何磁盘 IO 消耗
示例代码:
1 2 |
lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;"; log_by_lua_file log.lua; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
-- 引入操控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注入,可以预处理一下用户提供的参数:
1 |
req_id = ndk.set_var.set_quote_sql_str(req_id))) |
你可以用ngx.location.capture发起对另外一个location的子调用,并将后者配置为上游服务器的代理。如果:
- 内部请求数量较多
- 需要频繁修改上游服务器的地址
最好使用lua-resty-http模块。该模块提供了基于cosocket的HTTP客户端。具有特性:
- 支持HTTP 1.0/1.1
- 支持SSL
- 支持响应体的流式接口,内存用量可控
- 对于简单的应用场景,提供更简单的接口
- 支持Keepalive
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 |
cjson模块提供了编解码JSON的支持。
1 2 |
local json = require("cjson") json.encode(data) |
对于稀疏数组,例如:
1 2 |
local data = {1, 2} data[1000] = 99 -- 稀疏 |
会导致编码失败,提示:Cannot serialise table: excessively sparse array。其原因是数组太稀疏了,cjson为了保护资源默认抛出错误。
如果非要编码稀疏数组,考虑使用 encode_sparse_array函数。
由于Lua把字典、数组统一作为表格管理,因此就会牵涉到某个对象是编码为[]还是{}形式的问题:
1 2 3 4 5 6 7 |
cjson.encode({}) -- {} cjson.encode({dogs = {}}) -- {"dogs":{}} -- 可以提示cjson,让它把空表格编码为数组而非字典 cjson.encode_empty_table_as_object(false) cjson.encode({}) -- [] cjson.encode({dogs = {}}) -- {"dogs":[]} |
报错信息:error loading module 'cjson' from file '/home/alex/Lua/sdk/5.3.4': cannot read /home/alex/Lua/sdk/5.3.4: Is a directory
原因:安装了多套Lua环境,错误的设置了LUA_PATH、LUA_CPATH环境变量导致
解决办法:可以清空这些环境变量。另外这两个环境变量中不要包含目录。
Leave a Reply