Nginx/OpenResty+Lua实战

OpenResty集成了Nginx,在支持Nginx所有的功能前提下,整合了Lua、Mysql、 Redis、Memcached等插件,使Nginx功能更强大。做7层负载均衡,做web开发,缓存,流控、waf、网关。

推荐张开涛的《亿级流量网站架构核心技术》。里边有很多解决高并发问题的思路和方案,其中包含OpenResty的使用。

LUA第三方库存放在openresty\lualib\resty目录下,即可在lua中调用

最佳实战文档

百度->OpenResty最佳实践

https://moonbingbing.gitbooks.io/openresty-best-practices/content/

https://www.showapi.com/book/view/2123/65

测试LUA错误排查

查看错误日志 openresty-1.15.8.2-win64\logs\error.log,上边会有lua的异常。包含时间、lua文件、报错行数、错误信息。

2020/03/10 11:00:29 [error] 5496#8124: *1 lua entry thread aborted: runtime error: ./lua/api.lua:1: attempt to index global 'headers' (a nil value)
stack traceback:
coroutine 0:
	./lua/api.lua: in main chunk, client: 127.0.0.1, server: localhost, request: "POST /luacache?key=123 HTTP/1.1", subrequest: "/api", host: "localhost"

功能分P视频

https://space.bilibili.com/431715942/video?tid=0&page=1&keyword=&order=pubdate

Lua语法

https://www.runoob.com/lua/lua-tutorial.html

ngx_lua模块变量

https://blog.csdn.net/imilli/article/details/83621325

变量作用域

案例:https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/lua-variable-scope.html

局部变量:https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/local.html

local局部变量线程不共享,非局部变量线程共享

g_var = 1         -- global var
local l_var = 2   -- local var

控制Nginx跳转location 

可以通过LUA接收请求后控制请求跳转。这样LUA代码就可以包裹整个请求的响应前->响应后过程。可以实现:限流、灰度发布、缓存命中与存储等等。

ngx.exec("@url");
--跳转到nginx中配置的 location @url {}
--子请求,body 必须是字符串形式
--"/api"转发会nginx,寻找匹配location
local resp = ngx.location.capture("/api", {method=ngx.HTTP_POST,
                                                body = "hello",
                                                args = {a = "aa", b = "bb"}}) 
ngx.say(resp.body)

cjson 解析/生成json

生成、解析复杂Json字符串:{"age":"23","testArray":{"array":[8,9,11,14,25]},"Himi":"himigame.com"}

local cjson = require "cjson"

--生成json字符串
local _jsonArray={}
_jsonArray[1]=8
_jsonArray[2]=9
_jsonArray[3]=11
_jsonArray[4]=14
_jsonArray[5]=25
 
local _arrayFlagKey={}
_arrayFlagKey["array"]=_jsonArray
 
local tab = {}
tab["Himi"]="himigame.com"
tab["testArray"]=_arrayFlagKey
tab["age"]="23"
 
--tab转json字符串
local jsonData = cjson.encode(tab)
 
print(jsonData)
-- 打印结果: {"age":"23","testArray":{"array":[8,9,11,14,25]},"Himi":"himigame.com"}

--解析json
local data = cjson.decode(jsonData)
local a = data.age
local b = data.testArray.array[2]
local c = data.Himi

控制Nginx响应

--返回响应内容体;(内容体结束后没有换行符;)
ngx.print("aaaaaaaaaaaaaa")

--返回响应内容体;(内容体结束后,输出一个换行符;)
ngx.say("aaaaaaaaaaaaaaa")

Lua获取请求uri

https://blog.csdn.net/xiejunna/article/details/71647281

Lua获取请求参数

https://segmentfault.com/a/1190000007923803

Lua获取请求头

local ip=ngx.req.get_headers()["X-REAL-IP"]

Lua操作cookie

https://blog.csdn.net/qq362228416/article/details/54746353

Lua开发库-redis、mysql、http客户端

https://www.iteye.com/blog/jinnianshilongnian-2187328

redis长/短链接测试:长连接(用连接池)性能远远好过短连接

https://www.cnblogs.com/tinywan/p/6838630.ht

防止sql注入:

https://moonbingbing.gitbooks.io/openresty-best-practices/content/openresty/safe_sql.html

Lua实现本地缓存cache

