基于Nginx和Lua的Web平台-OpenResty
2018-05-25 10:26:06 阿炯

OpenResty® is a dynamic web platform based on Nginx and LuaJIT.

OpenResty(也称为 ngx_openresty)是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关,采用C语言开发并在多种开源协议下授权。


OpenResty® is a full-fledged web platform that integrates the standard Nginx core, LuaJIT, many carefully written Lua libraries, lots of high quality 3rd-party Nginx modules, and most of their external dependencies. It is designed to help developers easily build scalable web applications, web services, and dynamic web gateways.


OpenResty 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。它的目标是让 Web 服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

点击 组件 可以知道 OpenResty 中包含了多少软件,点击 上路 学习如何从最简单的 hello world 开始使用 OpenResty 开发 HTTP 业务,或前往下载获取 OpenResty 的源代码包开始体验。

因为 OpenResty 软件包的维护者也是其中打包的许多 Nginx 模块的作者,所以 OpenResty 可以确保所包含的所有组件可以可靠地协同工作。


使用入门

下载ngx_openresty,包中集成Nginx、LuaJIT、ngx_lua以及部分第三方模块,解压后可见有luajit、lualib以及nginx三个子目录,其中luajit以及lualib包含后续集成nginx的核心模块。

通常程序有两种运行方式:静态编译与动态直译。静态编译的程序在执行前全部被翻译为机器码,而动态直译执行的则是一句一句边运行边翻译。即时编译(Just-In-TimeCompiler)则混合了这二者,一句句编译源代码,但是会将翻译过的代码缓存起来以降低性能损耗。

NDK(Nginx Development Kit)模块是一个拓展Nginx服务器核心功能的模块,第三方模块开发可以基于它来快速实现NDK提供函数和宏处理一些基本任务,减轻第三方模块开发的代码量。

重新编译Nginx(这里使用了nginx的官方版本)

wget 'http://nginx.org/download/nginx-1.7.x.tar.gz'
tar -xzvf nginx-1.7.x.tar.gz
cd nginx-1.7.x/

# 定义luajit环境变量,后续nginx编译,整合Lua后编译、解释以及运行需要用到
export LUAJIT_LIB=/path/to/luajit/lib
export LUAJIT_INC=/path/to/luajit/include/luajit-2.1

# 可以使用配置lua环境变量替代,但是luajit的效率比lua的效率更高,推荐使用luajit
#export LUA_LIB=/path/to/lua/lib
#export LUA_INC=/path/to/lua/include

# 编译安装nginx,增加ndk以及lua_ngx_module两个模块
./configure --prefix=/export/service/nginx \
--add-module=/path/to/ngx_devel_kit \
--add-module=/path/to/lua-nginx-module
make -j2
make install


重启Nginx,重新加载配置文件

测试Lua,配置/export/servers/nginx/conf/nginx.conf

location /lua {
default_type 'text/plain';  
# ngx.say方法是ngix_lua_module提供的用于在nginx中集成lua脚本的API
content_by_lua 'ngx.say("hello")';  }  

重新加载nginx配置文件

测试访问lua脚本


上面完成了Lua与Nginx的集成,同时lua-ngx-module提供了很多API,用于在Nginx配置文件中通过Lua脚本进行调用,通过这些API可以直接访问各种HTTP请求变量,实现内部请求重定向,访问后端数据库服务器、访问后端缓存服务器(Redis以及Memcached)等功能。

Lua脚本的调用

目前在Nginx中操作Lua脚本主要有三种方式,一种是直接在Nginx配置文件中通过content_by_lua指令直接在nginx配置文件中编写lua脚本,如下所示:
location /lua {
    # 设置响应的页面解析类型为文本类型
    default_type 'text/plain';
    content_by_lua '
        # 调用ngx.location.capture方法实现请求的内部跳转
        local res = ngx.location.capture("/some_other_location")
        # 响应状态码为200时,打印响应内容
        if res.status == 200 then
            ngx.print(res.body)
    end
    ';
}

另一种方式是通过set_by_lua指令引用外部的lua脚本,如下所示:
location /inline_concat {
    default_type 'text/plain';
    # 设置变量a的值为hello
    set $a "hello";
    # 设置变量b的值为world
    set $b "world";
    # 使用内联脚本的方式,输出传递到lua脚本中得参数
    set_by_lua $res "return ngx.arg[1]..ngx.arg[2]" $a $b;
    # 输出 hello world
    echo $res;
}

