[nginx]openresty和lua - 迁
项目案例
nginx_lua_waf
http {
# lua_waf
lua_shared_dict limit 50m;
lua_shared_dict blackip 50m;
lua_package_path "/usr/local/openresty/nginx/conf/waf/?.lua;/usr/local/openresty/lualib/?.lua;";
init_by_lua_file /usr/local/openresty/nginx/conf/waf/init.lua;
access_by_lua_file /usr/local/openresty/nginx/conf/waf/access.lua;
include /usr/local/openresty/nginx/conf/vhost/*.conf;
include /usr/local/openresty/nginx/conf/white_ip.conf;
}
代码托管 github
waf/init.lua 注意 服务器那边必须是list类型的
-- 查询redis库
function get_redis()
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("10.0.4.111", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local res, err = red:lrange("whiteip",0,-1)
if res == nil then
return
end
RULE_TABLE = res
return RULE_TABLE
end
--Get whiteIP by iputils
function get_white_ip(whiteip)
local iputils = require("iputils")
-- ngx.say(whiteip)
local IP_WHITE_RULE = whiteip
-- local IP_WHITE_RULE = get_rule('whiteip.rule')
whitelist = iputils.parse_cidrs(IP_WHITE_RULE)
if whitelist ~= nil then
if iputils.ip_in_cidrs(ngx.var.remote_addr, whitelist) then
log_record('White_IP',ngx.var_request_uri,"_","_")
return true
end
end
end
lua语法
json
local cjson = require "cjson"
local sampleJson = [[{"age":"23","testArray":{"array":[8,9,11,14,25]},"Himi":"himigame.com"}]];
--解析json字符串
local data = cjson.decode(sampleJson);
--打印json字符串中的age字段
print(data["age"]);
--打印数组中的第一个值(lua默认是从0开始计数)
print(data["testArray"]["array"][1]);
# curl http://127.0.0.1:8080/123/
hello, ttlsa lua
正则匹配
content_by_lua '
local res = ngx.location.capture("/pass_get",
local ids = string.gmatch( h,"show_surveil_detail%S\'(%d+)\',\'(%d+)\',\'0\'%S" );
‘
应用
非penresty方式 安装 lua-nginx-redis
原理
1、每个worker(工作进程)创建一个Lua VM,worker内所有协程共享VM;
2、将Nginx I/O原语封装后注入 Lua VM,允许Lua代码直接访问;
3、每个外部请求都由一个Lua协程处理,协程之间数据隔离;
4、Lua代码调用I/O操作等异步接口时,会挂起当前协程(并保护上下文数据),而不阻塞worker;
5、I/O等异步操作完成时还原相关协程上下文数据,并继续运行;
一些注意点:
1:content_by_lua中代码容量有限制,一般不要写太多代码,正常编写代码一般在100行左右(具体容量没有细心测哈哈,在4kb左右),如果超出了则重启nginx的时候会报too long parameters
2:如果引入lua脚本文件也得控制一下lua脚本中函数里面代码的容量,不要太多
3:编写lua代码时一定得健壮,不然nginx虽然可以重启但是经常会导致500错误,比如参数的判断,使用一些未定义的变量(当然lua中是可以的,但是现在是在nginx环境中,情况有些不一样)
4:nginx_lua中不支持使用"..."的不限制参数模式的函数参数
5:content_by_lua中的代码一定要注意单引号或者双引号,引号和content_by_lua之间要有空格
6:在content_by_lua中如果使用正则(string.match,string.gmatch)的时候如果content_by_lua后面用如果单引号引起来lua代码的话,正则里面单引号要用 "\"进行转移而不是"%"转义符以上描述可能绕口,直接贴代码 "
7:在nginx_lua中nil的变量跟数字相加是不允许的,nginx会报500错误的.
8:经常在写lua脚本的时候有时nginx的reload不起作用,导致新写的nginx配置不生效,可以在reload之前nginx -t检查一下看那里报错
9:在使用共享内存api的时候一定要注意如:使用lua_shared_dict、ngx.shared.DICT的时候最好不要使用get_keys,否则指不定那次获取比较多的数据的时候共享内存被锁定,严重时可能导致nginx阻塞
10:在ngx_lua中数字类型跟字符类型的数字进行运算时会报错的,必须将两者都统一成数字类型的,如
local a=123;local b="333";
a+b(错误)
a+(b+0)这样就可以了
字符类型的数字加上0可以转换成数字类型的
如何导入lua模块
//在http头直接导入lua模块,注意先后顺序,防止覆盖openresty自带模块的路径
lua_shared_dict limit 50m;
lua_package_path "/usr/local/nginx/conf/waf/?.lua";
init_by_lua_file /usr/local/nginx/conf/waf/init.lua;
access_by_lua_file /usr/local/nginx/conf/waf/access.lua;
// 加了之后就用重启nginx也能改lua脚本,但是性能肯定变差
lua_code_cache off;
// 使用content_by_lua 直接写lua代码
server {
location /hello {
default_type text/html;
content_by_lua_block { ngx.say("HelloWorld") }
}
}
location ~* ^/123(.*) {
default_type 'text/plain';
content_by_lua 'ngx.say("hello, ttlsa lua")';
}
使用healthcheck做健康检查
https://github.com/openresty/lua-resty-upstream-healthcheck
配置举例
http {
lua_package_path "/usr/local/openresty/lualib/resty/?.lua;/usr/local/openresty/lualib/resty/upstream/?.lua;;";
lua_shared_dict healthcheck 1m;
lua_socket_log_errors off;
init_worker_by_lua_block {
local hc = require "resty.upstream.healthcheck"
local ok, err = hc.spawn_checker {
-- shm 表示共享内存区的名称
shm = "healthcheck",
-- upstream 指定 upstream 配置的名称
upstream = "upstream",
-- type 表示健康检查的类型, HTTP or TCP (目前只支持http)
type = "http",
-- 如果是http类型,指定健康检查发送的请求的URL
http_req = "GET /index.html HTTP/1.0\r\nHost: haichi.net\r\n\r\n",
-- 请求间隔时间,默认是 1 秒。最小值为 2毫秒
interval = 2000,
-- 请求的超时时间。 默认值为:1000 毫秒
timeout = 5000,
-- 失败多少次后,将节点标记为down。 默认值为 5
fall = 3,
-- 成功多少次后,将节点标记为up。默认值为 2
rise = 2,
-- 返回的http状态码,表示应用正常
valid_statuses = {200, 302,404},
-- 并发度, 默认值为 1
concurrency = 1,
}
if not ok then
ngx.log(ngx.ERR, "=======> failed to spawn health checker: ", err)
return
end
}
}
server {
......
# status page for all the peers:
location = /status {
access_log off;
allow 127.0.0.1;
deny all;
default_type text/plain;
content_by_lua_block {
local hc = require "resty.upstream.healthcheck"
ngx.say("Nginx Worker PID: ", ngx.worker.pid())
ngx.print(hc.status_page())
}
}
}
}
涉及到参数
hc.spawn_checker(options)
options中包含如下选项,在调用该接口时作为参数传递进来
type 必须存在并且是http,目前只支持http
http_req 必须存在,健康探测的http请求raw字符串
timeout 默认1000,单位ms
interval 健康探测的时间间隔,单位ms, 默认1000,推荐2000
valid_status 合法响应码的表,比如{200, 302}
concurrency 并发数,默认1
fall 默认5,对UP的设备,连续fall次失败,认定为DOWN
rise 默认2,对DOWN的设备,连续rise次成功,认定为UP
shm 必须配置,用于健康检查的共享内存名称,通过ngx.shared[shm]得到共享内存
upstream 指定要做健康检查的upstream组名,必须存在
version 默认0
primary_peers 主组
backup_peers 备组
statuses 存放合法响应码的数组,来自ipairs()得到的valid_status配置项
根据options会构造一个ctx表来存放所有的配置数据,并会作为定时器ngx.timer.at()中的第三个参数
ctx的内容如下
upstream 指定的upstream组名
primary_peers 主组
backup_peers 备组
http_req 健康检查的raw http请求
timeout 超时时间,单位s,注意不是ms
interval 健康检查的间隔,单位s,注意不是ms
dict 存放统计数据的共享内存
fall 认为DOWN之前的连续失败次数,默认5
rise 认为UP之前的连续成功次数,默认2
statuses 认为正常的http状态码的表{200,302}
version 0 每次执行定时任务时的版本号,有peer状态改变,版本号加1
concurrency 创建该数目的轻量线程来并发发送健康检测请求的个数
request和response包体内容
只有location中用到proxy_pass,fastcgi_pass,scgi_pass命令时,$request_body变量才有值
http {
log_format pdata escape=json '{"remote_addr":"$remote_addr","request_body":"$request_body","response_body":"$resp_body","request":"$request","time_local":"$time_iso8601"}';
server {
location {
lua_need_request_body on;
body_filter_by_lua '
local resp_body = string.sub(ngx.arg[1], 1, 1000)
ngx.ctx.buffered = (ngx.ctx.buffered or "") .. resp_body
if ngx.arg[2] then
ngx.var.resp_body = ngx.ctx.buffered
end
';
access_log /usr/local/nginx/logs/resp_body.log pdata;
}
}
实现定时器
在 nignx 启动时一个携程去定时执行
作用域 init_worker_by_lua
local delay = 2 -- in seconds
local new_timer = ngx.timer.at
local log = ngx.log
local ERR = ngx.ERR
local check
check = function(premature)
if not premature then
-- do the health check or other routine work
local res = ngx.location.capture("/12350")
log(ERR, res.body) -- 日志中会打印
local ok, err = new_timer(delay, check)
if not ok then
log(ERR, "failed to create timer: ", err)
return
end
end
end
if 0 == ngx.worker.id() then
local ok, err = new_timer(delay, check)
if not ok then
log(ERR, "failed to create timer: ", err)
return
end
end
lua-resty-limit-traffic模块
https://github.com/openresty/lua-resty-limit-traffic
server {
listen 4444;
location / {
access_by_lua_block {
-- 限制客户端并发(用多线程测试)
local limit_req = require "resty.limit.req"
-- 限制请求速率为20 req/sec,并且允许10 req/sec的突发请求
-- 就是说我们会把20以上30一下的请求请求给延迟
-- 超过30的请求将会被拒绝[503]
local lim, err = limit_req.new("my_limit_req_store", 2, 1)
local white_ip = {'192.168.3.15'}
local remote_addr = ngx.var.remote_addr
for _,ip in pairs(white_ip) do
if rule ~= "" and rulematch(remote_addr,ip,"joi") then
-- ngx.log(ngx.ERR,remote_addr.."whiteip")
return
end
end
if not lim then --申请limit_req对象失败
ngx.log(ngx.ERR,
"failed to instantiate a resty.limit.req object: ", err)
return ngx.exit(500)
end
-- 下面代码针对每一个单独的请求
-- 使用ip地址+uri作为限流的key
local key = ngx.var.binary_remote_addr..ngx.var.request_uri
-- ngx.log(ngx.ERR,ngx.var.request_uri)
local delay, err = lim:incoming(key, true)
if not delay then
if err == "rejected" then
return ngx.exit(503)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
if delay >= 0.001 then
-- 第二个参数(err)保存着超过请求速率的请求数
-- 例如err等于5,意味着当前速率是25 req/sec
local excess = err
-- 当前请求超过20 req/sec 但小于 30 req/sec
-- 因此我们sleep一下,保证速率是20 req/sec,请求延迟处理
ngx.sleep(delay)
end
}
echo "niaho";
}
}
语法: obj, err = class.new(shdict_name, rate, burst) 成功的话会返回resty.limit.req对象,失败的话返回nil和一个描述错误原因的字符串值 incoming 语法: delay, err = obj:incoming(key, commit) key这里是指需要限流的ip;commit真心没看懂(囧),先按照例子传true 返回值根据情况的不同返回不同的值 1.如果请求没超过速率,那么delay和err返回0 2.如果请求超过速率但没超过“速率+burst”的值,那么delay将会返回一个合适的秒数,告诉你多久后这个请求才会被处理;第二个参数(err)保存着超过请求速率的请求数量 3.如果请求超过“速率+burst”的值,那么delay会返回nil,err会返回”rejected”字符串 4.如果一个error发生了,delay会返回nil,err会返回具体错误的字符串描述 inconing方法不会sleep自己,需要调用者调用’ngx.sleep’去延迟请求处理。
配置共享字典
http {
......
lua_shared_dict limit_req_store 10m;
......
}
测试:
# -*- coding: utf-8 -*-
# @Author: richard
# @Date: 2018-11-14 18:21:37
# @Last Modified by: richard
# @Last Modified time: 2018-11-14 19:38:49
import multiprocessing
import requests,os,time,random
p = multiprocessing.Pool(100)
q = multiprocessing.Queue()
url="http://10.0.0.219:4444"
def get_data(url,i,q):
r = requests.get(url)
q.put("try count:[%s];result [%s]" % (i,r.status_code))
for i in range(100):
p = multiprocessing.Process(target=get_data,args=(url,i,q))
p.start()
while not q.empty():
print q.get()
输出:
try count:[1];result [200] try count:[0];result [200] try count:[12];result [503] try count:[14];result [503] try count:[23];result [503] try count:[13];result [503] try count:[21];result [503] try count:[18];result [503] try count:[17];result [503] try count:[22];result [503] .......
联合限制: 根据多个条件进行访问限制
server {
listen 7777;
location / {
access_by_lua_block {
local limit_conn = require "resty.limit.conn"
local limit_req = require "resty.limit.req"
local limit_traffic = require "resty.limit.traffic"
local lim1, err = limit_req.new("my_req_store", 3, 2)
assert(lim1, err)
local lim2, err = limit_req.new("my_req_store", 200, 100)
assert(lim2, err)
local lim3, err = limit_conn.new("my_conn_store", 1000, 1000, 0.5)
assert(lim3, err)
local limiters = {lim1, lim2, lim3}
local host = ngx.var.host
local client = ngx.var.binary_remote_addr
local keys = {host, client, client} -- 服务器限制,客户端请求限制,客户端连接限制
local states = {}
local delay, err = limit_traffic.combine(limiters, keys, states) -- 满足3个条件就触发
if not delay then
if err == "rejected" then
return ngx.exit(503)
end
ngx.log(ngx.ERR, "failed to limit traffic: ", err)
return ngx.exit(500)
end
if lim3:is_committed() then
local ctx = ngx.ctx
ctx.limit_conn = lim3
ctx.limit_conn_key = keys[3]
end
print("sleeping ", delay, " sec, states: ",
table.concat(states, ", "))
if delay >= 0.001 then
ngx.sleep(delay)
end
}
# content handler goes here. if it is content_by_lua, then you can
# merge the Lua code above in access_by_lua into your
# content_by_lua's Lua handler to save a little bit of CPU time.
log_by_lua_block {
local ctx = ngx.ctx
local lim = ctx.limit_conn
if lim then
-- if you are using an upstream module in the content phase,
-- then you probably want to use $upstream_response_time
-- instead of $request_time below.
local latency = tonumber(ngx.var.request_time)
local key = ctx.limit_conn_key
assert(key)
local conn, err = lim:leaving(key, latency)
if not conn then
ngx.log(ngx.ERR,
"failed to record the connection leaving ",
"request: ", err)
return
end
end
}
echo "hello world";
}
}
根据uri分发
可以做灰度发布
location / {
content_by_lua '
myIP = ngx.req.get_headers()["X-Real-IP"]
if myIP == nil then
myIP = ngx.var.remote_addr
end
if myIP == "172.23.4.218" then
ngx.exec("@refuse")
else
if ngx.var.uri == "/gjdx" then
ngx.exec("@gjdx")
elseif ngx.var.uri == "/gjent" then
ngx.exec("@gjent")
else
error("invalid operation")
end
end
';
}
location @refuse {
rewrite ^/(.*) http://ecp.189.cn/page/app/gonggao.html redirect;
}
location @gjdx {
proxy_pass http://gd_yixin_im; // 这边必须不能跟后面的目录 http://gd_yixin_im/gjdx,不然要报错!
root html;
index index.html index.htm;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location @gjent {
proxy_pass http://localhost:9080;
root html;
index index.html index.htm;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
结合redis2模块数据插入及查询
local cache = redis.new()
local ok, err = cache.connect(cache, '172.23.4.50', '6380')
cache:set_timeout(60000)
if not ok then
ngx.say("failed to connect:", err)
return
end
res, err = cache:set("dog", "an aniaml")
if not ok then
ngx.say("failed to set dog: ", err)
return
end
ngx.say("set result: ", res)
local res, err = cache:get("dog")
if not res then
ngx.say("failed to get dog: ", err)
return
end
if res == ngx.null then
ngx.say("dog not found.")
return
end
ngx.say("dog: ", res)
local ok, err = cache:close()
if not ok then
ngx.say("failed to close:", err)
return
end
ngx的变量和用法
http://yum.ops.net/ty?id=ui&id=hhj
// 请求的url和参数 /ty/?id=ui&id=hhj
ngx.var.request_uri
// 请求的url不带参数 /ty/
ngx.var.uri
// 获取请求的参数 值为1
ngx.var.arg_id
//获取请求的参数 值为 ['ui','hhj']
ngx.var.get_uri_args['id']
// 获取包体表单参数并以table的方式输出 {"data":"select * from daul"}
ngx.req.get_post_args()
// 获取包体数据,如果不是json或者字典,再做解析 data=select * from daul
ngx.req.get_body_data()
ngx_lua模块提供的指令和API
Nginx共11个处理阶段,而相应的处理阶段是可以做插入式处理,即可插拔式架构;另外指令可以在http、server、server if、location、location if几个范围进行配置:
|
指令 |
所处处理阶段 |
使用范围 |
解释 |
|
init_by_lua init_by_lua_file |
loading-config |
http |
nginx Master进程加载配置时执行; 通常用于初始化全局配置/预加载Lua模块 |
|
init_worker_by_lua init_worker_by_lua_file |
starting-worker |
http |
每个Nginx Worker进程启动时调用的计时器,如果Master进程不允许则只会在init_by_lua之后调用; 通常用于定时拉取配置/数据,或者后端服务的健康检查 |
|
set_by_lua set_by_lua_file |
rewrite |
server,server if,location,location if |
设置nginx变量,可以实现复杂的赋值逻辑;此处是阻塞的,Lua代码要做到非常快; |
|
rewrite_by_lua rewrite_by_lua_file |
rewrite tail |
http,server,location,location if |
rrewrite阶段处理,可以实现复杂的转发/重定向逻辑; |
|
access_by_lua access_by_lua_file |
access tail |
http,server,location,location if |
请求访问阶段处理,用于访问控制 |
|
content_by_lua content_by_lua_file |
content |
location,location if |
内容处理器,接收请求处理并输出响应 |
|
header_filter_by_lua header_filter_by_lua_file |
output-header-filter |
http,server,location,location if |
设置header和cookie |
|
body_filter_by_lua body_filter_by_lua_file |
output-body-filter |
http,server,location,location if |
对响应数据进行过滤,比如截断、替换。 |
|
log_by_lua log_by_lua_file |
log |
http,server,location,location if |
log阶段处理,比如记录访问量/统计平均响应时间 |

浙公网安备 33010602011771号