OpenResty有两种缓存方式,分别是shared_dict(worker之间共享,所以必须保证操作的原子性)和lua-resty-lrucache(worker不共享性能更好)。使用jmeter压测500线程3轮,有/无lua缓存性能差别很明显。

https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/cache.html

shared_dict—api

shared_dict提供了丰富的原子性api。可以实现LRU、TTL、putIfAbsent、setnx、incr等类似功能。

http://www.hangdaowangluo.com/archives/2762

Lock

利用双重检查锁解决缓存击穿

https://moonbingbing.gitbooks.io/openresty-best-practices/content/lock/cache-miss-storm.html

https://blog.csdn.net/echineselearning/article/details/63792905

local resty_lock = require "resty.lock";  
local lock = resty_lock:new(“lockkey”);  
local elapsed, err = lock:lock(key);  --试图获取锁,如果锁以被占用则当前请求被阻塞 
if not elapsed then  --取锁失败
    if err == “timeout” then --锁超时,被自动释放,根据自己的业务情况选择后续动作   
        do something  
        return;  
     end   
       
      do something  
      return;  
end 

--取锁成功
...

lock:unlock();

Lua渲染HTML模板

使用说明:https://www.kutu66.com//GitHub/article_107620

案例视频:https://www.bilibili.com/video/av61296135?p=18

首先通过后端程序生成静态html页面,保存到OpenResty的html目录下。静态html页面中为经常变化的值预留lua渲染占位符{},例如:库存,价格等信息。当外部请求此页面时,通过lua请求后端mysql\redis,渲染占位符部分{}信息后返回。

适用场景:1 与用户权限无关的页面。比如:门户首页、商品展示页、公共页等等

请求返回后继续执行

及时返回客户端请求,不影响用户体验,返回后继续执行lua代码完成后续处理,比如缓存、断连、日志等....

注意此时虽然已经响应用户,但是连接并没有断开。

ngx.eof() 下面的代码是响应后继续执行的

https://moonbingbing.gitbooks.io/openresty-best-practices/content/ngx_lua/continue_after_eof.html

ngx-waf模块

功能 
支持IP白名单和黑名单功能,直接将黑名单的IP访问拒绝。 
支持URL白名单,将不需要过滤的URL进行定义。 
支持User-Agent的过滤,匹配自定义规则中的条目,然后进行处理(返回403)。 
支持CC流量攻击防护,单个URL指定时间的访问次数,超过设定值,直接返回403。 
支持Cookie过滤,匹配自定义规则中的条目,然后进行处理(返回403)。 
支持URL过滤,匹配自定义规则中的条目,如果用户请求的URL包含这些,返回403。 
支持URL参数过滤,原理同上。 
支持日志记录,将所有拒绝的操作,记录到日志中去

视频:video/av73199123

文档:

https://blog.csdn.net/m0_37886429/article/details/73178889

https://blog.csdn.net/chuanxincui/article/details/86089763

 

 

配置案例

