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。

安装

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

起步


1. 创建工程

一个OpenRestry工程,实际上就是对应了Nginx运行环境的目录结构。例如:

mkdir -p ~/Lua/projects/openrestry
cd ~/Lua/projects/openrestry
mkdir conf && mkdir logs

2. 配置文件

使用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("<p>hello, world</p>")
            }
        }
    }
}

3. 启动服务

# 将Nginx运行时的前缀设置为上面的工程目录
~/Lua/openresty/1.13.6/nginx/sbin/nginx -p ~/Lua/projects/openrestry -c conf/nginx.conf

4. 测试服务

curl http://localhost:8080/
# <p>hello, world</p>

IDE


1. Intellij

安装三个插件:

  1. nginx support:支持Nginx配置文件的语法高亮、格式化、自动完成。自动基于Lua语言对lua-nginx-module模块的相关指令进行语法高亮、自动完成
  2. Lua:支持Lua语言的开发和调试
  3. OpenResty Lua Support:为OpenResty提供自动完成

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. Nginx.conf

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;
        }
    }
}

2.auth-and-check.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

使用Nginx变量


变量说明
arg_name请求中的name参数
args请求中的参数
binary_remote_addr远程地址的二进制表示
body_bytes_sent已发送的消息体字节数
content_lengthHTTP请求信息里的"Content-Length"
content_type请求信息里的"Content-Type"
document_root针对当前请求的根路径设置值
document_uri与$uri相同; 比如 /test2/test.php
host请求信息中的"Host",如果请求中没有Host行,则等于设置的服务器名
hostname机器名使用 gethostname系统调用的值
http_cookieCookie信息
http_referer引用地址
http_user_agent客户端代理信息
http_via最后一个访问服务器的Ip地址。
http_x_forwarded_for相当于网络访问路径
is_args如果请求行带有参数,返回“?”,否则返回空字符串
limit_rate

对连接速率的限制。此变量支持写入:

Lua

-- 设置当前请求的响应传递速率限制

ngx.var.limit_rate = 1000 

nginx_version当前运行的nginx版本号
pidWorker进程的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,可能和最初的值有不同,比如经过重定向之类的

数据共享


1.跨工作进程

可以使用共享内存方式实现。

2.跨请求

可以使用Lua模块方式实现。

3.跨阶段

在单个请求中,跨越多个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 扩展库的搜寻路径
# ';;' 是默认路径
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


1.内部调用

使用内部调用(子查询),可以向某个location非阻塞的发起调用。目录location可以是静态文件目录,也可以由gx_proxy、ngx_fastcgi、ngx_memc、ngx_postgres、ngx_drizzle甚至其它ngx_lua模块提供内容生成。

需要注意:

  1. 内部调用仅仅是模拟HTTP的接口形式,不会产生额外的HTTP/TCP流量
  2. 内部调用完全不同于HTTP 301/302重定向指令,和内部重定向(ngx.exec)也完全不同
  3. 、在发起内部调用之前,必须先读取完整的请求体。你可以设置lua_need_request_body指令或者调用ngx.req.read_body。如果请求体太大,可以考虑使用cosockets模块进行流式处理
  4. 子请求默认继承当前请求的所有请求头信息。配置 proxy_pass_request_headers=off;可以忽略父请求的头
  5. ngx.location.capture/capture_multi无法请求包含以下指令的location:add_before_body, add_after_body, auth_request, echo_location, echo_location_async, echo_subrequest, echo_subrequest_async
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)
    }
}

2.内部跳转

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协议层的信号。

3.外部跳转

使用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      -- 调试

1.日志归集

模块lua-resty-logger-socket用于替代ngx_http_log_module,将Nginx日志异步的推送到远程服务器上。该模块的特性包括:

  1. 基于 cosocket 非阻塞 IO 实现
  2. 日志累计到一定量,集体提交,增加网络传输利用率
  3. 短时间的网络抖动,自动容错
  4. 日志累计到一定量,如果没有传输完毕,直接丢弃
  5. 日志传输过程完全不落地,没有任何磁盘 IO 消耗
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)))

调用HTTP


你可以用ngx.location.capture发起对另外一个location的子调用,并将后者配置为上游服务器的代理。如果:

  1. 内部请求数量较多
  2. 需要频繁修改上游服务器的地址

最好使用lua-resty-http模块。该模块提供了基于cosocket的HTTP客户端。具有特性:

  1. 支持HTTP 1.0/1.1
  2. 支持SSL
  3. 支持响应体的流式接口,内存用量可控
  4. 对于简单的应用场景,提供更简单的接口
  5. 支持Keepalive
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

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值