基于OpenResty的弹性网关实践(二)
五、11個指令介紹
OpenResty 有 11 個 *_by_lua指令,它們和 NGINX 階段的關(guān)系如下圖所示
其中, init_by_lua 只會在 Master 進程被創(chuàng)建時執(zhí)行,init_worker_by_lua 只會在每個 Worker 進程被創(chuàng)建時執(zhí)行。其他的 *_by_lua 指令則是由終端請求觸發(fā),會被反復(fù)執(zhí)行。
所以在 init_by_lua 階段,我們可以預(yù)先加載 Lua 模塊和公共的只讀數(shù)據(jù),這樣可以利用操作系統(tǒng)的 COW(copy on write)特性,來節(jié)省一些內(nèi)存。
對于業(yè)務(wù)代碼來說,其實大部分的操作都可以在 content_by_lua 里面完成,但我更推薦的做法,是根據(jù)不同的功能來進行拆分,比如下面這樣:
-
set_by_lua*: 流程分支處理判斷變量初始化
-
rewrite_by_lua*: 轉(zhuǎn)發(fā)、重定向、緩存等功能(例如特定請求代理到外網(wǎng))
-
access_by_lua*: IP 準入、接口權(quán)限等情況集中處理(例如配合 iptable 完成簡單防火墻)
-
content_by_lua*: 內(nèi)容生成
-
header_filter_by_lua*: 響應(yīng)頭部過濾處理(例如添加頭部信息)
-
body_filter_by_lua*: 響應(yīng)體過濾處理(例如完成應(yīng)答內(nèi)容統(tǒng)一成大寫)
-
log_by_lua*: 會話完成后本地異步完成日志記錄(日志可以記錄在本地,還可以同步到其他機器)
我們假設(shè),你對外提供了很多明文 API,現(xiàn)在需要增加自定義的加密和解密邏輯。那么請問,你需要修改所有 API 的代碼嗎?
# 明文協(xié)議版本 location /mixed {content_by_lua '...'; ? ? ? # 處理請求 }當(dāng)然不用。事實上,利用階段的特性,我們只需要簡單地在 access 階段解密,在 body filter 階段加密就可以了,原來 content 階段的代碼是不用做任何修改的:
# 加密協(xié)議版本 location /mixed {access_by_lua '...'; ? ? ? ?# 請求體解密content_by_lua '...'; ? ? ? # 處理請求,不需要關(guān)心通信協(xié)議body_filter_by_lua '...'; ? # 應(yīng)答體加密 }真實代碼示例:
下面是一個真實項目中權(quán)限校驗的某個Lua腳本
nginx.conf
location ~ /paopao/(game/callback/recharge) {access_by_lua_file lua/util/commonVerifyNotUid.lua;proxy_pass ? http://paopao;proxy_redirect off;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header Remote-Addr $remote_addr;proxy_set_header X-Forwarded-For $http_x_forwarded_for; } ? ? ? upstream paopao{server xxx.xxx.xxx.xxx:port max_fails=1 fail_timeout=8s weight=5; }commonVerifyNotUid.lua
module("util.commonVerify", package.seeall) local cjson = require"cjson" local config = require"config" local securityver = require"util.securityver" ? local args = ngx.req.get_uri_args() local headers = ngx.req.get_headers() ? --安全驗證 必須參數(shù) local requestId = headers.requestId --流水號 local appid = headers.appId -- appid local c = headers.c -- 平臺 local vn = headers.vn -- 版本 local ua = headers.ua --ua local sign = headers.sign -- 簽名 local u = headers.u -- 渠道號 local subAppId = headers.subAppId -- 子AppId ? ngx.req.read_body() local postargs = ngx.req.get_post_args() ? ? if not u or u == true or u == "" thenngx.log(ngx.ERR, "u header is nil")ngx.print(cjson.encode({ result = 1 , msg ='gateway parameter absent: u'}))ngx.exit(200)return end ? if not requestId or requestId == true or requestId == "" thenngx.log(ngx.ERR, "requestId header is nil")ngx.print(cjson.encode({ result = 1 , msg ='gateway parameter absent: requestId'}))ngx.exit(200)return end ? if not appid or appid == true or appid == "" then-- 同上 end ? if not c or c == true or c == "" then-- 同上 end ? if not vn or vn == true or vn == "" then-- 同上 end ? if not ua or ua == true or ua == "" then-- 同上 end ? if not sign or sign == true or sign == "" then-- 同上 end ? -- 黑名單imei local f = io.open("/data/paopao/paopao_gw/lua/black_list", "r") local black_list = f:read("*all") f:close() ? --ngx.log(ngx.INFO, black_list) black_list = loadstring("return " .. black_list)() ? function getimei(s)for k,v in string.gmatch(s, "(%w+)=(%w+)") doif(k == 'imei')thenreturn vendend end ? function ifblock(imei)for k, v in ipairs(black_list) doif v == imei thenreturn trueendendreturn false end? imei_str = getimei(ua) if ifblock(imei_str) thenngx.print(cjson.encode({ result = 1, msg = 'black list'}))ngx.log(ngx.INFO, "黑名單")ngx.exit(200)return end ? if ngx.var.args thenngx.var.args = ?ngx.var.args .. '&pv=' .. config.PV[c] .. '&v=' .. vn .. '&appId=' .. appid .. '&u=' .. u elsengx.var.args = ?'pv=' .. config.PV[c] .. '&v=' .. vn .. '&appId=' .. appid .. '&u=' .. u end ? if subAppId ~= nil thenngx.var.args = ngx.var.args .. '&subAppId=' .. subAppId end六、API介紹
OpenResty 是基于 NGINX 的 Web 服務(wù)器,但它與 NGINX 卻有本質(zhì)的不同:NGINX 由靜態(tài)的配置文件驅(qū)動,而 OpenResty 是由 Lua API 驅(qū)動的,所以能提供更多的靈活性和可編程性。
OpenResty 的 API 主要分為下面幾個大類:
-
處理請求和響應(yīng);
-
SSL 相關(guān);
-
shared dict;
-
cosocket;
-
處理四層流量;
-
process 和 worker;
-
獲取 NGINX 變量和配置;
-
字符串、時間、編解碼等通用功能。
penResty 的 API 不僅僅存在于 lua-nginx-module 項目中,也存在于 lua-resty-core 項目中,比如 ngx.ssl、ngx.base64、ngx.errlog、ngx.process、ngx.re.split、ngx.resp.add_header、ngx.balancer、ngx.semaphore、ngx.ocsp 這些 API 。
而對于不在 lua-nginx-module 項目中的 API,你需要單獨 require 才能使用。舉個例子,比如你想使用 split 這個字符串分割函數(shù),就需要按照下面的方法來調(diào)用:
? $ resty -e 'local ngx_re = require "ngx.re"local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})print(res)'下面我們介紹幾個常用的API:
1.獲取uri參數(shù)
獲取一個 uri 有兩個方法:ngx.req.get_uri_args、ngx.req.get_post_args,二者主要的區(qū)別是參數(shù)來源有區(qū)別。
參考下面例子:
server {listen ? 80;server_name localhost; ?location /print_param {content_by_lua_block {local arg = ngx.req.get_uri_args()for k,v in pairs(arg) dongx.say("[GET ] key:", k, " v:", v)end ?ngx.req.read_body() -- 解析 body 參數(shù)之前一定要先讀取 bodylocal arg = ngx.req.get_post_args()for k,v in pairs(arg) dongx.say("[POST] key:", k, " v:", v)end}} }輸出結(jié)果:
? ~ curl '127.0.0.1/print_param?a=1&b=2' -d 'c=3&d=4' [GET ] key:b v:2 [GET ] key:a v:1 [POST] key:d v:4 [POST] key:c v:3從以上輸入結(jié)果可以看出:前者來自 uri 請求參數(shù),而后者來自 post 請求內(nèi)容。
我們拿真實的案例來分析一下:
nginx文件中需要攔截的路徑加入下面代碼
access_by_lua_file lua/util/commonVerify.lua;以下為Lua文件中部分代碼
lua會獲取到get和post的請求參數(shù),然后會對參數(shù)加密得到一個sign,同時對客戶端傳遞過來的sign進行比對,如果失敗,則返回錯誤提示。
commonVerify.lua
local securityver = require"util.securityver" ? local args = ngx.req.get_uri_args() local postargs = ngx.req.get_post_args() ? ? --安全驗證 local status1, res = pcall(securityver.Verify, args, postargs) if not status1 thenngx.log(ngx.ERR, "error in function securityver.Verify. status is nil")ngx.print(cjson.encode({ result = 3 , msg ='gateway verify: inner exception' }))ngx.exit(200)return endseurityver.lua
module("util.securityver", package.seeall) ? package.path = package.path .. ';/data/uxin/http_gw_v3/lua/protobuf/?.lua' package.cpath = package.cpath .. ';/data/uxin/http_gw_v3/lua/protobuf/?.so' ? require 'ssid_pb' local config = require"config" ? local string = require("string") local commonutil = require"util.commonutil" ? ? local map = { d = 0, e = 1, y = 2, b = 3, i = 4, p = 5, v = 6, k = 7, z = 8, o = 9 } ? local function sha1(src)local resty_sha1 = require"resty.sha1"local sha1 = resty_sha1:new()if not sha1 thenreturn ""end ?local ok = sha1:update(src)if not ok thenreturn ""end ?local digest = sha1:final() -- binary digest ?local str = require"resty.string"return str.to_hex(digest) end ? local function VerifySign(args,postargs)local appid = ?ngx.req.get_headers().appidlocal ua = ?ngx.req.get_headers().ualocal requestId = ?ngx.req.get_headers().requestIdlocal vn = ?ngx.req.get_headers().vnlocal paramsTable = {}local sign = ngx.req.get_headers().signlocal signSrc = "" ?ngx.log(ngx.ERR, "----vn :" .. vn)if vn >= "1.0.0" then--table.merge(args, postargs)for k,v in pairs(postargs) dongx.log(ngx.ERR, "args k" .. k) ngx.log(ngx.ERR, "args type: " .. type(v)) if type(v) ~= 'table' thenngx.log(ngx.ERR, "args v" .. v) args[k] = vendendendngx.log(ngx.ERR, "----vn :" .. vn)if args thenfor k, v in pairs(args) doif k ~= "sign" and v ~= true and v ~= "" thenngx.log(ngx.ERR, "args k" .. k) table.insert(paramsTable, k)endendtable.sort(paramsTable, function(a, b)return string.lower(a) < string.lower(b)end) ?for i = 1, #(paramsTable) dongx.log(ngx.ERR, "arg:" .. paramsTable[i] .."--- " .. ?args[paramsTable[i]])signSrc = signSrc .. args[paramsTable[i]]endend ?--local SignKey = config.SIGNKEYngx.log(ngx.INFO, "----------")ngx.log(ngx.INFO, config.SIGN_KEY[appid])ngx.log(ngx.INFO, "----------")local SignKey = config.SIGN_KEY[appid]local signStr = signSrc .. requestId .. ua .. appid .. SignKeylocal sing1 = sha1(signStr)--ngx.log(ngx.ERR, "----bad sign, signStr:" .. signStr)--ngx.log(ngx.ERR, "----bad sign, source sign:" .. sign)--ngx.log(ngx.ERR, "----bad sign, native sign:" .. sing1)if ?sing1 ~= sign thenngx.log(ngx.ERR, "bad sign, signStr:" .. signStr)ngx.log(ngx.ERR, "bad sign, source sign:" .. sign)ngx.log(ngx.ERR, "bad sign, native sign:" .. sing1)return falseendreturn true end ? -- 防止用戶重復(fù)請求的方法,客戶端會傳一個requestId,為隨機生成的字符串 local function VerifyRequestId(args)--make sure account+sn is different in one hour, then sign is differentlocal uid = args.uidif not uid thenargs = ngx.req.get_uri_args()uid = args.uidendlocal snSet = ngx.shared.SnSetlocal sn = ngx.req.get_headers().requestId ?if uid thensn = uid .. snend--ngx.log(ngx.ERR, "duplicate requestId" .. sn)local success, err, forcible = snSet:add(sn, 1, 5 * 60)if not success and err == "exists" thenngx.log(ngx.ERR, "duplicate requestId" .. sn)return false-- return trueendreturn true end ? local function VerifyParam(args)return true end ? function Verify(args,postargs)-- 注銷--return trueif not VerifyParam(args) thenngx.log(ngx.ERR, "VerifyParam is error")return falseendif not VerifyRequestId(args) thenngx.log(ngx.ERR, "verify requestId is error")return falseend ?return VerifySign(args,postargs) endconfig.lua
我們將常量單獨存放到一個lua類中保管
module("config", package.seeall) ? SIGNKEY = "xxxxxxxxxxxxxxxxxxxxxxx"2.獲取請求body
在 Nginx 的典型應(yīng)用場景中,幾乎都是只讀取 HTTP 頭即可,例如負載均衡、正反向代理等場景。但是對于 API Server 或者 Web Application ,對 body 可以說就比較敏感了。由于 OpenResty 基于 Nginx ,所以天然的對請求 body 的讀取細節(jié)與其他成熟 Web 框架有些不同。
我們先來構(gòu)造最簡單的一個請求,POST 一個名字給服務(wù)端,服務(wù)端應(yīng)答一個 “Hello xxx”。
http {server {listen ? 80; ?location /test {content_by_lua_block {local data = ngx.req.get_body_data()ngx.say("hello ", data)}}} }測試結(jié)果:
? ~ curl 127.0.0.1/test -d jack hello nil大家可以看到 data 部分獲取為空,如果你熟悉其他 web 開發(fā)框架,估計立刻就覺得 OpenResty 弱爆了。查閱一下官方 wiki 我們很快知道,原來我們還需要添加指令 lua_need_request_body 。究其原因,主要是 Nginx 誕生之初主要是為了解決負載均衡情況,而這種情況,是不需要讀取 body 就可以決定負載策略的,所以這個點對于 API Server 和 Web Application 開發(fā)的同學(xué)有點怪。
參看下面例子:
http {server {listen ? 80; ?# 默認讀取 bodylua_need_request_body on; ?location /test {content_by_lua_block {local data = ngx.req.get_body_data()ngx.say("hello ", data)}}} }再次測試,符合我們預(yù)期:
? ~ curl 127.0.0.1/test -d jack hello jack3.日志輸出
OpenResty 的標準日志輸出原句為 ngx.log(log_level, ...) ,幾乎可以在任何 ngx_lua 階段進行日志的輸出。
我們用泡泡真實項目中的日志模擬一下sign簽名校驗錯誤的日志輸入
nginx中的日志配置如下:
? ? ? ?log_format ?main ?'$remote_addr - $remote_user [$time_local] "$request" ''$status $body_bytes_sent "$http_referer" ''"$http_user_agent" "-"' '"upstream_addr:' '$upstream_addr' '-response_time:' '$upstream_response_time"'; ? server{access_log ? logs/httpserver.access.log main;error_log ? logs/httpserver.error.log error; }httpserver.error.log日志如下:
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:95: bad sign, signStr:50492122955321ssadaadf1200rko753*qpsd5vbalt#$%^19plmo!@&kn, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345" 2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:96: bad sign, source sign:asadfdfa3124321asd, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345" 2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:97: bad sign, native sign:7b5a29291b78e9d7432e0a2bca39b94a1c8857e1, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345" 2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] commonVerify.lua:103: error in function securityver.Verify. res is nil, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gUid=122955&gameId=50492 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"4.發(fā)起http請求
OpenResty 最主要的應(yīng)用場景之一是 API Server,有別于傳統(tǒng) Nginx 的代理轉(zhuǎn)發(fā)應(yīng)用場景,API Server 中心內(nèi)部有各種復(fù)雜的交易流程和判斷邏輯。
① 引用 resty.http 庫資源,它來自 github https://github.com/pintsized/lua-resty-http。
② 參考 resty-http 官方 wiki 說明,我們可以知道 request_uri 函數(shù)完成了連接池、HTTP 請求等一系列動作。
下面的代碼是我們利用http庫自定義的一個簡單的發(fā)起http請求的方法:
module("util.httplib", package.seeall) ? local os = require("os") ? function geturl(purl)local http = require"resty.http"local hc = http:new()local startTime = os.time()local ok, code, headers, status, body = hc:request{url = purl,timeout = 4000,method = "GET"}if not ok thenngx.log(ngx.ERR, "WARN: " .. purl .. " code: " .. (code or "nil") .. " status: " .. (status or "nil"))endif (os.time() - startTime) >= 4 thenngx.log(ngx.ERR, "WARN: call remote server timeout. the url: " .. purl)endreturn ok, code, headers, status, body end ? ? function posturl(purl, pbody)local http = require"resty.http"local hc = http:new()local startTime = os.time()local ok, code, headers, status, body = hc:request{url = purl,timeout = 4000,method = "POST",body = pbody,-- add post content-type and cookieheaders = { ["Content-Type"] = "application/x-www-form-urlencoded" },}if not ok thenngx.log(ngx.ERR, "WARN: " .. purl .. " pbody: " .. pbody .. " code: " .. (code or "nil") .. " status: " .. (status or "nil"))endif (os.time() - startTime) >= 4 thenngx.log(ngx.ERR, "WARN: call remote server timeout. the url: " .. purl .. pbody)endreturn ok, code, headers, status, body end七、OpenResty緩存
ngx.shared.DICT
我們在nginx.conf中配置
lua_shared_dict SnSet 300m; lua_shared_dict Threshold 300m;我們可以用 shared dict 來共享數(shù)據(jù),這些數(shù)據(jù)可以在多個 worker 之間共享。內(nèi)部使用的 LRU 算法(最近最少使用)來判斷緩存是否在內(nèi)存占滿時被清除。
在上面的多處代碼中我們都使用了配置文件所定義好的全局變量。
它對外提供了 20 多個 Lua API,不過所有的這些 API 都是原子操作,你不用擔(dān)心多個 worker 和高并發(fā)的情況下的競爭問題。
這些 API 都有官方詳細的文檔,使用的時候可以查閱:shared_dict
繼續(xù)看 shared dict 的 API,這些 API 可以分為下面三個大類,也就是字典讀寫類、隊列操作類和管理類這三種。
1.字典讀寫類
首先來看字典讀寫類。在最初的版本中,只有字典讀寫類的 API,它們也是共享字典最常用的功能。下面是一個最簡單的示例:
$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogsdict:set("Tom", 56)print(dict:get("Tom"))'除了 set 外,OpenResty 還提供了 safe_set、add、safe_add、replace 這四種寫入的方法。這里safe 前綴的含義是,在內(nèi)存占滿的情況下,不根據(jù) LRU 淘汰舊的數(shù)據(jù),而是寫入失敗并返回 no memory 的錯誤信息。
除了 get 外,OpenResty 還提供了 get_stale 的讀取數(shù)據(jù)的方法,相比 get 方法,它多了一個過期數(shù)據(jù)的返回值:
value, flags, stale = ngx.shared.DICT:get_stale(key)還可以調(diào)用 delete 方法來刪除指定的 key,它和 set(key, nil) 是等價的。
2.管理類
用戶申請了 100M 的空間作為 shared dict,那么這 100M 是否夠用呢?里面存放了多少 key?具體是哪些 key 呢?
首先是 get_keys(max_count?),它默認也只返回前 1024 個 key;如果你把 max_count 設(shè)置為 0,那就返回所有 key。
然后是 capacity 和 free_space,這兩個 API 都屬于 lua-resty-core 倉庫,所以需要你 require 后才能使用:
? require "resty.core.shdict" ?local cats = ngx.shared.catslocal capacity_bytes = cats:capacity()local free_page_bytes = cats:free_space()它們分別返回的,是共享內(nèi)存的大小(也就是 lua_shared_dict 中配置的大小)和空閑頁的字節(jié)數(shù)。因為 shared dict 是按照頁來分配的,即使 free_space 返回為 0,在已經(jīng)分配的頁面中也可能存在空間,所以它的返回值并不能代表共享內(nèi)存實際被占用的情況。
3.隊列操作類
-
lpush/rpush,表示在隊列兩端增加元素;
-
lpop/rpop,表示在隊列兩端彈出元素;
-
llen,表示返回隊列的元素數(shù)量。
下面是代碼示例:
=== TEST 1: lpush & lpop --- http_configlua_shared_dict dogs 1m; --- configlocation = /test {content_by_lua_block {local dogs = ngx.shared.dogs ?local len, err = dogs:lpush("foo", "bar")if len thenngx.say("push success")elsengx.say("push err: ", err)end ?local val, err = dogs:llen("foo")ngx.say(val, " ", err) ?local val, err = dogs:lpop("foo")ngx.say(val, " ", err) ?local val, err = dogs:llen("foo")ngx.say(val, " ", err) ?local val, err = dogs:lpop("foo")ngx.say(val, " ", err)}} --- request GET /test --- response_body push success 1 nil bar nil 0 nil nil nil --- no_error_log [error]八、典型的應(yīng)用場景
-
在 Lua 中混合處理不同 Nginx 模塊輸出(proxy, drizzle, postgres, Redis, memcached 等)。
-
在請求真正到達上游服務(wù)之前,Lua 中處理復(fù)雜的準入控制和安全檢查。
-
比較隨意的控制應(yīng)答頭(通過 Lua)。
-
從外部存儲中獲取后端信息,并用這些信息來實時選擇哪一個后端來完成業(yè)務(wù)訪問。
-
在內(nèi)容 handler 中隨意編寫復(fù)雜的 web 應(yīng)用,同步編寫異步訪問后端數(shù)據(jù)庫和其他存儲。
-
在 rewrite 階段,通過 Lua 完成非常復(fù)雜的處理。
-
在 Nginx 子查詢、location 調(diào)用中,通過 Lua 實現(xiàn)高級緩存機制。
-
對外暴露強勁的 Lua 語言,允許使用各種 Nginx 模塊,自由拼合沒有任何限制。該模塊的腳本有充分的靈活性,同時提供的性能水平與本地 C 語言程序無論是在 CPU 時間方面以及內(nèi)存占用差距非常小。所有這些都要求 LuaJIT 2.x 是啟用的。其他腳本語言實現(xiàn)通常很難滿足這一性能水平。
總結(jié)
以上是生活随笔為你收集整理的基于OpenResty的弹性网关实践(二)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于OpenResty的弹性网关实践(一
- 下一篇: git的一些常用命令讲解和开发规范总结