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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章