\openresty-1.15.8.2-win64\conf\nginx.conf

    #user  nobody;
    worker_processes  1;

                                  #error_log  logs/error.log;
                                  #error_log  logs/error.log  notice;
                                  #error_log  logs/error.log  info;

                                  #pid        logs/nginx.pid;

    events {
            worker_connections  5000;  #设置单个worker_processes最大连接数
            use epoll; #linux使用epoll事件驱动,因为epoll的性能相比其他事件驱动要好很多
    }


     http {
                    include       mime.types;
                    default_type  text/html;

                    #添加;;标识默认路径下的lualib
                    lua_package_path "$prefix/lualib/?.lua;;";
                    lua_package_cpath "$prefix/lualib/?.so;;";

                            #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                            #                  '$status $body_bytes_sent "$http_referer" '
                            #                  '"$http_user_agent" "$http_x_forwarded_for"';

                            #access_log  logs/access.log  main;
                    #零拷贝
                    sendfile        on;
                    #tcp_nopush     on;

                    #keepalive_timeout  0; (秒) 1d=1天  1h=1小时 1m=1分钟
                    keepalive_timeout  30;

                    gzip  on;  #文件压缩 (优化带宽和响应速度)
                    gzip_buffers 32 4K;  #缓冲
                    gzip_comp_level 6;  #压缩级别(级别越高,压的越小,越浪费CPU计算资源)
                    gzip_min_length 10000;  #小于10000字节不压缩
                    gzip_types application/javascript text/css text/xml; #图片/mp3这样的二进制文件,不必压缩。因为压缩率比较小, 比如100->80字节,而且压缩也是耗费CPU资源的.
                    gzip_disable "MSIE [1-6]\."; #配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
                    gzip_vary on;  # 是否传输gzip压缩标志


                    proxy_buffer_size 128k;
                    #代理请求缓存区_这个缓存区间会保存用户的头信息以供Nginx进行规则处理_一般只要能保存下头信息即可
                    proxy_buffers 4 128k;
                    #同上 告诉Nginx保存单个用的几个Buffer最大用多大空间
                    proxy_busy_buffers_size 256k;
                    #如果系统很忙的时候可以申请更大的proxy_buffers 官方推荐*2
                    proxy_temp_file_write_size 128k;
                    #proxy缓存临时文件的大小
                    proxy_temp_path E:/nginx/temp;
                    #用于指定本地目录来缓冲较大的代理请求(绝对路径)
                    proxy_cache_path  E:/nginx/cache  levels=1:2  keys_zone=cache_one:200m   inactive=1d  max_size=20g;
                    #设置web缓存区名为cache_one,可以缓存任意格式响应数据。levels=E:/nginx/cache中文件目录级别,
                    #内存缓存空间大小为200M,自动清除超过1天没有被访问过的缓存数据,硬盘缓存空间大小20g

                    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=20r/s;
                    #         限流异常:503 Service Temporarily Unavailable
                     #        1)limit_req_zone定义在http块中,$binary_remote_addr表示保存客户端IP地址的二进制形式作为key。
                     #        2)Zone定义IP状态及URL访问频率的共享内存区域。zone=keyword标识区域的名字,以及冒号后面跟区域大小。16000个IP地址的状态信息约1MB,所以示例中区域可以存储160000个IP地址。
                      #        3)Rate定义最大请求速率。示例中速率不能超过每秒10个请求。
                     #        4)burst排队大小,瞬时大流量会放入队列。nodelay表示队列中请求并行执行。默认为串行执行

                     # lua的本地缓存空间。自定义名:my_cache
                    lua_shared_dict my_cache 128m;

                    upstream mysvr {
                       server 127.0.0.1:18083 weight=1 max_fails=2  fail_timeout=10s;
                    }

            server {
                       #监听端口
            listen       80;
            server_name  localhost;

            #charset koi8-r;
            #access_log  logs/host.access.log  main;
            #图片缓存时间设置
            location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
            {       #浏览器缓存10天
                    expires 10d;
            }
            #JS和CSS缓存时间设置
            location ~ .*\.(js|css)?$
            {       #浏览器缓存1小时
                    expires 1h;
            }

                #所有静态文件由nginx直接读取,不经过tomcat或resin
                    #		location ~ .*.(htm|html|gif|jpg|jpeg|png|bmp|swf|ioc|rar|zip|txt|flv|mid|doc|ppt|pdf|xls|mp3|wma)$
                    #		{ expires 15d; }
                    #		location ~ .*.(js|css)?$
                    #		{ expires 1h; }

     #设定查看Nginx状态的地址
     location /NginxStatus {
                    stub_status on;
                    access_log on;
                    auth_basic "NginxStatus";
                    auth_basic_user_file conf/htpasswd;
                    #htpasswd文件的内容可以用apache提供的htpasswd工具来产生。
    }

     location /a {
            proxy_pass  http://192.168.1.55:8788;

            #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Cookie $http_cookie;
    }

    location /b {
            proxy_pass  http://192.168.1.55:8787;

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Cookie $http_cookie;
    }

    location /luacache {
            #IP限流
            limit_req zone=mylimit burst=10 nodelay;

#            lua本地缓存实现。测试路径http://localhost/luacache?key=123
#            在不限流情况下压测对比 /luacache 和 /api。有缓存的/luacache性能提升10%以上,如果/api是java服务器则性能提升更巨大。
            content_by_lua_file lua/luacache.lua;
    }

    location /api {
            #lua模拟后端rest_api返回json。
            content_by_lua_file lua/api.lua;
    }

    location /redis {
                #限流
            limit_req zone=mylimit burst=10 nodelay;
                    #为每个请求执行redis.lua脚本(相对路径)
            content_by_lua_file lua/redis.lua;
    }


     #只有跳转过来的请求,才会享受location中的配置,比如缓存、超时等
     location @url1 {
            proxy_pass  http://mysvr;

            proxy_cache cache_one; #此处的cache_one必须于上一步配置的缓存区域名称相同
            proxy_cache_methods GET;  #缓存GET请求的响应数据(html、json...)
            proxy_cache_valid 200 304 2m;  #1d=1天  1h=1小时 1m=1分钟
            proxy_cache_valid 301 302 1d;
            proxy_cache_valid any 1m;
                                           #不同的请求设置不同的缓存时效
            proxy_cache_key $host$uri$is_args$args;
                #缓存的key

            proxy_connect_timeout 10;#跟后端服务器连接的超时时间_发起握手等候响应超时时间 (秒)
            proxy_read_timeout 10;#连接成功后_等候后端服务器响应的时间_其实已经进入后端的排队之中等候处理(秒)
            proxy_send_timeout 500;#后端服务器数据回传时间_就是在规定时间内后端服务器必须传完所有数据(秒)

            add_header X-Cache-Status $upstream_cache_status;  #在响应头查看缓存命中状态 X-Cache-Status
                           #		·MISS 未命中,请求被传送到后端
                           #		·HIT 缓存命中
                           #		·EXPIRED 缓存已经过期请求被传送到后端
                           #		·UPDATING 正在更新缓存,将使用旧的应答
                           #		·STALE 后端将得到过期的应答

                           #向后端服务转发内容
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Cookie $http_cookie;
    }

                #需要安装ngx_cache_purge插件
                    #location ~ /purge(/.*) {
                    #    #删除指定缓存区域cache_one的特定缓存文件$1$is_args$args
                    #    proxy_cache_purge cache_one $host$1$is_args$args;
                    #    #运行本机和10.0.217.0网段的机器访问,拒绝其它所有
                    #    allow           127.0.0.1;
                    #    allow           10.0.217.0/24;
                    #    deny          all;
                    #}

            error_page   500 502 503 504  /50x.html;
            location = /50x.html {
                root   html;
            }
    }

    }