另一种方式是通过set_by_lua指令用内联的方式编写lua脚本,如下所示:
# 设置变量a的值为hello
    set $a "hello";
    # 设置变量b的值为world
    set $b "world";
    # 使用调用外部lua脚本的方式,输出传递到lua脚本中得参数
    set_by_lua $res conf/concat.lua $a $b;
    # 输出 hello world
    echo $res;


Lua执行顺序

(1)init_by_lua
在nginx重新加载配置文件时,运行里面lua脚本,常用于全局变量的申请。例如lua_shared_dict共享内存的申请,只有当nginx重起后,共享内存数据才清空,这常用于统计。

(2)set_by_lua:
设置一个变量,常用与计算一个逻辑,然后返回结果。该阶段不能运行Output API、ControlAPI、SubrequestAPI、CosocketAPI。

(3)rewrite_by_lua:
在access阶段前运行,主要用于rewrite。

(4)access_by_lua:
主要用于访问控制,能收集到大部分变量,类似status需要在log阶段才有。这条指令运行于nginx access阶段的末尾,因此总是在 allow 和 deny 这样的指令之后运行,虽然它们同属 access 阶段。

(5)content_by_lua:
阶段是所有请求处理阶段中最为重要的一个,运行在这个阶段的配置指令一般都肩负着生成内容(content)并输出HTTP响应。

(6)header_filter_by_lua:
一般只用于设置Cookie和Headers等。该阶段不能运行Output API、ControlAPI、SubrequestAPI、CosocketAPI

(7)body_filter_by_lua:
一般会在一次请求中被调用多次, 因为这是实现基于 HTTP 1.1 chunked 编码的所谓“流式输出”的。该阶段不能运行Output API、ControlAPI、SubrequestAPI、CosocketAPI

(8)log_by_lua:
该阶段总是运行在请求结束的时候,用于请求的后续操作,如在共享内存中进行统计数据,如果要高精确的数据统计,应该使用body_filter_by_lua。该阶段不能运行Output API、ControlAPI、SubrequestAPI、CosocketAPI

(9)timer:


这里就要连带的讲一下Nginx执行顺序

Nginx处理每一个用户请求时,都是按照若干个不同阶段(phase)依次处理的,而不是根据配置文件上的顺序。Nginx 处理请求的过程一共划分为 11 个阶段,按照执行顺序依次是:post-read、server-rewrite、find-config、rewrite、post-rewrite、preaccess、access、post-access、try-files、content、log。

(1)post-read
读取请求内容阶段,Nginx读取并解析完请求头之后就立即开始运行。例如模块 ngx_realip 就在post-read 阶段注册了处理程序,它的功能是迫使 Nginx 认为当前请求的来源地址是指定的某一个请求头的值。

(2)server-rewrite
Server请求地址重写阶段,当 ngx_rewrite 模块的set配置指令直接书写在 server 配置块中时,基本上都是运行在server-rewrite 阶段。

(3)find-config
配置查找阶段,这个阶段并不支持 Nginx 模块注册处理程序,而是由 Nginx 核心来完成当前请求与 location 配置块之间的配对工作。

(4)rewrite
Location请求地址重写阶段,当 ngx_rewrite 模块的指令用于location 块中时,便是运行在这个 rewrite 阶段。另外,ngx_set_misc(设置md5、encode_base64等)模块的指令,还有 ngx_lua 模块的set_by_lua 指令和rewrite_by_lua 指令也在此阶段。

(5)post-rewrite
请求地址重写提交阶段,由 Nginx 核心完成rewrite 阶段所要求的“内部跳转”操作,如果rewrite 阶段有此要求的话。

(6)preaccess
访问权限检查准备阶段,标准模块ngx_limit_req 和ngx_limit_zone 就运行在此阶段,前者可以控制请求的访问频度,而后者可以限制访问的并发度。

(7)access
访问权限检查阶段,标准模块ngx_access、第三方模块ngx_auth_request 以及第三方模块 ngx_lua 的access_by_lua 指令就运行在这个阶段。配置指令多是执行访问控制性质的任务,比如检查用户的访问权限,检查用户的来源 IP 地址是否合法。

