OpenResty

OpenResty

简介与安装

1. 简介

OpenResty是一个基于Nginx与Lua的高性能web平台,其内部继承了大量精良的Lua库,第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发扩展性极高的动态的web应用

让你的web服务直接跑在Nginx服务内部,充分利用Nginx的非阻塞I/O模型,不仅仅对HTTP客户端请求,甚至于对远程后端诸如MySQL,PostgreSQL以及Redis等都进行一些列的高性能响应。

OpenResty的主要作用和功能包括:

  1. web服务器:OpenResty 作为一个高性能的 Web 服务器,可以处理大量的并发请求,提供高效的静态文件服务和动态内容生成。
  2. 反向代理:OpenResty可以作为反向代理服务器,用于将请求转发到后端的应用服务器,并且可以进行负载均衡,缓存,SSL终止等操作。
  3. 动态内容处理:OpenResty提供了强大的lua编程能力,可以用Lua编写脚本来处理请求,实现动态内容生成,访问控制,日志记录等功能。
  4. 扩展性:OpenResty 的模块化架构使得它可以轻松地集成各种第三方模块,扩展了 NGINX 的功能,例如集成了各种缓存模块、安全模块、访问控制模块等。
  5. 高性能代理:OpenResty 可以作为高性能代理服务器,用于构建 CDN、缓存代理等场景,提供高性能的数据传输和缓存服务。

2. Linux安装

  • 安装OpenResty的依赖库

    yum install -y pcre-devel openssl-devel gcc --skip-broken

  • 安装OpenResty仓库

    yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

    • 如果提示说命令不存在,则运行

      yum install -y yum-utils

    • 然后重复上面的命令

  • 安装OpenResty

    yum install -y openresty

  • 安装opm工具

    yum install -y openresty-opm

  • 目录结构

    默认情况下,OpenResty安装的目录是: /usr/local/openresty OpenResty就是在Nginx基础上继承了一些lua模块。

    • 配置Nginx环境变量

      • 打开配置文件

        vi /etc/profile

      • 在最下面加入两行

        export NGINX_HOME=/usr/local/openresty/nginx

        export PATH={NGINX_HOME}/sbin:PATH

      • NGINX_HOME:后面是OpenResty安装目录下的Nginx的目录

      • 然后让配置生效

        source /etc/profile

    • OpenResty启动

      # 启动nginx

      nginx

      # 重新加载配置

      nginx -s reload

      # 停止

      nginx -s stop

    • nginx的默认配置文件注释太多,影响后续我们的编辑,这里将nginx.conf中的注释部分删除,保留有效部分。

      修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:

      nginx 复制代码
      #user  nobody;
      worker_processes  1;
      error_log  logs/error.log;
      
      events {
          worker_connections  1024;
      }
      
      http {
          include       mime.types;
          default_type  application/octet-stream;
          sendfile        on;
          keepalive_timeout  65;
      
          server {
              listen       8081;
              server_name  localhost;
              location / {
                  root   html;
                  index  index.html index.htm;
              }
              error_page   500 502 503 504  /50x.html;
              location = /50x.html {
                  root   html;
              }
          }
      }
    • 在Linux的控制台输入命令以启动nginx:

      bash 复制代码
      nginx

    3. Windows安装

    • 下载: http://openresty.org/cn/download.html

    • 解压

    • 双击nginx.exe 或者执行 start nginx启动nginx

    • 验证是否成功 tasklist /fi "imagename eq nginx.exe" 其中一个是 master 进程,另一个是 worker 进程

    另外当 nginx 成功启动后,master 进程的 pid 存放在 logs\nginx.pid 文件中。

HelloWorld