\openresty-1.15.8.2-win64\lua\redis.lua

--获取客户端ip
local function get_client_ip()
    local headers=ngx.req.get_headers() --请求头
    local ip=headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"
    return ip
end

--连接池注意:复用连接,减少创建TCP链接的消耗。
--测试:不使用连接池,直接close。一定概率响应慢或超时现象,应是创建连接导致。
--1、连接池是每Worker进程的,而不是每Server的;
--2、当连接超过最大连接池大小时,会按照LRU算法回收空闲连接为新连接使用;
--3、连接池中的空闲连接出现异常时会自动被移除;
--4、连接池是通过ip和port标识的,即相同的ip和port会使用同一个连接池(即使是不同类型的客户端如Redis、Memcached);
--5、第一次set_keepalive时连接池大小就确定下了,不会再变更;
local function close_redis(red)
    if not red then
        return
    end
    --local ok, err = red:close()
    --释放连接回连接池(设置 空闲链接存活毫秒 + 连接池大小)
    local ok, err = red:set_keepalive(10000, 20)--设置空闲连接超时时间防止连接一直占用不释放
    if not ok then
        ngx.say("set keepalive error : ", err)
    end
end

local clientIP = get_client_ip();
--local cjson = require("cjson")  -- 引入json模块
--require引用openresty\lualib目录下的lua库
local redis=require "resty.redis";
local red=redis:new();
red:set_timeout(500);

--redis连接
local ok,err=red:connect("39.106.1.1", 6379);
if not ok then
    ngx.say("failed to connect redis ",err);
    --ngx.log(ngx.WARN, "failed to connect redis");
    close_redis(red);
    return ngx.exec("@url1");--链接redis失败,nginx跳转到 location @url1
end

ok,err = red:auth('xxxxxxx');
if not ok then
    ngx.say("failed to authenticate: ", err)
    return close_redis(red);
end
red:select('0');

--redis中获取缓存(red:后对应redis指令)
local message, err=red:get(clientIP);

if not message or message == ngx.null then --message ~= ngx.null
    ok, err = red:set(clientIP, "context");
    ok, err = red:expire(clientIP, 10);
    if not ok then
        ngx.say("failed to set cache: ", err);
        --ngx.log(ngx.WARN, "failed to set cache");
        return close_redis(red);
    end
    --nginx跳转到 location @url1
    ngx.exec("@url1");
