[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阶段处理,比如记录访问量/统计平均响应时间 |