使用lua-nginx模块实现请求解析与调度
系统版本及需求:
OS:CentOS 7.7.1908
OpenResty:1.15.8.2
描述
lua-nginx-module模块是什么:
It is a core component of OpenResty. If you are using this module, then you are essentially using OpenResty.
By leveraging Nginx's subrequests, this module allows the integration of the powerful Lua threads (known as Lua "coroutines") into the Nginx event model.
Unlike Apache's mod_lua and Lighttpd's mod_magnet, Lua code executed using this module can be 100% non-blocking on network traffic as long as the Nginx API for Lua provided by this module is used to handle requests to upstream services such as MySQL, PostgreSQL, Memcached, Redis, or upstream HTTP web services.
OpenResty的核心组件,将lua线程集成到nginx模型中,且不会阻塞网络流量。
可以通过编译将其安装为Nginx Module。本文直接安装OpenResty,通过lua脚本主要实现以下两个目标:
- 将一份请求转发给多个后端,但仅用其中一个后端回复请求。
- 分析一份请求的参数内容,根据规则将请求分发到不同的后端。
通过以上两个目标,也很容易衍生出其他的可能性,例如通过此模块实现根据请求用户的特征将其调度到不同的服务器:以此来达到目标(比如灰度、就近访问、黑白名单等);根据转发多后端特性,实现完全的真实环境压力测试等。
安装配置
安装openresty
通过源码编译安装,具体步骤如下:
yum install -y pcre-devel openssl-devel gcc curl
mkdir -p /data/pkg/ && cd /data/pkg/
wget https://openresty.org/download/openresty-1.15.8.2.tar.gz
tar xf openresty-1.15.8.2.tar.gz
cd openresty-1.15.8.2
./configure --with-file-aio --with-http_ssl_module --with-http_realip_module --with-http_sub_module --with-http_gzip_static_module --with-http_auth_request_module --with-http_stub_status_module
make -j$nproc
make install
编译时Nginx的大多数选项都支持。如上启动了一些Module,具体根据你的需求选择。
默认安装路径/usr/local/openresty,想更改路径通过--prefix=/path指定。
使用示例
创建一个存放脚本的目录:
cd /usr/local/openresty
mkdir nginx/conf/lua
创建一个lua测试脚本:
vim nginx/conf/lua/hello.lua
local action = ngx.var.request_method
if(action == "POST") then
ngx.say("Method: POST; Hello world")
elseif(action == "GET") then
ngx.say("Method: GET; Welcome to the web site")
end
在Server段配置中启用lua脚本:
vim nginx/conf/nginx.conf
在nginx.conf中新增加一个server段,请求路径以/开头的则使用lua脚本进行处理。
server {
listen 0.0.0.0:8080;
location / {
root html;
index index.html index.htm;
}
location ~* ^/(.*)$ {
content_by_lua_file "conf/lua/hello.lua"; # lua script location
}
}
测试效果:
使用./bin/openresty -t命令检查配置无误,然后使用./bin/openresty命令启动服务。
POST和GET请求方式返回不同的相应内容
curl 127.0.0.1:8080
# 返回信息
Method: GET; Welcome to the web site
curl -d "" 127.0.0.1:8080
# 返回信息
Method: POST; Hello world
HTTP请求复制
环境准备妥当,现在通过lua脚本程序配合openresty实现对于HTTP请求的复制。
当一个请求来到openresty服务时,把此请求转发给后端的server1、server2等等,但只是用server1或server2的应答消息回复这个请求。
一个简单的示例图:

需求:将请求同时发送到/prod和/test路径后的真实后端,但只使用/prod的后端回复client的请求
lua代码
vim nginx/conf/lua/copyRequest.lua
function req_copy()
local resp_prod, resp_test = ngx.location.capture_multi {
{"/prod" .. ngx.var.request_uri, arry},
{"/test" .. ngx.var.request_uri, arry},
}
if resp_prod.status == ngx.HTTP_OK then
local header_list = {"Content-Type", "Content-Encoding", "Accept-Ranges"}
for _, i in ipairs(header_list) do
if resp_prod.header[i] then
ngx.header[i] = resp_prod.header[i]
end
end
ngx.say(resp_prod.body)
else
ngx.say("Upstream server error : " .. resp_prod.status)
end
end
req_copy()
nginx新建的server段配置如下,且引入vhosts目录下配置文件,nginx/conf/nginx.conf:
server {
listen 0.0.0.0:8080;
location / {
root html;
index index.html index.htm;
}
# 匹配lua文件中/prod+原请求的uri(/copy/*)
location ^~ /prod/ {
# 路径重写后,/prod后端服务器收到的路径为客户端请求路径去掉开头/copy/
rewrite /prod/copy/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8081;
}
# 匹配lua文件中/test+原请求的uri(/copy/*)
location ^~ /test/ {
# 路径重写后,/prod后端服务器收到的路径为客户端请求路径去掉开头/copy/
rewrite /test/copy/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8082;
}
location ^~ /copy/ {
content_by_lua_file "conf/lua/copyRequest.lua";
}
}
include vhosts/*.conf;
创建两个后端主机,模拟代表不同环境:
# nginx/conf/vhosts/prod.conf
server {
listen 8081;
server_name localhost;
access_log logs/prod_server.log;
location / {
return 200 "Welcome to prod server";
}
location /api/v1 {
return 200 "API V1";
}
}
# nginx/conf/vhosts/test.conf
server {
listen 8082;
server_name localhost;
access_log logs/test_server.log;
location / {
return 200 "Welcome to test server";
}
}
配置更新后重载服务,然后测试访问:
> curl 127.0.0.1:8080/copy/
Welcome to prod server
> curl 127.0.0.1:8080/copy/api/v1
API V1
# /prod和/test后端的服务都收到了请求
# 查看日志;tail -2 nginx/logs/prod_server.log
127.0.0.1 - - [01/Mar/2020:01:41:12 +0800] "GET / HTTP/1.0" 200 22 "-" "curl/7.29.0"
127.0.0.1 - - [01/Mar/2020:01:41:15 +0800] "GET /api/v1 HTTP/1.0" 200 6 "-" "curl/7.29.0"
# 查看日志;tail -2 nginx/logs/test_server.log
127.0.0.1 - - [01/Mar/2020:01:41:12 +0800] "GET / HTTP/1.0" 200 22 "-" "curl/7.29.0"
127.0.0.1 - - [01/Mar/2020:01:41:15 +0800] "GET /api/v1 HTTP/1.0" 200 22 "-" "curl/7.29.0"
模拟场景图示:

- client访问nginx服务监听的
IP:8080/copy/路径 - lua脚本处理收到的请求,代访问
/prod和/test路径 /prod和/test路径在本地被代理到真实后端- 真实后端接收到请求并返回
- lua脚本仅处理
/prod(resp_prod)后端服务器返回的内容(见lua脚本中代码) - 将/prod后端服务器返回的内容返回给client
HTTP报文解析
需求:将请求报文参数中的userid在某个区间的请求调度到指定的后端服务上。
创建nginx/conf/lua/requestBody.lua代码:
-- post body提交方式为application/x-www-form-urlencoded的内容获取方法
function urlencodedMethod()
local postBody = {}
for key, val in pairs(args) do
postBody[key] = val
end
local uid = postBody["userid"]
postBody = nil
return tonumber(uid)
end
-- get请求方式为xx.com/?userid=x其params的获取方式
function uriParameterMethod()
local getParameter = {}, key, val
for key, val in pairs(args) do
if type(val) == "table" then
getParameter[key] = table.concat(val)
else
getParameter[key] = val
end
end
local uid = getParameter["userid"]
getParameter = nil
return tonumber(uid)
end
-- 获取post body提交的方式;multipart/from-data在此示例中没有实现对其内容的处理
function contentType()
local conType = ngx.req.get_headers()["Content-Type"]
local conTypeTable = {"application/x-www-form-urlencoded", "multipart/form-data"}
local receiveConType, y
if(type(conType) == "string") then
for y = 1, 2 do
local word = conTypeTable[y]
local from, to, err = ngx.re.find(conType, word, "jo")
if from and to then
receiveConType = string.sub(conType, from, to)
end
end
else
receiveConType = nil
end
return receiveConType
end
-- 循环出一些需要的header返回给客户端
function iterHeaders(resp_content)
local header_list = {"Content-Type", "Content-Encoding", "Accept-Ranges","Access-Control-Allow-Origin", "Access-Control-Allow-Methods","Access-Control-Allow-Headers", "Access-Control-Allow-Credentials"}
for _, i in ipairs(header_list) do
if(resp_content.header[i]) then
ngx.header[i] = resp_content.header[i]
end
end
return resp_content
end
-- 将userid大于等于1小于等于10的请求发送给/prod路径的后端
-- 将userid大于等于11小于等于20的请求发送给/test路径的后端
-- 将userid非以上两种的同时发送给/prod和/test路径的后端,使用/prod后端回复请求
function requestTo(uid)
local resp, resp_noReply
if(uid >= 1 and uid <= 10) then
resp = ngx.location.capture_multi {
{"/prod".. ngx.var.request_uri, arry},
}
elseif(uid >= 11 and uid <= 20) then
resp = ngx.location.capture_multi {
{"/test".. ngx.var.request_uri, arry},
}
else
resp, resp_noReply = ngx.location.capture_multi {
{"/prod" .. ngx.var.request_uri, arry},
{"/test" .. ngx.var.request_uri, arry},
}
end
local res
if(resp.status == ngx.HTTP_OK) then
res = iterHeaders(resp)
else
res = "Upstream server err : " .. reps_content.status
end
ngx.say(res.body)
end
-- 处理主函数
function main_func()
ngx.req.read_body()
local action = ngx.var.request_method
if(action == "POST") then
arry = {method = ngx.HTTP_POST, body = ngx.req.read_body()}
args = ngx.req.get_post_args()
elseif(action == "GET") then
args = ngx.req.get_uri_args()
arry = {method = ngx.HTTP_GET}
end
local u
if(action == "POST") then
if args then
local getContentType = contentType()
if(getContentType == "application/x-www-form-urlencoded") then
u = urlencodedMethod()
end
end
elseif(action == "GET") then
if args then
u = uriParameterMethod()
end
end
if(u == nil) then
ngx.say("Request parameter cannot be empty: userid<type: int>")
else
requestTo(u)
end
end
main_func()
配置nginx/conf/nginx.conf文件,修改新增的server段内容如下:
server {
listen 0.0.0.0:8080;
location / {
root html;
index index.html index.htm;
}
location ^~ /prod/ {
rewrite /prod/request/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8081;
}
location ^~ /test/ {
rewrite /test/request/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8082;
}
location ^~ /request/ {
content_by_lua_file "conf/lua/requestBody.lua";
}
}
include vhosts/*.conf;
使用上个示例中的两个后端服务,重载服务,测试效果:
> curl -X POST -d 'userid=1' 127.0.0.1:8080/request/
Welcome to prod server
> curl -X POST -d 'userid=11' 127.0.0.1:8080/request/
Welcome to test server
> curl -X POST -d 'userid=21' 127.0.0.1:8080/request/
Welcome to prod server
> curl 127.0.0.1:8080/request/?userid=1
Welcome to prod server
> curl 127.0.0.1:8080/request/?userid=11
Welcome to test server
> curl 127.0.0.1:8080/request/?userid=21
Welcome to prod server
请求过程解析:
- client访问nginx服务监听的
IP:8080/copy/路径 - lua脚本处理收到的请求,根据其请求参数中userid进行对后端请求的调度
- 各路径后的真实后端接受请求并处理返回
- nginx(lua)将后端服务器返回的内容返回给client
总结
tips:
- nginx配置正确性检测不会检测lua脚本的正确性
- lua脚本的错误会记录到error.log中
- 调试时可使用ngx.log方法进行调试
通过使用lua-nginx-module扩展增强nginx处理能力,可以根据自身的业务需求开发脚本,实现针对请求的方式、参数等内容进行按需调度。
使用lua-nginx模块实现请求解析与调度的更多相关文章
- OpenResty / Nginx模块,Lua库和相关资源的列表
OpenResty / Nginx模块,Lua库和相关资源的列表 什么是OpenResty OpenResty是一个成熟的网络平台,它集成了标准的Nginx核心,LuaJIT,许多精心编写的Lua库, ...
- 【精选】Nginx模块Lua-Nginx-Module学习笔记(二)Lua指令详解(Directives)
源码地址:https://github.com/Tinywan/Lua-Nginx-Redis Nginx与Lua编写脚本的基本构建块是指令. 指令用于指定何时运行用户Lua代码以及如何使用结果. 下 ...
- Nginx模块Lua-Nginx-Module学习笔记(二)Lua指令详解(Directives)
源码地址:https://github.com/Tinywan/Lua-Nginx-Redis Nginx与Lua编写脚本的基本构建块是指令. 指令用于指定何时运行用户Lua代码以及如何使用结果. 下 ...
- Nginx重要结构request_t解析之http请求的获取
请在文章页面明显位置给出原文连接,否则保留追究法律责任的权利. 本文主要参考为<深入理解nginx模块开发与架构解析>一书,处理用户请求部分,是一篇包含作者理解的读书笔记.欢迎指正,讨论. ...
- 【精选】Nginx模块Lua-Nginx-Module学习笔记(一)Nginx Lua API 接口详解
源码地址:https://github.com/Tinywan/Lua-Nginx-Redis 一.介绍 各种* _by_lua,* _by_lua_block和* _by_lua_file配置指令用 ...
- Nginx模块开发与架构解析(nginx安装、配置说明)
第一章 研究nginx前的准备工作 Linux操作系统需要2.6及其以上的内核(支持epoll) 使用nginx的必备软件 Linux内核参数优化方案 安装nginx 控制nginx 第二章 ngin ...
- Nginx模块Lua-Nginx-Module学习笔记(一)Nginx Lua API 接口详解
源码地址:https://github.com/Tinywan/Lua-Nginx-Redis 一.介绍 各种* _by_lua,* _by_lua_block和* _by_lua_file配置指令用 ...
- 【重要】Nginx模块Lua-Nginx-Module学习笔记(三)Nginx + Lua + Redis 已安装成功(非openresty 方式安装)
源码地址:https://github.com/Tinywan/Lua-Nginx-Redis 一. 目标 使用Redis做分布式缓存:使用lua API来访问redis缓存:使用nginx向客户端提 ...
- nginx源代码分析--nginx模块解析
nginx的模块很之多.能够觉得全部代码都是以模块的形式组织.这包含核心模块和功能模块,针对不同的应用场合.并不是全部的功能模块都要被用到,附录A给出的是默认configure(即简单的httpser ...
随机推荐
- 使用VisualStudio直接运行简单的C#语句
场景 经常有这样的需求, 想要测试一些简单的C#语法, 或者测试一下 文件 目录 操作相关的Api, 通常的做法是建立一个C#控制台项目, 然后写代码测试, 但是这样的做法对测试简单的语法和Api来说 ...
- vue开发东京买菜,全栈项目,前端django,带手机GPS精准定位,带发票系统,带快递系统,带微信/支付宝/花呗/银行卡支付/带手机号一键登陆,等等
因为博客园不能发视频,所以,完整的视频,开发文档,源码,请向博主索取 完整视频+开发文档+源码,duanshuiLu.com下载 vue+django手机购物商城APP,带支付,带GPS精准定位用户, ...
- Codeforces Round #625 (Div. 2, based on Technocup 2020 Final Round) D. Navigation System(有向图,BFS,最短路)
题意: n 点 m 边有向图,给出行走路径,求行走途中到路径终点最短路变化次数的最小值和最大值 . 思路 : 逆向广搜,正向模拟. #include <bits/stdc++.h> usi ...
- Codeforces Round #570 (Div. 3) B. Equalize Prices、C. Computer Game、D. Candy Box (easy version)、E. Subsequences (easy version)
B题题意: 给你n个物品的价格,你需要找出来一个值b,使得每一个物品与这个b的差值的绝对值小于k.找到最大的b输出,如果找不到,那就输出-1 题解: 很简单嘛,找到上下限直接二分.下限就是所有物品中最 ...
- CodeForces - 1209D 并查集
题意: 有 n个不同的糖果,从 1到 n编号.有 k个客人.要用糖果招待客人.对于每个客人,这些糖果中恰有两个是其最爱.第 i个客人最爱的糖果编号是 xi和 y.将 k 个客人任意排列,他们按顺序去拿 ...
- Python内置模块(你还在pip install time?)&& apt-get install -f
一.内置模块 之前不知道time是python自带的,还用pip安装.......还报错..... Python中有以下模块不用单独安装 1.random模块 2.sys模块 3.time模块 4.o ...
- 在kubernetes集群里集成Apollo配置中心(5)之dubbo服务消费者连接apollo实战
1.在Apollo的portal创建dubbo消费者项目 (1)添加dubbo消费者项目 (2)在dubbo消费者项目中添加配置项 (3)发布 2.通过jenkins构建dubbo消费者镜像 3.登录 ...
- oslab oranges 一个操作系统的实现 实验二 认识保护模式
https://github.com/yyu/osfs00 实验目的: 理解x86架构下的段式内存管理 掌握实模式和保护模式下段式寻址的组织方式. 关键数据结构.代码组织方式 掌握实模式与保护模式的切 ...
- 2019牛客多校第六场H Pair(数位DP 多个数相关)题解
题意: 传送门 给你\(A,B,C\),要求你给出有多少对\((x, y)\)满足\(x\in [1,A],y\in [1,B]\),且满足以下任意一个条件:\(x \& y > C\) ...
- HDU 6623 Minimal Power of Prime(思维)题解
题意: 已知任意大于\(1\)的整数\(a = p_1^{q_1}p_2^{q_2} \cdots p_k^{q_k}\),现给出\(a \in [2,1e18]\),求\(min\{q_i\},q ...