(8)post-access
访问权限检查提交阶段,主要用于配合 access 阶段实现标准ngx_http_core 模块提供的配置指令 satisfy 的功能。satisfy all(与关系)satisfy any(或关系)。

(9)try-files
配置项try_files处理阶段,专门用于实现标准配置指令 try_files 的功能,如果前N-1 个参数所对应的文件系统对象都不存在,try-files 阶段就会立即发起“内部跳转”到最后一个参数(即第 N 个参数)所指定的 URI。

(10)content
内容产生阶段,Nginx 的content 阶段是所有请求处理阶段中最为重要的一个,因为运行在这个阶段的配置指令一般都肩负着生成“内容”并输出HTTP 响应的使命。

(11)log
日志模块处理阶段,记录日志。

Lua+Nginx+Memcached整合

使用场景:假设使用Tomcat作为后端服务器,但是Tomcat对于静态文件以及高并发请求的处理能力较弱。使用Lua+Nginx+Memcached的前端部署方式后,通过Lua去作为前置的控制器,处理模块渲染的请求,决定是模块内容由后端Tomcat服务器渲染还是直接从Memcached中获取。通过这种方式,处理高并发请求交由Nginx进行处理,而Tomcat只处理缓存失效或者没有缓存的模块渲染请求,极大的降低了Tomcat的压力,同时由于Nginx具有优秀的处理高并发的能力,并且内存以及CPU暂用小,并且由于直接在Nginx上操作缓存,能提升请求响应速度,节省带宽,提升用户体验。

Lua操作Memcahed

该操作主要是通过调用lua-ngx-module模块提供的API实现的,主要是通过在nginx.conf文件中加载memcached.lua模块,调用该包提供的API实现对Memcached的操作(操作Redis,MySQL的使用方式一致,只需要加载对应的模块即可)。

# 加载memcached.lua包
lua_package_path "/usr/local/openresty/lualib/resty/memcached.lua";
location /lua {
    # 设置响应文本类型为文本类型
    default_type 'text/plain';
    # 设置通过lua脚本获取响应内容
    content_by_lua '
        # 调用resty.memcached模块中得memcached函数
        local memcached = require "resty.memcached"
        # 初始化memcached
        local mem,err = memcached:new()
        # 初始化失败则返回,并且打印错误日志到/logs/error.log中
        if not mem then
            ngx.say("failed to initiall memcached",err)
            return
        end
        ngx.say("success to initiall memcached")
        # 连接memcached实例,ip:127.0.0.0,port:11211
        local ok,err = mem:connect("127.0.0.1",11211)
        # 连接失败则返回,并且打印错误日志到/logs/error.log中
        if not ok then
            ngx.say("failed to connect memcached",err)
            return
        end
        ngx.say("success to connect memcached")
        # 获取请求参数
        local args = ngx.req.get_uri_args()
        local key = "Module-Html-Cache-Key-"
        for key,value in pairs(args) do
            # 获取模块实例参数,组装缓存key
                if key == "instanceId"  then
                key = key .. value
                ngx.say("cache key is",key)
            end
        end
        # 根据缓存key从memcached中获取缓存
        local res, flags, err = mem:get(key)
        # 获取缓存失败则返回,并且打印错误日志到/logs/error.log中
        if err then
            ngx.say("failed to get cache: ", err)
            return
        end
        # 获取缓存为nil(lua中nil表示空),则内部转发请求
        if not res then
            ngx.say("cache not found")
            # 通过ngx.location.capture函数转发,ngx.location.capture不能跨server转发
        local res = ngx.location.capture("/module/renderModule.html?instanceId=10001")
        # 判断响应状态是否为200,200则表示正确响应,设置缓存
        if res.status == ngx.HTTP_OK then
            local ok, err = mem:set(key, res.body, 30)
            if not ok then
                # 设置缓存失败则返回,并且打印错误日志到/logs/error.log中
                ngx.say("failed to set cache: ", err)
            end
            # 输出响应内容主体
            ngx.print(res.body)
        end
    end
    ';
}


网关技术选型优先选择Openresty:事件驱动+协程

