[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" );
‘

  

 

应用

lua在线测试

教程

非penresty方式 安装 lua-nginx-redis

使用教程

ngx_lua内部变量

原理

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

posted @ 2019-12-26 11:06  richardzgt  阅读(647)  评论(0编辑  收藏  举报