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參數過濾,原理同上。
支持日誌記錄,將所有拒絕的操作,記錄到日誌中去
文檔:
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