网关是连接客户端与服务端的中间桥梁,将很多通用、非业务逻辑抽离,前置到网关系统,减少了很多重复性开发工作,是整个网站的唯一流量入口。为了提高系统的扩展性,网关通常采用组件式架构,高内聚低耦合。常用的组件功能:黑名单拦截、日志、参数校验、鉴权、限流、负载均衡、路由转发、监控、灰度分流、多协议支持、熔断、降级、重试、数据聚合等。


系统设计上一般采用责任链设计模式,定义好抽象接口,每个组件实现自己的专属功能,职责单一。并且根据不同的业务请求API,添加、删除一些节点,动态构建新的节点链,从而满足多样化的业务需求。

选择OpenRest做为网关主要有以下几个原因:
能实现跨网络的gRPC请求转发,底层采用 HTTP/2 协议
支持 SSL/TLS 证书加密,通讯安全
性能方面,支持较高的并发请求
性能开销低,延迟少

常用的 12306 的余票查询功能,京东的商品详情页,这些高流量的背后,其实都是 OpenResty 在提供服务。它最擅长的是部署在流量入口处,处理各种高并发流量。

Nginx采用 master-worker 进程模型,分工明确,职责单一,也是其具备高性能的原因之一。

1、master 进程
管理进程,处理指令如:-s reload、-s stop,通过进程间通信,将管理指令发送给worker进程,从而实现对worker进程的控制。

2、worker 进程
工作进程,不断接收客户端的连接请求,处理请求。数量通常设置为与CPU核数一致,nginx也会将每个进程与每个CPU进行绑定,充分利用其多核特性。

多个worker进程会竞争一个共享锁,只有抢到锁的进程才能处理客户端的请求。如果请求是accept事件,则会将其添加到accept队列中;如果是read或者write事件,则会将其添加到read-write队列。

借助Nginx的高并发能力,反向代理服务器通常流量很大,本身不涉及复杂计算,属于I/O密集型服务。Nginx 采用基于 epoll 机制的事件驱动,异步非阻塞,大大提高并发处理能力。但是 Nginx 采用 C 语言 开发,二次开发门槛较高。市场应用广泛,更多是基于nginx.conf预留配置参数,如:反向代理、负载均衡、静态Web服务器等。

如果想让Nginx访问 MySQL ,定制化开发一些业务逻辑,难度很高。OpenResty 通过嫁接方式,将 Nginx 和 Lua 脚本相结合,既保留 Nginx 高并发优势,也拥有脚本语言的开发效率,也大大降低了开发门槛。

Lua 是最快的、动态脚本语言,接近C语言运行速度。LuaJIT 将一些常用的lua函数和工具库预编译并缓存,下次调用时直接使用缓存的字节码,速度很快。另外Lua支持协程,这个很重要。

协程是用户态的操作,上下文切换不用涉及内核态,系统资源开销小;另外协程占用内存很小,初始 2KB

OpenResty 核心架构

OpenResty基于Nginx的Web平台,内部嵌入LuaJIT虚拟机运行Lua脚本。使用 Lua 编程语言对 Nginx 核心以及各种 Nginx C 模块进行脚本编程。


每接到一个客户端请求,通过抢占锁,由一个worker进程来跟进处理;
worker内部会创建一个lua协程,绑定请求,也就是说一个请求对应一个lua协程;
lua协程将请求通过网络发出,并添加一个event事件到nginx。然后,当前协程就处于 yield,让出CPU控制权;
当服务端响应数据后,网络流程会创建一个新的event事件,将之前的协程唤醒,将结果返回。

注意:不同的lua协程之间数据隔离,从而保证了不同的客户端请求不会相互影响。另外,一个worker中同一时刻只会有一个协程在运行。


cosocket 将 Lua 协程 + Nginx 事件通知两个重要特性组合。是 OpenResty 世界中技术、实用价值最高部分。可以用非常低廉的成本,优雅的方势,比传统 socket 编程效率高好几倍的方式进行网络编程。无论资源占用、执行效率、并发能力都非常出色。

为了方便开发,OpenResty 将一个 HTTP 请求划分为11个阶段(上文亦有述),每个阶段有自己的专属职责。


函数功能说明:
set_by_lua:用于设置变量
rewrite_by_lua:用于转发、重定向等
access_by_lua:用于准入、权限等
content_by_lua:用于生成返回内容
balancer_by_lua:负载均衡,路由转发
header_filter_by_lua:用于响应头过滤处理
body_filter_by_lua:用于响应体过滤处理
log_by_lua:日志记录

