Nginx结合Lua做动态加载(四)
#紧接上文,我们consul的使用已经总结的差不多了,但是是不是感觉没有展现一个实际的场景来把consul结合起来使其作用更大化呢?
我下面描述一个具体的场景:一般我们会有一套稳定环境,但是会有N多套测试需求环境,而且这是一个动态变化的过程,这里我们需求用需求号代替,比如今天测试一个集群的功能创建了一个2001的需求,后天又创建了一个2002的需求,那么正常来说你访问网站是不是就变成了aaa-2001.xxx.com/aaa-2002.xxx.com,这就带来一个问题测试起来太繁琐了(每次测试你都要更换URL),现在很多都是app测试了,app更换域名还是挺麻烦的。还有一个问题现在很多都是微服务架构,就是你是不是要1比1的部署测试环境,不然你的测试流程很可能走不下去。
那么我们怎么解决上述问题呢,一方面让测试更便捷另一方面让成本更低,我们可以试想一下:先提供一个web平台用户可以选择自己要访问哪个环境,然后nginx+lua成为网关,一个固定域名基于来源来判断要访问哪个环境并路由,然后后端是nginx+consul将请求转发到执行的需求环境容器,然后需求号再作为tag一直透传下去,有对应的需求就将请求交给对应的需求容器,没有对应的需求容器就将请求交给稳定环境是不是就解决了我们上面提到的问题,好了大体思路有了我们去实现它。
一、Nginx+Lua简单使用
1.1 简单方式直接使用openresty
1.1.1 openresty安装
#官网:https://openresty.org/en/
# yum install -y readline-devel pcre-devel openssl-devel
# wget https://openresty.org/download/openresty-1.27.1.1.tar.gz
# tar xf openresty-1.27.1.1.tar.gz
# cd openresty-1.27.1.1/
# ./configure --prefix=/opt/soft/openresty --with-luajit --with-http_stub_status_module --with-pcre --with-pcre-jit
# gmake && gmake install
# vim /opt/soft/openresty/nginx/conf/nginx.conf #在server区域中加上这么一句调用lua
location /hello { default_type text/html; content_by_lua 'ngx.say("hello,lua scripts")'; }
# /opt/soft/openresty/nginx/sbin/nginx
# curl 127.0.0.1/hello #直接curl调用测试一下会看到有内容数据,说明lua调用成功
hello,lua scripts
1.2 nginx编译安装支持lua
安装luajit
# wget https://codeload.github.com/openresty/luajit2/tar.gz/refs/tags/v2.1-20250117
# tar xf luajit2-2.1-20250117.tar.gz
# cd luajit2-2.1-20250117
# make install PREFIX=/usr/local/luajit
# vim /etc/profile.d/lua.sh
export LUAJIT_LIB=/usr/local/luajit/lib export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1
# source /etc/profile.d/lua.sh
# echo "/usr/local/luajit/lib/" >>/etc/ld.so.conf.d/lua.conf
# ldconfig
#首先安装2.1版本的lua,因为我们是用OpenResty得lua-resty-core会提示需要luajit2.1,下面为报错提醒[注意但是不影响nginx启动,所以大家常用的https://codeload.github.com/LuaJIT/LuaJIT/tar.gz/refs/tags/v2.0.4 还是可以用的 ]:
nginx: [alert] detected a LuaJIT version which is not OpenResty's; many optimizations will be disabled and performance will be compromised (see https://github.com/openresty/luajit2 for OpenResty's LuaJIT or, even better, consider using the OpenResty releases from https://openresty.org/en/download.html) nginx: [alert] [lua] base.lua:42: use of lua-resty-core with LuaJIT 2.0 is not recommended; use LuaJIT 2.1+ instead nginx: [alert] [lua] lrucache.lua:25: use of lua-resty-lrucache with LuaJIT 2.0 is not recommended; use LuaJIT 2.1+ instead
#然后你说我安装官网的 https://github.com/LuaJIT/LuaJIT/archive/refs/tags/v2.1.ROLLING.tar.gz 好了吧,但是还是会有提醒依旧不会影响你服务启动,下面提醒:
nginx: [alert] detected a LuaJIT version which is not OpenResty's; many optimizations will be disabled and performance will be compromised (see https://github.com/openresty/luajit2 for OpenResty's LuaJIT or, even better, consider using the OpenResty releases from https://openresty.org/en/download.html)
安装需要的lua模块
#wget https://codeload.github.com/openresty/lua-nginx-module/tar.gz/refs/tags/v0.10.28
#tar xf lua-nginx-module-0.10.28.tar.gz
#wget https://codeload.github.com/openresty/lua-resty-core/tar.gz/refs/tags/v0.1.31
#tar xf lua-resty-core-0.1.31.tar.gz
#cd lua-resty-core-0.1.31
#make install PREFIX=/usr/local/lua_core
#wget https://codeload.github.com/openresty/lua-resty-lrucache/tar.gz/refs/tags/v0.15
#tar xf lua-resty-lrucache-0.15.tar.gz
#cd lua-resty-lrucache
#make install PREFIX=/usr/local/lua_core
安装nginx并配置lua进行测试
# yum install pcre pcre-devel openssl openssl-devel gd gd-devel -y
#wget https://nginx.org/download/nginx-1.28.0.tar.gz
#tar xf nginx-1.28.0.tar.gz
# cd nginx-1.28.0/
#./configure --prefix=/opt/soft/nginx --sbin-path=/opt/soft/nginx/sbin/nginx --conf-path=/opt/soft/nginx/main-conf/nginx.conf --error-log-path=/opt/log/nginx/error.log --http-log-path=/opt/log/nginx/access.log --pid-path=/opt/soft/nginx/run/nginx.pid --lock-path=/opt/soft/nginx/run/nginx.lock --user=work --group=work --http-client-body-temp-path=/opt/soft/nginx/cache/client_temp --http-proxy-temp-path=/opt/soft/nginx/cache/proxy_temp --http-fastcgi-temp-path=/opt/soft/nginx/cache/fastcgi_temp --http-uwsgi-temp-path=/opt/soft/nginx/cache/uwsgi_tmp --http-scgi-temp-path=/opt/soft/nginx/cache/scgi_temp --with-http_v2_module --with-http_stub_status_module --with-http_ssl_module --with-http_realip_module --with-http_sub_module --with-http_gzip_static_module --with-pcre --with-http_addition_module --with-http_image_filter_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_gzip_static_module --with-file-aio --with-http_random_index_module --with-ld-opt=-Wl,-rpath,/usr/local/luajit/lib --add-module=/opt/soft/package/nginx_lua/lua-nginx-module-0.10.28
#make -j 8
#make install
# vim /opt/soft/nginx/main-conf/nginx.conf #增加一条lua测试location
location /lua { default_type 'text/plain'; content_by_lua 'ngx.say("hello, lua")'; } location / { root html; index index.html index.htm; }
#mkdir /opt/soft/nginx/cache
#/opt/soft/nginx/sbin/nginx -t #测试是没有报错的所以语法没问题
#/opt/soft/nginx/sbin/nginx #报错如下:
nginx: [alert] failed to load the 'resty.core' module (https://github.com/openresty/lua-resty-core); ensure you are using an OpenResty release from https://openresty.org/en/download.html (reason: module 'resty.core' not found: no field package.preload['resty.core'] no file './resty/core.lua' no file '/usr/local/luajit/share/luajit-2.1/resty/core.lua' no file '/usr/local/share/lua/5.1/resty/core.lua' no file '/usr/local/share/lua/5.1/resty/core/init.lua' no file '/usr/local/luajit/share/lua/5.1/resty/core.lua' no file '/usr/local/luajit/share/lua/5.1/resty/core/init.lua' no file './resty/core.so' no file '/usr/local/lib/lua/5.1/resty/core.so' no file '/usr/local/luajit/lib/lua/5.1/resty/core.so' no file '/usr/local/lib/lua/5.1/loadall.so' no file './resty.so' no file '/usr/local/lib/lua/5.1/resty.so' no file '/usr/local/luajit/lib/lua/5.1/resty.so' no file '/usr/local/lib/lua/5.1/loadall.so') in /opt/soft/nginx/main-conf/nginx.conf:119
# vim /opt/soft/nginx/main-conf/nginx.conf #开启引用前面编译的lua模块
#gzip on; #就是增加下面这一段 lua_package_path "/usr/local/lua_core/lib/lua/?.lua;;";
# /opt/soft/nginx/sbin/nginx #启动没有再报错
# curl 127.0.0.1/lua #测试输出正常
hello, lua
1.3 nginx调用lua的简单介绍
lua在nginx的指令执行顺序:
我们顺着上面的流程图把贯穿整个 Nginx生命周期的Lua 脚本执行链路,也就是Nginx Lua 模块提供了一系列钩子函数(hook),允许我们在 Nginx 的不同阶段执行 Lua 脚本介绍一下。以下是这些钩子的主要作用和调用顺序:
#一、初始化阶段 init_by_lua钩子函数 #全局初始化阶段,这是Nginx启动时最早的一个阶段,发生在主进程(master process)启动期间,且只执行一次。 init_worker_by_lua钩子函数 #Worker初始化阶段,在每个worker进程启动时执行,每个worker 进程都会独立运行一次,初始化的内容仅对当前 worker进程有效 #二、TLS(传输层安全协议)相关的钩子函数主要用于处理与TLS 握手、证书动态加载以及客户端认证等相关的逻辑 ssl_client_hello_by_lua #TLS客户端握手阶段,在收到客户端的TLS ClientHello消息时触发,发生在SSL/TLS握手的最早阶段 ssl_certificate_by_lua #TLS证书动态加载阶段,在TLS握手期间,当客户端请求连接时触发。发生在SSL/TLS握手之前 ssl_session_fetch_by_lua #TLS会话恢复阶段,在TLS握手期间,尝试恢复之前保存的TLS会话时触发。发生在SSL/TLS握手之前 ssl_session_store_by_lua #TLS会话存储阶段,在TLS握手完成后,将新的TLS会话保存到外部存储时触发 #三、HTTP level阶段,Nginx处理HTTP请求的各个阶段 server_rewrite_by_lua #这个阶段主要用于URL重写和复杂的逻辑处理,如访问控制或动态内容生成 set_by_lua #允许通过Lua脚本计算并设置变量值。它通常用于实现复杂的逻辑处理,并返回结果,在请求的rewrite和access阶段之前完成 rewrite_by_lua #用于在Nginx的rewrite阶段执行Lua脚本,主要用于URL重写和转发等功能 access_by_lua #用于在访问阶段执行用户自定义的Lua代码,在请求到达后端服务前进行权限检查,可以用来做权限管理/动态路由/限流控制/黑名单机制 content_by_lua #处理请求内容并生成响应 balancer_by_lua #在Nginx的负载均衡阶段动态选择上游服务器,它主要用于实现复杂的、自定义的负载均衡逻辑,例如基于请求参数、哈希算法或动态服务发现来选择后端服务器。 header_filter_by_lua #允许用户在HTTP响应头发送给客户端之前执行自定义Lua脚本。这一阶段的主要用途是修改或设置响应头 body_filter_by_lua #用于在响应体发送到客户端之前对其进行修改。它允许开发者以流式处理的方式操作响应内容 log_filter_by_lua #允许使用Lua脚本在日志记录阶段执行自定义逻辑,可用于统计分析或添加额外信息到日志中 #四、Worker进程退出阶段 exit_worker_by_lua #当Nginx Worker进程需要关闭时(例如服务器重启、重新加载配置或正常关闭时),这一阶段适合用于清理资源、记录日志或通知外部系统等操作
Nginx调⽤ Lua 模块指令,Nginx的可插拔模块加载执⾏:
set_by_lua set_by_lua_file #设置Nginx变量,可以实现负载的赋值逻辑 access_by_lua access_by_lua_file #请求访问阶段处理, ⽤于访问控制 content_by_lua content_by_lua_file #内容处理器, 接受请求处理并输出响应
Nginx 调⽤ Lua API:
ngx.var #nginx变量 ngx.req.get_headers #获取请求头 ngx.req.get_uri_args #获取url请求参数 ngx.redirect #重定向 ngx.print #输出响应内容体 ngx.say #输出响应内容体,最后输出⼀个换⾏符 ngx.header #输出响应头
1.4 lua在nginx中的一些阶段简单用法介绍
1.4.1 初始阶段例子介绍
init_by_lua例子介绍:
# vim nginx.conf
--init_by_lua块:在这里我们定义了一个全局变量_G.my_global_variable和一个全局函数_G.say_hello init_by_lua ' -- 定义一个全局变量 _G.my_global_variable = "Hello from init_by_lua" -- 定义一个全局函数 _G.say_hello = function(name) return "Hello, " .. name .. "! This message is from init_by_lua." end '; server { ...... location /init_by_lua { --- content_by_lua块:在处理HTTP请求时,我们通过ngx.say输出了这个全局变量和调用了这个全局函数 content_by_lua ' -- 使用预加载的全局变量 ngx.say(_G.my_global_variable) -- 调用预加载的全局函数 local greeting = _G.say_hello("World") ngx.say(greeting) '; }
# curl 127.0.0.1/init_by_lua #访问一下查看效果
Hello from init_by_lua Hello, World! This message is from init_by_lua.
init_worker_by_lua例子介绍:
# vim nginx.conf
-- init_worker_by_lua块:当每个worker进程启动时,这段Lua代码会被执行一次。记录了每个worker进程的PID到Nginx的日志中,并且将这个PID存储到了全局变量 _G.worker_pid 中。 init_worker_by_lua ' -- 引入lua库,我们使用了ngx.log函数来记录不同级别的日志消息(如ERROR、WARN、INFO)。 local ngx_log = ngx.log local ngx_ERR = ngx.ERR local ngx_WARN = ngx.WARN local ngx_INFO = ngx.INFO -- 每个worker进程启动时输出一条日志 ngx_log(ngx_INFO, "Worker process started with pid: ", ngx.worker.pid()) -- 可以在这里初始化worker级别的一些变量或连接池等 _G.worker_pid = ngx.worker.pid() '; server { location /init_worker_by_lua { -- content_by_lua块:在处理HTTP请求时,可以访问在init_worker_by_lua中初始化的全局变量 _G.worker_pid,并将其作为响应的一部分返回给客户端 content_by_lua ' -- 在处理请求时可以使用worker级别初始化的数据 ngx.say("This request is handled by worker with pid: ", _G.worker_pid) '; }
# curl 127.0.0.1/init_worker_by_lua
This request is handled by worker with pid: 2871584
1.4.2 HTTP Level阶段介绍
rewrite_by_lua例子介绍:
#rewrite_by_lua 是早期的语法形式,直接将 Lua 脚本作为字符串传入。rewrite_by_lua_block 是更现代的形式,支持多行脚本编写,可读性更高。
#如果你的 Nginx 版本较新,建议使用 rewrite_by_lua_block,因为它更易读、更易维护。
# vim /opt/soft/nginx/main-conf/nginx.conf
location / { rewrite_by_lua ' local uri = ngx.var.uri ngx.log(ngx.ERR, "Original URI: ", uri) -- 判断是否以 /api/v2/ 开头 if not string.match(uri, "^/api/v2/") then -- 如果不是,则重写为 /api/v2/ 前缀 local new_uri = "/api/v2" .. uri ngx.log(ngx.ERR, "Rewritten URI: ", new_uri) ngx.req.set_uri(new_uri, true) -- 设置新的 URI 并应用 end '; #将请求转发到后端服务 proxy_pass http://192.168.1.101; }
# /opt/soft/nginx/sbin/nginx -s reload
# curl 127.0.0.1/api/abc
# tail -f /opt/log/nginx/access.log #当前lua机器查看日志
"GET /api/abc HTTP/1.1" 404 153 "-" "curl/7.61.1"
# tail -f /usr/local/nginx/logs/access.log #登录后台机器查看请求日志,可以看到url被重写请求过来了
"GET /api/v2/api/abc HTTP/1.0" 404 153 "-" "curl/7.61.1"
server_rewrite_by_lua例子介绍:
#例子描述:检查请求的 URI 是否以 /api/v2/ 开头,如果不是,则将原始 URI 拼接到 /api/v2 后面形成新的 URI,将重写后的 URI 应用到当前请求中。
#通过这种方式,可以确保所有请求都以 /api/v2/ 开头,从而实现统一的 API 路径管理
# vim /opt/soft/nginx/main-conf/nginx.conf
location /api { -- 因为server_rewrite_by_lua提示不认,所以更换为rewrite_by_lua_block rewrite_by_lua_block { ngx.log(ngx.ERR, "Original URI: ", ngx.var.uri) -- 打印调试日志 -- 避免重复嵌套,如果URI不以/api/v2/开头,则进入if 块内的逻辑;否则跳过该逻辑 if not string.match(ngx.var.uri, "^/api/v2/") then -- 这一行的作用是将原始URI拼接到/api/v2后面,形成一个新的 URI local new_uri = "/api/v2" .. ngx.var.uri ngx.log(ngx.ERR, "Rewritten URI: ", new_uri) -- 打印重写后的 URI -- 设置新的URI,并应用重写规则,第二个参数true表示强制更新 URI,即使原始 URI 已经被重写过 ngx.req.set_uri(new_uri, true) end } }
# /opt/soft/nginx/sbin/nginx -s reload
# curl 127.0.0.1/api/reource
# tail -f /opt/log/nginx/error.log #因为是例子,所以访问资源不存在,直接看错误日志就行,从例子可看出这个请求是进入到url重写然后再出去重新进入判断然后响应
[error] 168085#0: *730 [lua] rewrite_by_lua(nginx.conf:46):2: Original URI: /api/reource, client: 127.0.0.1, server: localhost, request: "GET /api/reource HTTP/1.1", host: "127.0.0.1" [error] 168085#0: *730 [lua] rewrite_by_lua(nginx.conf:46):5: Rewritten URI: /api/v2/api/reource, client: 127.0.0.1, server: localhost, request: "GET /api/reource HTTP/1.1", host: "127.0.0.1" [error] 168085#0: *730 [lua] rewrite_by_lua(nginx.conf:46):2: Original URI: /api/v2/api/reource, client: 127.0.0.1, server: localhost, request: "GET /api/reource HTTP/1.1", host: "127.0.0.1" [error] 168085#0: *730 open() "/opt/soft/nginx/html/api/v2/api/reource" failed (2: No such file or directory), client: 127.0.0.1, server: localhost, request: "GET /api/reource HTTP/1.1", host: "127.0.0.1"
set_by_lua例子介绍:
set_by_lua和set_by_lua_file区别介绍:
set_by_lua 和 set_by_lua_file :这两个模块都⽤于设置 Nginx 变量。 set_by_lua 通过 inline Lua 代码设置变量的值, set_by_lua_file 则可以通过引⼊ Lua 脚本⽂件来设置变量。这两个模块通常⽤于实现负载的赋值逻辑,如根据请求的 headers 头部信息等进⾏动态变量设置。
先来一个set_by_lua的例子:
location ~ /ftest { #定义一个 Nginx 变量 $input,其初始值为 "Hello, Lua!" set $input "Hello,Lua!"; #通过 Lua 脚本对变量$input使用string.upper 函数将字符串转换为大写,并将结果赋值给变量 $output set_by_lua $output 'return string.upper(ngx.var.input)'; return 200 "Input: $input, Output: $output\n"; }
# curl 127.0.0.1/ftest
Input: Hello,Lua!, Output: HELLO,LUA!
再来一个set_by_lua_block的例子(动态生成变量):
location /time { #用于动态生成变量 $current_time 的值 set_by_lua_block $current_time { #获取当前时间并格式化为标准的日期时间字符串 return os.date("!%Y-%m-%d %H:%M:%S") } #将生成的时间作为响应头的一部分返回 add_header X-Current-Time $current_time; return 200 "The current time is: $current_time\n"; }
再来一个set_by_lua_file的简单例子:
location /multiply { # 定义一个变量 $result 来存储 Lua 脚本的返回值 set $result ""; # 调用外部Lua脚本进行计算,将$arg_number(即 URL参数number的值)作为参数传递给Lua脚本并将返回值赋值给变量$result set_by_lua_file $result '/opt/soft/openresty/nginx/conf/lua/multiply.lua' $arg_number; # 将计算结果返回给客户端 return 200 "Result: $result\n"; }
# \vi /opt/soft/openresty/nginx/conf/lua/multiply.lua
-- 获取第一个参数(即$arg_number的值),并尝试将其转换为数字类型 local number = tonumber(ngx.arg[1]) -- 如果参数无效或无法转换为数字,则返回错误信息 "Invalid input" if not number then return "Invalid input" end -- 将数字乘以2,并将结果转换为字符串类型后返回 return tostring(number * 2)
# curl 127.0.0.1//multiply?number=6
Result: 12
# curl 127.0.0.1//multiply?number=dadssa
Result: Invalid input
access_by_lua_block例子介绍:
#同上,access_by_lua得写法适合简单字符串的形式,access_by_lua_block是后来支持的新特性支持直接编写多行Lua 代码,适合复杂的逻辑场景。更直观、更易维护
#写个简单针对token认证的例子.为了确保所有访问受保护资源的请求都经过身份验证,可以使用 access_by_lua 来实现以下功能
# vim /opt/soft/nginx/main-conf/nginx.conf
location /protected { access_by_lua_block { local token = ngx.req.get_headers()["Authorization"] -- 检查是否有 Authorization 头部 if not token then ngx.status = 401 ngx.header["Content-Type"] = "application/json" ngx.say('{"error": "Unauthorized: Missing token"}') -- ngx.HTTP_UNAUTHORIZED是OpenResty中Nginx Lua模块的一个函数调用,用于立即终止当前请求的处理,并返回指定的 HTTP 状态码给客户端。 ngx.exit(ngx.HTTP_UNAUTHORIZED) end -- 假设简单的验证逻辑:token 必须以 "Bearer" 开头 if not string.match(token, "^Bearer") then ngx.status = 402 ngx.header["Content-Type"] = "application/json" ngx.say('{"error": "Unauthorized: Invalid token format"}') ngx.exit(ngx.HTTP_UNAUTHORIZED) end -- 进一步的验证逻辑可以在这里添加,例如解码 JWT 并验证签名 ngx.log(ngx.ERR, "Token is valid, allowing request") } #上面都if判断后没有退出自然就流转到了后端响应接口 proxy_pass http://192.168.1.102; }
# /opt/soft/nginx/sbin/nginx -s reload
# curl -I 127.0.0.1/protected #无token请求
HTTP/1.1 401 Unauthorized
# curl -H "Authorization: tokentest" 127.0.0.1/protected #token不规范请求
{"error": "Unauthorized: Invalid token format"}
# curl -H "Authorization: Bearer aabbccdd" 127.0.0.1/protected #正常请求
content_by_lua_block例子介绍:
#content_by_lua是 OpenResty中的一个指令,用于在 Nginx 的content 阶段嵌入Lua 脚本代码,它通常用来动态生成 HTTP响应内容。假设现在要实现一个 RESTful API,用于返回当前服务器的时间。如果用户提供了有效的 API Token,则返回时间;否则返回 401 Unauthorized 错误。
# vim /opt/soft/nginx/main-conf/nginx.conf
location /api/time { content_by_lua_block { -- 获取请求头中的 Authorization 字段 local authorization = ngx.req.get_headers()["Authorization"] -- 检查是否提供了有效的 Token if not authorization or authorization ~= "Bearer" then ngx.status = ngx.HTTP_UNAUTHORIZED ngx.say("Unauthorized: Please provide a valid token") ngx.exit(ngx.HTTP_UNAUTHORIZED) end -- 如果通过了身份验证,返回当前服务器时间 local time = os.date("!%Y-%m-%d %H:%M:%S") ngx.say("Current server time: ", time) } }
# /opt/soft/nginx/sbin/nginx -s reload
# curl -H "Authorization: xxxxyyyy" 127.0.0.1/api/time
Unauthorized: Please provide a valid token
# curl -H "Authorization: Bearer " 127.0.0.1/api/time #注意这是UTC时间,如果想显示北京时区的时间并且服务配置的是北京时区就用local time = os.date("%Y-%m-%d %H:%M:%S")
Current server time: 2025-03-26 07:39:10
header_filter_by_lua_block例子介绍:
# vim /opt/soft/nginx/main-conf/nginx.conf
location / { content_by_lua_block { ngx.say("Hello, World!") } header_filter_by_lua_block { -- 动态设置 X-Custom-Header 的值 if ngx.var.uri == "/" then ngx.header["X-Custom-Header"] = "This is the root path" elseif ngx.var.uri == "/about" then ngx.header["X-Custom-Header"] = "This is the about page" else ngx.header["X-Custom-Header"] = "Unknown path" end } } location /about { content_by_lua_block { ngx.say("About Page") } header_filter_by_lua_block { ngx.header["X-Custom-Header"] = "Custom value for about page" } }
# /opt/soft/nginx/sbin/nginx -s reload
# curl -I http://localhost/
X-Custom-Header: This is the root path
# curl -I http://localhost/dsada
X-Custom-Header: Unknown path
# curl -I http://localhost/about
X-Custom-Header: Custom value for about page
body_filter_by_lua_block例子介绍:
#body_filter_by_lua_block指令允许用户通过 Lua 脚本对HTTP响应体进行修改或处理,主要用途:修改响应内容/分块传输处理/后处理逻辑(在响应发送给客户端之前,执行一些额外的逻辑。例如,压缩响应内容、加密数据或记录日志)
#工作原理: ngx.arg[1]: 表示当前块的响应体数据(字符串类型)/ngx.arg[2]: 表示是否为最后一块数据(布尔值)。修改 ngx.arg[1] 的值会影响最终发送给客户端的响应体内容。body_filter_by_lua_block会影响每个请求的响应阶段,确保脚本逻辑尽量简单以避免性能问题
# vim /opt/soft/nginx/main-conf/nginx.conf
location /body { content_by_lua_block { ngx.say("Hello, this is the original response body.") } body_filter_by_lua_block { -- 检查是否有剩余的响应数据 if ngx.arg[2] then -- 如果是最后一块数据,则添加注释 local last_part = ngx.arg[1] ngx.arg[1] = last_part .. "\n<!-- This is a dynamic footer added by Lua -->" end } }
# curl http://localhost/body
Hello, this is the original response body. <!-- This is a dynamic footer added by Lua -->
1.4.3 work进程退出阶段介绍
#当我们进程退出的时候,我们想记录一些信息到日志中,这就需要用到exit_worker_by_lua_block
# vim /usr/local/nginx/conf/nginx.conf
#nginx进程多一点才能测出效果 worker_processes 4; http { ...... #在每个工作进程启动时初始化一个全局计数器worker_counter init_worker_by_lua_block { -- ngx.shared是nginx提供的一个共享内存区域,允许多个请求和工作进程之间共享数据。 -- worker_counter是我们在共享内存中定义的一个键。如果该键不存在,则初始化为一个空表 {}。 ngx.shared.worker_counter = ngx.shared.worker_counter or {} -- 在共享内存中,我们定义了一个count 键,用于记录当前工作进程处理的请求数 -- 如果count键不存在(即第一次启动),则初始化为 0;否则,将其值加1 ngx.shared.worker_counter.count = (ngx.shared.worker_counter.count or 0) + 1 } #在每个工作进程退出时执行清理操作。在这个例子中,它记录了该工作进程处理的请求数,并清除了计数器。 exit_worker_by_lua_block { -- 从共享内存中读取当前工作进程处理的请求数,并赋值给变量count local count = ngx.shared.worker_counter.count -- 检查count是否存在。如果存在,则继续执行后续逻辑 if count then -- 使用 ngx.log函数记录一条日志消息。日志级别为 INFO,表示这是一条普通信息日志。日志内容包括工作进程退出的信息以及它处理的总请求数 ngx.log(ngx.INFO, "Worker process exiting. Total requests handled: ", count) -- 将共享内存中的count键设置为 nil,以清理数据 ngx.shared.worker_counter.count = nil -- 清理计数器 end } } server { ...... #每次请求/count路径时,增加请求计数并返回当前计数值。 location /count { content_by_lua_block { --从共享内存中读取当前工作进程处理的请求数,并赋值给变量count local count = ngx.shared.worker_counter.count -- 将共享内存中的count键的值加 1,表示处理了一个新的请求 ngx.shared.worker_counter.count = count + 1 ngx.say("Worker process pid: ", ngx.worker.pid()..",".."Hello, worker counter: ", count) } } }
# /usr/local/nginx/sbin/nginx -s reload
# curl 127.0.0.1/count #以此类推,多curl几回
Worker process pid: 3373320,Hello, worker counter: 1
# curl 127.0.0.1/count
Worker process pid: 3373321,Hello, worker counter: 1
# /usr/local/nginx/sbin/nginx -s reload
# tail -100f /usr/local/nginx/logs/error.log #可以看下打印的日志
[info] 3373321#0: [lua] exit_worker_by_lua(nginx.conf:46):5: Worker process exiting. Total requests handled: 2 [info] 3373322#0: [lua] exit_worker_by_lua(nginx.conf:46):5: Worker process exiting. Total requests handled: 2 [info] 3373323#0: [lua] exit_worker_by_lua(nginx.conf:46):5: Worker process exiting. Total requests handled: 2 [info] 3373320#0: [lua] exit_worker_by_lua(nginx.conf:46):5: Worker process exiting. Total requests handled: 3
二、lua常用模块使用
#nginx使用到的模块如何使用:https://github.com/openresty/lua-nginx-module#description
#从过上面的章节我们对lua在nginx各个阶段的使用有了一个初步的了解,但是如果要实现我们的功能还要结合mysql、redis等一些功能来使用
2.1 lua-resty-mysql的学习使用
2.1.1 使用方法学习
#https://github.com/openresty/lua-resty-mysql?tab=readme-ov-file#methods #先根据文档把使用方法学习一下
new:
语法:db, err = mysql:new()
#创建MySQL连接对象。如果失败,则返回nil和一个描述错误的字符串。
connect:
语法: ok, err, errcode, sqlstate = db:connect(options)
#options参数是一个Lua表,其中包含以下键,详细的参照官网文档把
set_timeout:
#为后续操作(包括连接方法)设置超时(以毫秒为单位)保护。
set_keepalive:
语法: ok, err = db:set_keepalive(max_idle_timeout, pool_size)
#立即将当前MySQL连接放入ngx_lua cosocket连接池。
get_reused_times:
#此方法返回当前连接的(成功)重用次数。如果发生错误,它将返回nil和一个描述错误的字符串。
#如果当前连接不是来自内置连接池,则此方法始终返回0,即连接从未被重用(尚未)。如果连接来自连接池,则返回值始终为非零。因此,此方法也可用于确定当前连接是否来自池。
#关闭当前mysql连接并返回状态。如果成功,返回1。如果发生错误,则返回nil,并返回一个描述错误的字符串。
send_query:
语法:bytes, err = db:send_query(query)
#将查询发送到远程MySQL服务器,而不等待其回复。返回成功发送的字节,否则返回nil和描述错误的字符串。之后,使用read_result方法读取MySQL的回复。
read_result:
语法: res, err, errcode, sqlstate = db:read_result()
语法: res, err, errcode, sqlstate = db:read_result(nrows)
#读取MySQL服务器返回的一个结果。它返回一个包含所有行的数组。每行包含每个数据字段的键值对。例如:
{ { name = "Bob", age = 32, phone = ngx.null }, { name = "Marry", age = 18, phone = "10666372"} }
#如果发生错误,此方法最多返回4个值:nil、err、errcode和sqlstate。err返回值包含一个描述错误的字符串,errcode返回值包含MySQL错误代码(一个数值),最后,sqlstate返回值包含由5个字符组成的标准SQL错误代码。请注意,如果MySQL不返回errcode和sqlstate,它们可能为nil。
query:
语法: res, err, errcode, sqlstate = db:query(query)
#这是组合send_query调用和第一个read_result调用的快捷方式。如果成功,应该始终检查err返回值是否再次出现,因为此方法只会调用read_result一次。
server_ver:
#返回MySQL服务器版本字符串,如“5.1.64”。
set_compact_arrays:
语法:db:set_compact_arrays(boolean)
#设置是否对后续查询返回的结果集使用“紧凑数组”结构。
2.1.2 环境支持
#如果不想使用 OpenResty,但仍然可以单独安装 lua-resty-mysql 模块。可以通过 LuaRocks 或手动编译安装。
LuaRocks安装方式:
#yum install libtermcap-devel ncurses-devel libevent-devel readline-devel -y
#wget https://luarocks.org/releases/luarocks-3.11.1.tar.gz
#tar xf luarocks-3.11.1.tar.gz
#cd luarocks-3.11.1/
#./configure --with-lua="/usr/local/luajit/" --lua-suffix="jit" --with-lua-include=/usr/local/luajit/include/luajit-2.1/
#make && make install
# luarocks --version
/usr/local/bin/luarocks 3.11.1 LuaRocks main command-line interface
#luarocks install lua-resty-mysql #因为拉取的是https://github.com/openresty/lua-resty-mysql/失败了就多执行几次
# find /usr/local/|grep mysql|grep resty #可以查看一下是否安装到了指定位置
/usr/local/lib/luarocks/rocks-5.1/lua-resty-mysql /usr/local/lib/luarocks/rocks-5.1/lua-resty-mysql/0.15-0 /usr/local/lib/luarocks/rocks-5.1/lua-resty-mysql/0.15-0/lua-resty-mysql-0.15-0.rockspec /usr/local/lib/luarocks/rocks-5.1/lua-resty-mysql/0.15-0/doc /usr/local/lib/luarocks/rocks-5.1/lua-resty-mysql/0.15-0/doc/README.markdown /usr/local/lib/luarocks/rocks-5.1/lua-resty-mysql/0.15-0/rock_manifest /usr/local/share/lua/5.1/resty/mysql.lua
#然后写例子测试一下:
# vim /usr/local/nginx/conf/lua_conf/lua_mysql_script.lua
-- 通过Lua的require函数加载了resty.mysql模块,并将其赋值给局部变量mysql。这个模块提供了与 MySQL数据库交互的功能 local mysql = require "resty.mysql" -- 创建一个新的MySQL连接对象。如果创建失败,err将包含错误信息;否则,db 将是新创建的数据库连接对象。 local db, err = mysql:new() -- 如果未能成功创建数据库连接对象(即db为nil),则输出错误信息并通过 return 结束脚本执行。 if not db then ngx.say("failed to instantiate mysql: ", err) return end -- 设置MySQL操作的超时时间,单位为毫秒。这里设置的是1秒。如果操作在1秒内未完成,将抛出超时错误 db:set_timeout(1000) -- 使用db:connect方法尝试连接到MySQL数据库服务器 local ok, err, errno, sqlstate = db:connect{ host = "127.0.0.1", port = 3306, database = "testdb", user = "root", password = "aabbccdd123", -- max_packet_size: 允许的最大数据包大小,这里设置为 1MB max_packet_size = 1024 * 1024 } -- 如果连接失败(即ok为false),则输出详细的错误信息并结束脚本执行 if not ok then ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate) return end -- 执行一个SQL查询语句,从test表中选择所有列的第一行记录。如果查询失败,err, errno,和sqlstate将包含错误信息 local res, err, errno, sqlstate = db:query("SELECT * FROM users LIMIT 1") if not res then ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate) return end -- 遍历查询结果res中的每一行,并打印出每行的id和name字段。这里的ipairs是用于遍历数组形式的表 for i, row in ipairs(res) do ngx.say("Row: ", row.id, " - ", row.name) end -- 将当前的数据库连接放入连接池中,以便后续使用。set_keepalive方法的第一个参数是最大空闲时间(毫秒),第二个参数是连接池中的最大连接数 local ok, err = db:set_keepalive(10000, 100) -- 如果无法将连接放入连接池,则输出错误信息并结束脚本执行。 if not ok then ngx.say("failed to set keepalive: ", err) return end
# vim /usr/local/nginx/conf/nginx.conf
location /test { content_by_lua_file /usr/local/nginx/conf/lua_conf/lua_mysql_script.lua; }
#/usr/local/nginx/sbin/nginx -s reload
# mysql -uroot -p -e "select * from testdb.users;" #查看一下数据库现在的测试信息
# curl 127.0.0.1/test
Row: 1 - dage
原生源码包的方式:
#wget https://github.com/openresty/lua-resty-mysql/archive/refs/tags/v0.27.tar.gz
#tar xf v0.27.tar.gz
#cd lua-resty-mysql-0.27/
# /usr/local/luajit/bin/luajit -e "print(package.path)" #查看lua加载的路径
#mkdir /usr/local/luajit/share/lua/5.1/resty/ #选取上面查出来的任意路径
#cp lib/resty/mysql.lua /usr/local/luajit/share/lua/5.1/resty/
#wget https://github.com/openresty/lua-resty-string/archive/refs/tags/v0.16.tar.gz #lua-resty-string 模块,它包含了 resty.sha256,如果不安装这个的话会有下面报错,需要加密的模块:
usr/local/luajit/bin/luajit: /usr/local/luajit/share/luajit-2.1/resty/mysql.lua:5: module 'resty.sha256' not found: no field package.preload['resty.sha256'] ......
#tar xf v0.16.tar.gz
#cd lua-resty-string-0.16/
#cp lib/resty/*.lua /usr/local/luajit/share/lua/5.1/resty/ #至此就可以用上面的lua例子测试一下mysql的连接了
#当然你还有另一种加载模块的方式,你说我不想cp太麻烦我想加载多目录就可以在nginx上面这样配置:
-- 路径之间用分号 (;) 分隔。双分号 (;;) 表示保留Lua默认的搜索路径。 lua_package_path "/opt/soft/package/nginx_lua/lua-resty-mysql-0.27/lib/?.lua;/opt/soft/package/nginx_lua/lua-resty-string-0.16/lib/?.lua;/usr/local/lua_core/lib/lua/?.lua;;"; -- 当然使用绝对路径太长了,可以把这个lua文件都放到nginx指定的目录下面,然后使用./modules/?.lua;这种相对路径的方式
2.2 lua-resty-redis的学习使用
#一般mysql和redis都结合者使用,所以我们照着上面的方法也学习一下redis的使用
2.2.1 使用方法学习
#除了小写之外,所有Redis命令都有自己的同名方法。你可以在这里找到Redis命令的完整列表:http://redis.io/commands
new:
语法: red, err = redis:new()
#创建redis对象。如果失败,则返回nil和一个描述错误的字符串。
语法: ok, err = red:connect("unix:/path/to/unix.sock", options_table?)
#尝试连接到redis服务器正在侦听的远程主机和端口,或redis服务器侦听的本地unix域套接字文件。在实际解析主机名并连接到远程后端之前,此方法将始终在连接池中查找由此方法的先前调用创建的匹配空闲连接。可选的options_table参数是一个Lua表,包含以下键:ssl、ssl_verify、server_name、pool、pool_size、backlog
set_timeout:
语法: red:set_timeout(time)
#为后续操作(包括连接方法)设置超时(以毫秒为单位)保护。从该模块的v0.28版本开始,建议使用set_timeouts来支持此方法。
#分别设置后续套接字操作的连接、发送和读取超时阈值(以毫秒为单位)。使用此方法设置超时阈值比set_timeout提供了更多的粒度。因此,最好使用set_timeout而不是set_timeout。v0.28版本中添加了此方法。
set_keepalive:
语法: ok, err = red:set_keepalive(max_idle_timeout, pool_size)
#立即将当前Redis连接放入ngx_lua cosocket连接池。可以指定连接在池中时的最大空闲超时(以毫秒为单位)以及每个nginx工作进程的池的最大大小。
#如果成功,返回1。如果发生错误,则返回nil,并返回一个描述错误的字符串。
get_reused_times:
语法: times, err = red:get_reused_times()
#此方法返回当前连接的(成功)重用次数。如果发生错误,它将返回nil和一个描述错误的字符串。
#如果当前连接不是来自内置连接池,则此方法始终返回0,即连接从未被重用(尚未)。如果连接来自连接池,则返回值始终为非零。因此,此方法也可用于确定当前连接是否来自池。
语法: ok, err = red:close()
#关闭当前的redis连接并返回状态。如果成功,返回1。如果发生错误,则返回nil,并返回一个描述错误的字符串。
语法: red:init_pipeline(n)
#启用redis流水线模式。所有后续对Redis命令方法的调用都将自动缓存,并在调用commit_pipeline方法时一次性发送到服务器,或者通过调用cancel_pipeline来取消。
#通过在一次运行中将所有缓存的Redis查询提交到远程服务器来退出流水线模式。这些查询的所有回复都将被自动收集,并作为最高级别的大型多批量回复返回。
#通过丢弃自上次调用init_pipeline方法以来所有现有的缓存Redis命令来退出流水线模式。
hmset:
语法: res, err = red:hmset(myhash, { field1 = value1, field2 = value2, ... })
#Redis“hmset”命令的特殊包装器。当只有三个参数(包括“red”对象本身)时,最后一个参数必须是一个包含所有字段/值对的Lua表。
语法: hash = red:array_to_hash(array)
#将类似数组的Lua表转换为类似哈希表的辅助函数。此方法首次在v0.11版本中引入。
语法: res, err = red:read_reply()
#正在从redis服务器读取回复。该方法例如对于Redis Pub/Sub API最有用