本章內(nèi)容來自openresty討論組 這里
先看兩段代碼:
-- index.lua
local uri_args = ngx.req.get_uri_args()
local mo = require('mo')
mo.args = uri_args
-- mo.lua
local showJs = function(callback, data)
local cjson = require('cjson')
ngx.say(callback .. '(' .. cjson.encode(data) .. ')')
end
local self.jsonp = self.args.jsonp
local keyList = string.split(self.args.key_list, ',')
for i=1, #keyList do
-- do something
ngx.say(self.args.kind)
end
showJs(self.jsonp, valList)
大概代碼邏輯如上,然后出現(xiàn)這種情況:
生產(chǎn)服務(wù)器中,如果沒有用戶訪問,自己幾個(gè)人測試,一切正常。
同樣生產(chǎn)服務(wù)器,我將大量的用戶請求接入后,我不停刷新頁面的時(shí)候會(huì)出現(xiàn)部分情況(概率也不低,幾分之一,大于 10%),輸出的 callback(也就是來源于 self.jsonp,即 URL 參數(shù)中的 jsonp 變量)和 url 地址中不一致(我自己測試的值是 ?jsonp=jsonp1435220570933
,而用戶的請求基本上都是 ?jsonp=jquery....
)。錯(cuò)誤的情況都是會(huì)出現(xiàn)用戶請求才會(huì)有的 jquery....
這種字符串。另外 URL 參數(shù)中的 kind 是 1 ,我在循環(huán)中輸出會(huì)有“1”或“nil”的情況。不僅這兩種參數(shù),幾乎所有 url 中傳遞的參數(shù),都有可能變成其他請求鏈接中的參數(shù)。
基于以上情況,個(gè)人判斷會(huì)不會(huì)是在生產(chǎn)服務(wù)器大量用戶請求中,不同請求參數(shù)串掉了,但是如果這樣,是否應(yīng)該會(huì)出現(xiàn)我本次的獲取參數(shù)是某個(gè)其他用戶的值,那么 for 循環(huán)中的值也應(yīng)該固定的,而不會(huì)是一會(huì)兒是我自己請求中的參數(shù)值,一會(huì)兒是其他用戶請求中的參數(shù)值。
Lua module 是 VM 級別共享的,見這里。
mo.args
變量一不留神全局共享了,而這肯定不是作者期望的。所以導(dǎo)致了高并發(fā)應(yīng)用場景下偶爾出現(xiàn)異常錯(cuò)誤的情況。
每個(gè)請求的數(shù)據(jù)在傳遞和存儲(chǔ)時(shí)須特別小心,只應(yīng)通過你自己的函數(shù)參數(shù)來傳遞,或者通過 ngx.ctx 表。前者效率顯然較高,而后者勝在能跨階段使用。
貼一個(gè) ngx.ctx 的例子:
location /test {
rewrite_by_lua_block {
ngx.ctx.foo = 76
}
access_by_lua_block {
ngx.ctx.foo = ngx.ctx.foo + 3
}
content_by_lua_block {
ngx.say(ngx.ctx.foo)
}
}
在 OpenResty 里面,只有在 init_by_lua*
和 init_worker_by_lua*
階段才能定義真正的全局變量。
這是因?yàn)槠渌A段里面,OpenResty 會(huì)設(shè)置一個(gè)隔離的全局變量表,以免在處理過程污染了其他請求。
即使在上述兩個(gè)可以定義全局變量的階段,也盡量避免這么做。全局變量能解決的問題,用模塊變量也能解決,
而且會(huì)更清晰、更干凈。
這里把定義在模塊里面的變量稱為模塊變量。無論定義變量時(shí)有沒有加 local
,有沒有通過 _M
把變量引用起來,
定義在模塊里面的變量都是模塊變量。
由于 Lua VM 會(huì)把 require 進(jìn)來的模塊緩存到 package.loaded
表里,除非設(shè)置了 lua_code_cache off
,
模塊里定義的變量都會(huì)被緩存起來。而且重要的是,模塊變量在每個(gè)請求中是共享的。
模塊變量的跨請求特性,可以有很多用途。比如在變量間共享值,或者在 init_worker_by_lua*
中初始化全局用到的數(shù)值。
作為硬幣的反面,無視這一特性也會(huì)帶來許多問題。下面讓我們看看一個(gè)例子。
nginx.conf
location = /index {
content_by_lua_file conf/lua/web/index.lua;
}
index.lua
local var = require "var"
if var.calc() == 1 then
ngx.say("ok")
else
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
ngx.say("error")
end
var.lua
local count = 1
local _M = {}
local function add()
count = count + 1
end
local function sub()
count = count - 1
end
function _M.calc()
add()
-- 模擬協(xié)程調(diào)度
ngx.sleep(ngx.time()%0.003)
sub()
return count
end
return _M
分別用單個(gè)客戶端和兩個(gè)客戶端請求之:
~/test/openresty wrk --timeout 10s -t 1 -c 1 -d 1s http://localhost:6699/index
Running 1s test @ http://localhost:6699
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.22ms 291.09us 3.48ms 77.30%
Req/Sec 822.64 175.27 1.01k 54.55%
900 requests in 1.10s, 153.76KB read
~/test/openresty wrk --timeout 10s -t 2 -c 2 -d 1s http://localhost:6699/index
Running 1s test @ http://localhost:6699
2 threads and 2 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.18ms 387.03us 7.92ms 85.98%
Req/Sec 0.86k 168.12 1.02k 60.00%
1709 requests in 1.00s, 310.29KB read
Non-2xx or 3xx responses: 852
對于那些返回 500 狀態(tài)碼的請求,其調(diào)度過程大概是這樣的:
coroutine A | coroutine B | count
add | | 2
sleep | | 2
| add | 3
| sleep | 3
sub | | 2
(2 != 1) => HTTP_INTERNAL_SERVER_ERROR!
同樣道理,如果在模塊級別共享 TCP/UDP Client,比如在模塊開頭 local httpc = http.new()
,高并發(fā)下難免
會(huì)有奇怪的問題。把 httpc:send
看作 add
;httpc:receive
看作 sub
,幾乎就是上述的例子。
運(yùn)氣好的話,你可能只會(huì)碰到一個(gè) bad request
的異常;運(yùn)氣不好,就是一個(gè)潛在的坑。
跟全局變量、模塊變量相對,這里我們姑且把 *_by_lua*
里面定義的變量稱之為本地變量。
本地變量僅在當(dāng)前階段有效,如果要跨階段使用,需要借助 ngx.ctx
或者附加在模塊變量里。
值得注意的是 ngx.timer.*
。雖然 timer 代碼占的是別的上下文的位置,但是每個(gè) timer 都是運(yùn)行在自己的協(xié)程里面,
里面定義的變量都是協(xié)程內(nèi)部的。
舉個(gè)例子,讓我們在 init_worker_by_lua_block
里面定義一個(gè) timer。
init_worker_by_lua_block {
local delay = 5
local handler
handler = function()
counter = counter or 0
counter = counter + 1
ngx.log(ngx.ERR, counter)
local ok, err = ngx.timer.at(delay, handler)
if not ok then
ngx.log(ngx.ERR, "failed to create the timer: ", err)
return
end
end
local ok, err = ngx.timer.at(delay, handler)
if not ok then
ngx.log(ngx.ERR, "failed to create the timer: ", err)
return
end
}
counter 變量初看是定義在 init_worker_by_lua*
的全局變量。定義在 init_worker_by_lua*
階段,沒有 local
修飾,
根據(jù)前面的討論,它肯定是個(gè)全局變量嘛。
運(yùn)行一下,你會(huì)發(fā)現(xiàn),每次 counter 的輸出都是 1。
其實(shí) counter 實(shí)際的定義域是 handler 這個(gè)函數(shù)內(nèi)部。由于每個(gè) timer 都運(yùn)行在獨(dú)立的協(xié)程里,timer 的每次觸發(fā),
都會(huì)重新把 counter
定義一遍。如果要在 timer 的每次觸發(fā)中共享變量,你有兩個(gè)選擇:
(當(dāng)然也可以選擇在 init_worker_by_lua*
里面、ngx.timer.*
外面定義真正的全局變量,不過不太推薦罷了)