else
    --注意default_type设置类型,部分类型会使say响应变为文件下载
    --命中缓存响应
    ngx.say("msg: ",message);
end

--ngx.redirect("http://www.elong.com") --302
ok,err=close_redis(red);

\openresty-1.15.8.2-win64\lua\api.lua 

local cjson = require "cjson"

local headers=ngx.req.get_headers() --请求头
local info=headers["info"]

--取得URL中参数
local urlarg = ngx.req.get_uri_args()
--获取url请求参数key的值,等效arg["key"]
local key =urlarg.key

--若是body=nil ,则前边加上ngx.req.read_body(),当前body可读
ngx.req.read_body()
local body = ngx.req.get_body_data()

if nil == body then
    --解决大body(16k)读不到的问题
    local body_file = ngx.req.get_body_file()
    if body_file then
        body = read_from_file(body_file)
    end
end

local res={}
res['code']=200
res['msg']='ok'
res['req_body']=body

--模拟后端restapi返回json
ngx.say(cjson.encode(res))



\openresty-1.15.8.2-win64\lua\luacache.lua  

--【nginx.conf 里面配置 lua_shared_dict my_cache 128m;】
--API说明: http://www.hangdaowangluo.com/archives/2762
--shared_dict—api:都保证原子性

--head获取客户端ip
local function get_client_ip()
    local headers=ngx.req.get_headers() --请求头
    local ip=headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"
    return ip
end

function get_from_cache(key)
    local cache_ngx = ngx.shared.my_cache
    local value = cache_ngx:get(key)
    return value
end

function set_to_cache(key, value, exptime)
    if not exptime then
        exptime = 0
    end

    local cache_ngx = ngx.shared.my_cache
    local succ, err, forcible = cache_ngx:set(key, value, exptime)
    return succ
end

function add_to_cache(key, value, exptime)
    --exptime过期时间s
    if not exptime then
        exptime = 0
    end

    local cache_ngx = ngx.shared.my_cache
    local success, err, forcible = cache_ngx:add(key, value, exptime)
    return success
    --success, err, forcible = ngx.shared.DICT:set(key, value, exptime?, flags?)
    --与【set】方法区别在于不会插入重复的键,如果待插入的key已经存在,将会返回nil和和err=”exists”
    --API保证原子性
    --参数
    --参数value:可设置 booleans, numbers, strings, 或 nil;
    --可选参数exptime:表明key的有效期时间,单位是秒(s),默认值为0,表明永远不会过期;
    --可选参数flags:是一个用户标志值,会在调用get方法时同时获取得到。
    --返回值
    --success:成功插入为true,插入失败为false
    --err:操作失败时的错误信息,可能类似add"no memory"
    --forcible:true表明需要通过强制删除(LRU算法)共享内存上其他字典项来实现插入,false表明没有删除
    --共享内存上的字典项来实现插入。
end

local cjson = require("cjson")  -- 引入json模块
--local request_uri = ngx.var.request_uri --带参数uri
--不带参数uri
local uri = ngx.var.uri
--取得URL中参数
local urlarg = ngx.req.get_uri_args()
--获取url请求参数key的值,urlarg.key等效urlarg["key"]
local key =urlarg.key

--ngx.req.read_body()=设置当前body可读
ngx.req.read_body()
local body = ngx.req.get_body_data()
if nil == body then
    --解决大body(16k)读不到的问题
    local body_file = ngx.req.get_body_file()
    if body_file then
        body = read_from_file(body_file)
    end
end

--查lua缓存
local value =get_from_cache(key)
local success = false

if not value or value == ngx.null then
    --子请求跳转nginx location /api。默认是GET,默认会转发head。
    local resp = ngx.location.capture("/api", {method=ngx.HTTP_POST,body = body,args=urlarg})
    ngx.say(resp.body,"miss-cache")
    --先响应用户,再缓存
    ngx.eof()
    if resp.status==200 then
        --不存在则放入缓存 ,相当于redis setnx
        success = add_to_cache(key,resp.body,10)
    end
else
    --命中缓存
    ngx.say(value,"hit")
end

 

你可能感兴趣的:(Nginx/OpenResty+Lua实战)