为了工作目录与安装目录互不干扰,并顺便学下简单的配置文件编写,就另外创建一个OpenResty的工作目录来练习,并且另外写一个配置文件。

  1. 创建OpenResty-test目录,并在该目录下创建logs和conf子目录分别用于存放日志和配置文件

    $ mkdir ~/openresty-test ~/openresty-test/logs/ ~/openresty-test/conf/
    $
    $ tree ~/openresty-test
    /Users/yuansheng/openresty-test
    ├── conf
    └── logs
    
    2 directories, 0 files
    
  2. 创建配置文件

    在 conf 目录下创建一个文本文件作为配置文件,命名为 nginx.conf,文件内容如下:

    worker_processes  1;        #nginx worker 数量
    error_log logs/error.log;   #指定错误日志文件路径
    events {
        worker_connections 1024;
    }
    
    http {
        server {
            #监听端口,若你的6699端口已经被占用,则需要修改
            listen 6699;
            location / {
                default_type text/html;
    
                content_by_lua_block {
                    ngx.say("HelloWorld")
                }
            }
        }
    }
    

    提示:如果你安装的是 openresty 1.9.3.1 及以下版本,请使用 content_by_lua 命令代替示例中的 content_by_lua_block。可使用 nginx -V 命令查看版本号。

    text 复制代码
    ---  启动nginx
    ➜  ~ nginx -p ~/openresty-test
    
    ---  查看nginx进程
    ➜  ~ ps -ef | grep nginx
      501 88620     1   0 10:58AM ?? 0:00.00 nginx: master process nginx -p
                                        /Users/yuansheng/openresty-test
      501 88622 88620   0 10:58AM ?? 0:00.00 nginx: worker process
    
    --- 访问nginx
    ➜  ~ curl http://localhost:6699 -i
    HTTP/1.1 200 OK
    Server: openresty/1.9.7.3
    Date: Sun, 20 Mar 2016 03:01:35 GMT
    Content-Type: text/html
    Transfer-Encoding: chunked
    Connection: keep-alive
    
    HelloWorld

与其他Location配合

利用不同的Location的功能组合,可以完成内部调用,流水线方式跳转,外部重定向等几大不同方式,

内部调用

例如对数据库,内部公共函数的统一接口,可以把他们放到统一的Location中,通常情况下,为了保护这些内部接口,都会把这些接口设置为internal。这么做的好处就是可以让这个内部接口相互独立,不受外界干扰。

示例代码

nginx 复制代码
location = /sum {
    # 只允许内部调用
    internal;

    # 这里做了一个求和运算只是一个例子,可以在这里完成一些数据库、
    # 缓存服务器的操作,达到基础模块和业务逻辑分离目的
    content_by_lua_block {
        local args = ngx.req.get_uri_args()
        ngx.say(tonumber(args.a) + tonumber(args.b))
    }
}

location = /app/test {
    content_by_lua_block {
        local res = ngx.location.capture(
                        "/sum", {args={a=3, b=8}}
                        )
        ngx.say("status:", res.status, " response:", res.body)
    }
}

稍微扩充一下, 并行请求效果,示例如下:

nginx 复制代码
location = /sum {
    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 = /subduction {
    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 = /app/test_parallels {
    content_by_lua_block {
        local start_time = ngx.now()
        local res1, res2 = ngx.location.capture_multi( {
                        {"/sum", {args={a=3, b=8}}},
                        {"/subduction", {args={a=3, b=8}}}
                    })
        ngx.say("status:", res1.status, " response:", res1.body)
        ngx.say("status:", res2.status, " response:", res2.body)
        ngx.say("time used:", ngx.now() - start_time)
    }
}

-- ngx.location.capture_multi 方法允许在同一个请求中并发地发起多个子请求
location = /app/test_queue {
    content_by_lua_block {
        local start_time = ngx.now()
        local res1 = ngx.location.capture_multi( {
                        {"/sum", {args={a=3, b=8}}}
                    })
        local res2 = ngx.location.capture_multi( {
                        {"/subduction", {args={a=3, b=8}}}
                    })
        ngx.say("status:", res1.status, " response:", res1.body)
        ngx.say("status:", res2.status, " response:", res2.body)
        ngx.say("time used:", ngx.now() - start_time)
    }
}
➜  ~ curl 127.0.0.1/app/test_parallels
status:200 response:11
status:200 response:-5
time used:0.10099983215332
➜  ~ curl 127.0.0.1/app/test_queue
status:200 response:11
status:200 response:-5
time used:0.20199990272522

利用 nginx.location.capture_multi 函数,直接完成了两个子请求并执行。当两个请求没有相互依赖,这种方法可以极大提高查询效率

该方法,可以被广泛应用于广告系统(1:N模型,一个请求,后端从N家供应商中获取条件最优的广告)、高并发前段页面展示(并行无依赖界面,降级开关等)。

流水线方式跳转

各种不同的API,下载请求混杂在一起,要求厂商对下载的动态调整有各种不同的定制策略,而这些策略在一天的不同时间段,规则可能还不一样。这个时候还可以效仿工厂的流水线模式,逐层过滤,处理。

示例代码

nginx 复制代码
location ~ ^/static/([-_a-zA-Z0-9/]+).jpg {
    set $image_name $1;
    content_by_lua_block {
        ngx.exec("/download_internal/images/"
                .. ngx.var.image_name .. ".jpg");
    };
}

location /download_internal {
    internal;
    # 这里还可以有其他统一的 download 下载设置,例如限速等
    alias ../download;
}

注意,ngx.exec 方法与 ngx.redirect 是完全不同的,前者是个纯粹的内部跳转并且没有引入任何额外 HTTP 信号。 这里的两个 location 更像是流水线上工人之间的协作关系。第一环节的工人对完成自己处理部分后,直接交给第二环节处理人(实际上可以有更多环节),它们之间的数据流是定向的。

外部重定向

百度的首页已经不再是 HTTP 协议,它已经全面修改到了 HTTPS 协议上。但是对于大家的输入习惯,估计还是在地址栏里面输入 baidu.com ,回车后发现它会自动跳转到 https://www.baidu.com ,这时候就需要的外部重定向了。

nginx 复制代码
location = /foo {
    content_by_lua_block {
        ngx.say([[I am foo]])
    }
}

location = / {
    rewrite_by_lua_block {
        return ngx.redirect('/foo');
    }
}

测试结果如下

nginx 复制代码
--   -i  表示输出响应头信息 
➜  ~  curl 127.0.0.1 -i
HTTP/1.1 302 Moved Temporarily
Server: openresty/1.9.3.2rc3
Date: Sun, 22 Nov 2015 11:04:03 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: /foo

<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>openresty/1.9.3.2rc3</center>
</body>
</html>

➜  ~  curl 127.0.0.1/foo -i
HTTP/1.1 200 OK
Server: openresty/1.9.3.2rc3
Date: Sun, 22 Nov 2015 10:43:51 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive

I am foo

外部重定向是可以跨域名的。例如从 A 网站跳转到 B 网站是绝对允许的。在 CDN 场景的大量下载应用中,一般分为调度、存储两个重要环节。调度就是通过根据请求方 IP 、下载文件等信息寻找最近、最快节点,应答跳转给请求方完成下载。

获取uri参数

获取请求uri参数

获取一个uri有两个方法:ngx.req.get_uri_argsngx.req.get_post_args ,二者主要的区别是参数来源有区别。前者来自uri参数,后者来自post请求内容

示例

nginx 复制代码
server {
   listen    80;
   server_name  localhost;

   location /print_param {
       content_by_lua_block {
           local arg = ngx.req.get_uri_args()
           for k,v in pairs(arg) do
               ngx.say("[GET ] key:", k, " v:", v)
           end

           ngx.req.read_body() -- 解析 body 参数之前一定要先读取 body
           local arg = ngx.req.get_post_args()
           for k,v in pairs(arg) do
               ngx.say("[POST] key:", k, " v:", v)
           end
       }
   }
}

输出结果:

shell 复制代码
➜  ~  curl '127.0.0.1/print_param?a=1&b=2%26' -d 'c=3&d=4%26'
[GET ] key:b v:2&
[GET ] key:a v:1
[POST] key:d v:4&
[POST] key:c v:3

传递uri参数

ngx.location.capture 函数用于发起一个内部子请求,并且等待子请求的响应

nginx 复制代码
  location /test {
       content_by_lua_block {
           local res = ngx.location.capture(
                    '/print_param',
                    {
                       method = ngx.HTTP_POST,
                       args = ngx.encode_args({a = 1, b = '2&'}),
                       body = ngx.encode_args({c = 3, d = '4&'})
                   }
                )
           ngx.say(res.body)
       }
   }

输出响应结果

nginx 复制代码
➜  ~  curl '127.0.0.1/test'
[GET]  key:b v:2&
[GET]  key:a v:1
[POST] key:d v:4&
[POST] key:c v:3

如果这里不调用ngx.encode_args ,可能就会比较丑了,看下面例子:

lua 复制代码
local res = ngx.location.capture('/print_param',
         {
            method = ngx.HTTP_POST,
            args = 'a=1&b=2%26',  -- 注意这里的 %26 ,代表的是 & 字符
            body = 'c=3&d=4%26'
        }
     )
ngx.say(res.body)

PS:对于 ngx.location.capture 这里有个小技巧,args 参数可以接受字符串或Lua 表的,这样我们的代码就更加简洁直观。

lua 复制代码
local res = ngx.location.capture('/print_param',
         {
            method = ngx.HTTP_POST,
            args = {a = 1, b = '2&'},
            body = 'c=3&d=4%26'
        }
     )
ngx.say(res.body)

获取请求body

在nginx的典型应用场景中,几乎都是只读取HTTP请求头即可,例如负载均衡,正反向代理等场景。但是对于API server 或者 web Application,对body可以说比较敏感了。

最简单的"hello *******"

我们先来构造最简单的一个请求,POST 一个名字给服务端,服务端应答一个 "Hello ********"。

nginx 复制代码
http {
    server {
        listen    80;

        location /test {
            content_by_lua_block {
                local data = ngx.req.get_body_data()
                ngx.say("hello ", data)
            }
        }
    }
}

测试结果:

➜  ~  curl 127.0.0.1/test -d jack
hello nil

从结果中可以看出data部分获取为nil,原因是还需要添加指令 lua_need_request_body

nginx 复制代码
http {
    server {
        listen    80;

        # 默认读取 body  设置全局行为
        lua_need_request_body on;

        location /test {
            content_by_lua_block {
                local data = ngx.req.get_body_data()
                ngx.say("hello ", data)
            }
        }
    }
}

再次测试,符合我们预期:

➜  ~  curl 127.0.0.1/test -d jack
hello jack

如果读取body并非全局行为,也可以显示的调用ngx.req.read_body() 接口,参看下面示例:

nginx 复制代码
http {
    server {
        listen    80;

        location /test {
            content_by_lua_block {
                ngx.req.read_body()
                local data = ngx.req.get_body_data()
                ngx.say("hello ", data)
            }
        }
    }
}

输出响应体

HTTP响应报文分为三个部分:

  1. 响应行
  2. 响应头
  3. 响应体

对于HTTP响应体的输出,在OpenResty中调用 ngx.sayngx.print即可。

区别:

  • ngx.say会对输出响应体多输出一个 \n

ngx.say 与 ngx.print 均为异步输出,也就是说当调用 ngx.say 后并不会立刻输出响应体。

nginx 复制代码
  server {
        listen    80;

        location /test {
            content_by_lua_block {
                ngx.say("hello")
                ngx.sleep(3)
                ngx.say("the world")
            }
        }

        location /test2 {
            content_by_lua_block {
                ngx.say("hello")
                ngx.flush() -- 显式的向客户端刷新响应输出
                ngx.sleep(3)
                ngx.say("the world")
            }
        }
    }

测试接口可以观察到, /test 响应内容实在触发请求 3s 后一起接收到响应体,而 /test2 则是先收到一个 hello 停顿 3s 后又接收到后面的 the world

再看下面的例子:

nginx 复制代码
 server {
        listen    80;
        lua_code_cache off;

        location /test {
            content_by_lua_block {
                ngx.say(string.rep("hello", 1000))
                ngx.sleep(3)
                ngx.say("the world")
            }
        }
    }

执行测试,可以发现首先收到了所有的 "hello" ,停顿大约 3 秒后,接着又收到了 "the world" 。

通过两个例子对比,可以知道,因为是异步输出,两个响应体的输出时机是 不一样 的。

处理响应体过大的输出

当响应体过大(例如超过2G),是不能直接调用API完成响应体输出的。响应体过大,分两种情况:

  1. 输出内容本身体积很大,例如超过2G的文件下载
  2. 输出内容本身是由各种碎片拼凑的,碎片数量庞大,例如应答数据是某地区所有人的姓名

第①个情况,要利用 HTTP 1.1 特性 CHUNKED 编码来完成

可以利用CHUNKED格式,把一个大的响应体拆分成多个小的响应体,分批,有节制的响应给请求方

nginx 复制代码
location /test {
    content_by_lua_block {
        -- ngx.var.limit_rate = 1024*1024
        local file, err = io.open(ngx.config.prefix() .. "data.db","r")
        if not file then
            ngx.log(ngx.ERR, "open file error:", err)
            ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
        end

        local data
        while true do
            data = file:read(1024)
            if nil == data then
                break
            end
            ngx.print(data)
            ngx.flush(true)
        end
        file:close()
    }
}

日志输出

标准日志输出

OpenResty的标准日志输入原句为 ngx.log(log_level, ...),几乎可以在任何ngx_lua阶段进行日志的输出

nginx 复制代码
worker_processes  1;

error_log  logs/error.log error;    # 日志级别
#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    server {
        listen    80;
        location / {
            content_by_lua_block {
                local num = 55
                local str = "string"
                local obj
                ngx.log(ngx.ERR, "num:", num)
                ngx.log(ngx.INFO, " string:", str)
                print([[i am print]])
                ngx.log(ngx.ERR, " object:", obj)
            }
        }
    }
}