OpenResty 提供了大量的 Lua API 接口用于操作 Nginx;只要熟悉lua语法,同时对 Nginx 的运行流程有较清楚理解,那么就可以轻松的在Nginx上做二次开发。

无论是作为应用网关,还是高性能的web应用,支持连接各种丰富的后端存储,如:MySQL、Redis、Memcache、PostgreSQL 等,周边生态非常丰富。

注意:OpenResty 的 API 有使用范围限制,每个 API 都有与之对应的使用阶段列表,如果你超范围使用就会报错。


以 CentOS 系统中为例进行部署安装

1、添加 openresty 仓库,这样以后可以通过 yum updata 命令安装或更新我们的软件包
yum install yum-utils -y
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

2、安装软件
yum install openresty -y

3、安装命令行工具 resty
yum install openresty-resty -y

项目实战

1、修改nginx.conf配置文件
worker_processes  auto;
worker_rlimit_nofile 1000000;
events {
use epoll;
worker_connections  150000;
}

http {
include       mime.types;
default_type  application/octet-stream;

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
  '$status $content_length $body_bytes_sent "$http_referer" '
  '"$http_user_agent" "$http_x_forwarded_for" "$upstream_response_time" "$request_time"';

access_log  logs/access.log  main;

server {
listen 8080;
location / {
access_by_lua_block {
local headers = ngx.req.get_headers(0)
local trace_id= headers["X-Trace-Id"]
ngx.log(ngx.ERR, trace_id)
}
#  ngx.say("<p>hello !</p>")
proxy_pass http://168.12.8.10:8080;
}
}

server {
listen 8082;
location / {
default_type text/html;
content_by_lua_block {
ngx.say("<p>Hello Openresty!</p>")
}
}
}
}

nginx.conf 内部分为三层嵌套:
最外层的http,表示处理 HTTP 协议
http 内部 的 server 监听端口,会启动一个 LuaJIT 虚拟机,执行lua代码
同一个端口内部,区分不同的业务功能,采用 location 配置,通过不同的 path 路径,处理不同的业务逻辑。

2、添加环境变量
echo "export PATH=$PATH:/usr/local/openresty/nginx/sbin" >> /etc/profile
source /etc/profile

3、启动 openresty,启动命令和nginx一致
nginx -c /usr/local/openresty/nginx/conf/nginx.conf

4、访问 Web 服务
curl http://localhost:8082/

如果正常,浏览器页面会输出 Hello Openresty!


最新版本:1.13
OpenResty 1.13.6.2 已发布,从这个版本开始,官方将会提供 64 位 Windows 原生二进制包,也为 Ubuntu 18.04 Bionic 提供新的 yum 包。下一个 OpenResty 版本将基于 nginx core 1.13.12 或即将推出的 nginx 1.15.x 系列中的一个版本。更新亮点包括:
对 OpenSSL 1.1.0 系列有完整的官方支持(最新测试版本为 OpenSSL 1.1.0h)
添加新的 flush_all() 方法到 lua-resty-lrucache Lua 库
避免在 Nginx 的辅助进程中运行 Lua VM 实例如“缓存加载器”和“缓存管理器”,以减少这些内存占用进程
更多完整请查看查看更新内容以及发布公告

最新版本:1.19
OpenResty 1.19.9.1 已于2021年8月中旬正式发布,此版本包含了过去几个月所有的优化、bug 修复和新特性:
底层基于较新的 nginx 主线版本 1.19.9
从上游 LuaJIT 仓库引入了许多错误修复程序
引入新的宏LUAJIT_TEST_FIXED_ORDER用于 lua 表的固定(fixed-order)顺序遍历
当 lua 请求内存失败时,会采取调用abort()的方式来处理,而不是进行关闭
get_ctx_table支持使用来自调用者的 ctx 表,可降低创建新 ctx 表的成本
修复使用 lua-tablepool 清除 lua table 内容时,metatable 没有被清除的问题
为了在使用 lua-tablepool 时获得更好性能,当内存池的大小大于 max_pool_size 时丢弃对象
针对 stream 子系统实现ngx.processAPI
此外,官方新增了 alpine 3.14 的 x86_64 和 arm64 的官方仓库包
完整的发布公告请见此处


官方主页:http://openresty.org/

项目主页:https://github.com/openresty