访问网页,生成日志(logs/error.log文件)结果如下:

nginx 复制代码
2016/01/22 16:43:34 [error] 61610#0: *10 [lua] content_by_lua(nginx.conf:26):5:
 num:55, client: 127.0.0.1, server: , request: "GET /hello HTTP/1.1",
 host: "127.0.0.1"

2016/01/22 16:43:34 [error] 61610#0: *10 [lua] content_by_lua(nginx.conf:26):7:
 object:nil, client: 127.0.0.1, server: , request: "GET /hello HTTP/1.1",
 host: "127.0.0.1"

Nginx的日志级别:

nginx 复制代码
ngx.STDERR     -- 标准输出
ngx.EMERG      -- 紧急报错
ngx.ALERT      -- 报警
ngx.CRIT       -- 严重,系统故障,触发运维告警系统
ngx.ERR        -- 错误,业务不可恢复性错误
ngx.WARN       -- 告警,业务中可忽略错误
ngx.NOTICE     -- 提醒,业务比较重要信息
ngx.INFO       -- 信息,业务琐碎日志信息,包含不同情况判断等
ngx.DEBUG      -- 调试

网络日志输出

如果你的日志需要归集,并且对时效性要求比较高那么这里要推荐的库可能就让你很喜欢了。 lua-resty-logger-socket ,可以说很好的解决了上面提及的几个特性。

lua-resty-logger-socket 的目标是替代 Nginx 标准的 ngx_http_log_module 以非阻塞 IO 方式推送 access log 到远程服务器上。对远程服务器的要求是支持 syslog-ng 的日志服务。

nginx 复制代码
lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;";

    server {
        location / {
            log_by_lua_block {
                local logger = require "resty.logger.socket"
                if not logger.initted() then
                    local ok, err = logger.init{
                        host = 'xxx',
                        port = 1234,
                        flush_limit = 1234,
                        drop_limit = 5678,
                    }
                    if not ok then
                        ngx.log(ngx.ERR, "failed to initialize the logger: ",
                                err)
                        return
                    end
                end

                -- construct the custom access log message in
                -- the Lua variable "msg"

                local bytes, err = logger.log(msg)
                if err then
                    ngx.log(ngx.ERR, "failed to log message: ", err)
                    return
                end
            }
        }
    }

例举几个好处:

  • 基于cosocket非阻塞IO实现
  • 日志累计到一定量,集体提交,增加网络传输利用率
  • 短时间的网络抖动,自动容错
  • 日志累计到一定量,如果没有传输完毕,直接丢弃
  • 日志传输过程完全不落地,没有任何磁盘 IO 消耗