[code.openresty] Openresty框架-Nginx的lua模塊

openresty

OpenResty是一個(gè)基于 Nginx 與 Lua 的高性能 Web 平臺(tái),其內(nèi)部集成了大量精良的 Lua 庫(kù)、第三方模塊以及大多數(shù)的依賴項(xiàng)。用于方便地搭建能夠處理超高并發(fā)、擴(kuò)展性極高的動(dòng)態(tài) Web 應(yīng)用、Web 服務(wù)和動(dòng)態(tài)網(wǎng)關(guān)。
-- Openresty中文官方站

nginx的http lua模塊

Openresty的ngx_http_lua_module將lua的功能嵌入到nginx http服務(wù)中。這個(gè)模塊不是和原始的nginx服務(wù)分離的,而是打包在一起的,需要安裝包含有nginx完整功能和各個(gè)lua模塊的openresty框架。

概要

    
    # 設(shè)置純lua外部函數(shù)庫(kù)的搜索路徑(';;'代表默認(rèn)的路徑)
    lua_package_path '/foo/bar/?.lua;/blah/?.lua;;';
    
    # 設(shè)置用C語(yǔ)言編寫的lua外部函數(shù)庫(kù)的搜索路徑(也可以使用';;')
    lua_package_cpath '/bar/baz/?.so;/blah/blah/?.so;;';

    server {
        location /lua_content{
            #使用default_type確定MIME的類型
            default_type 'text/plain';
            
            content_by_lua_block{
                ngx.say('Hello,world!')
            }
            
        }
        
        location /nginx_var {
            #使用default_type確定MIME的類型
            default_type 'text/plain';
            
            #嘗試訪問(wèn) /nginx_var?a=hello,world
            content_by_lua_block {
                ngx.say(ngx.var.arg_a)
            }
        }
        
        location = /request_body {
            client_max_body_size 50k;
            client_body_buffer_size 50k;
            
            content_by_lua_blick {
                ngx.req.read_body() -- 明確要讀取請(qǐng)求的body
                local data = ngx.req.get_body_data()
                if data then
                    ngx.say("body data")
                    ngx.print(data)
                    return
                end
                
                -- body有可能緩存到一個(gè)臨時(shí)文件中
                local file = ngx.req.get_body_file()
                if file then
                    ngx.say("body is in file ",file)
                else
                    ngx.say("no body found")
                end
            }
        }
        
        # 在lua中非阻塞IO的子請(qǐng)求
        # (當(dāng)然,一個(gè)更好的方式是使用cosockets)
        location = /lua {
            # 使用default_type來(lái)確定MIME的類型
            default_type 'text/plain';
            
            content_by_lua_block {
                local res = ngx.location.capture("/some_other_location")
                if res then
                    ngx.say("status:",res.status)
                    ngx.say("body:")
                    ngx.print(res.body)
                end
            }
            
        }
        
        location = /foo {
            rewrite_by_lua_block {
                res = ngx.location.capture("/memc",
                    { args = { cmd = "incr",key = ngx.var.uri }}
                )
            }
            
            proxy_pass http://blah.blah.com
        }
        
        location = /mixed {
            rewrite_by_lua_file /path/to/rewrite.lua;
            access_by_lua_file /path/to/access.lua;
            content_by_lua_file /path/to/content.lua;
        }
        
        # 在代碼路徑中使用nginx變量
        # 警告:nginx變量中的內(nèi)容必須被小心的過(guò)濾出來(lái)
        # 否則這里會(huì)有嚴(yán)重的安全風(fēng)險(xiǎn)
        location ~ ^/app/([-_a-zA-Z0-9/]+) {
            set $path $1;
            content_by_lua_file /path/to/lua/app/root/$path.lua;
        }
        
        location / {
            client_max_body_size 100k;
            client_body_buffer_size 100k;
            
            access_by_lua_block {
                -- 檢查客戶端的IP地址是否在我們的黑名單中
                if ngx.var.remote_addr == "132.5.72.3" then
                    ngx.exit(ngx.HTTP_FORBIDDEN)
                end
                
                -- 檢查URI中是否含有不良的詞語(yǔ)
                if ngx.var.uri and 
                    string.match(nax.var.request_body,"evil")
                then
                    return ngx.redirect("/terms_of_use.html")
                end
                
                -- 測(cè)試通過(guò)
            }
            
            # proxy_pass/fastcgi_pass/etc settings
        }
        
    }

描述

lua-nginx-module嵌入了lua,把標(biāo)準(zhǔn)的lua 5.1解釋器或者LuaJIT 2.0/2.1嵌入到Nginx中并且通過(guò)利用Nginx的子請(qǐng)求,允許集成強(qiáng)大的lua線程 (Lua coroutines) 到Nginx的事件模型中。

Apache's mod_lua、Lighttpd's mod_magnet不同的是,Lua代碼通過(guò)這個(gè)模塊執(zhí)行可以在網(wǎng)絡(luò)通信中實(shí)現(xiàn)100%非阻塞,只要使用openresty的lua模特提供的Nginx API for lua去handle請(qǐng)求、upstream服務(wù)如MySQL、PostgreSQL、Memcached、Redis或者upstream HTTP web services。
至少如下的Lua函數(shù)庫(kù)和Nginx模塊能被此ngx_lua模塊使用到:

幾乎上面所有的Nginx模塊可以用ngx.location.capture or ngx.location.capture_multi來(lái)調(diào)用,但是這里推薦使用這些lua-resty-*函數(shù)庫(kù)來(lái)替代創(chuàng)建子請(qǐng)求訪問(wèn)Nginx upstream模塊,因?yàn)榍罢咄ǔ?lái)說(shuō)更加靈活和節(jié)省內(nèi)存。

Lua解釋器或者LuaJIT實(shí)例可以在一個(gè)單獨(dú)的nginx工作進(jìn)程中被所有的請(qǐng)求共享。但是在使用輕量級(jí)的Lua coroutines序時(shí)請(qǐng)求上下文是被隔離的。
留存在Nginx的工作進(jìn)程級(jí)別的那些已經(jīng)被加載的Lua模塊只會(huì)導(dǎo)致一個(gè)很小的內(nèi)存占用,即使是在沉重的負(fù)載下。
本lua-nginx模塊已經(jīng)被嵌入到NGINX的http子系統(tǒng)中,所以他只能識(shí)別HTTP家族中的downstream通信協(xié)議(HTTP 0.9/1.0/1.1/2.0,WebSockets等等)。如果你想通過(guò)downstream客戶端完成一個(gè)一般的TCP通信,那么你應(yīng)該使用ngx_stream_lua模塊來(lái)代替,此模塊也提供一個(gè)兼容的Lua API。

典型使用

只列舉出一部分:

  • 通過(guò)Lua來(lái)混合和加工各種nginx upstream輸出(proxy,drizzle,postgres,redis,memcached等等),
  • 通過(guò)Lua在請(qǐng)求實(shí)際到達(dá)upstream后端之前,做任意的復(fù)雜訪問(wèn)控制和安全監(jiān)測(cè),
  • 通過(guò)Lua任意的操縱返回的headers
  • 從外部存取后端中獲取后端信息(如:reids,memcached,mysql,postgresql),并且使用這些信息在傳輸過(guò)程中選擇使用哪個(gè)upstream后端,
  • 編寫任意的復(fù)雜web應(yīng)用程序,使用同步的但是仍然非阻塞的后端數(shù)據(jù)庫(kù)或者其他存儲(chǔ)設(shè)備來(lái)處理內(nèi)容。
  • 在rewrite階段通過(guò)Lua做一個(gè)非常復(fù)雜的URL dispatch,
  • 使用Lua為Nginx的子請(qǐng)求和任意的location實(shí)現(xiàn)高級(jí)的緩存機(jī)制。

此模塊有無(wú)限的發(fā)展?jié)摿?,因?yàn)榇四K允許在Nginx中結(jié)合各種模塊同時(shí)也在使用者面前展示了Lua的能力。這個(gè)模塊提供了完整的腳本語(yǔ)言的靈活性,同時(shí),在CUP時(shí)間和內(nèi)存占用方面提供了可以與C語(yǔ)言程序相比的性能水平。特別是LuaJIT 2.x被開啟時(shí)。

其他的腳本語(yǔ)言實(shí)現(xiàn)通常很難匹配到這個(gè)性能水平。

Lua狀態(tài)(Lua VM實(shí)例)在一個(gè)nginx工作進(jìn)程中被所有的請(qǐng)求共享來(lái)達(dá)到最少的內(nèi)存使用。

安裝

強(qiáng)烈推薦使用OpenResty releases,這個(gè)集成了Nginx,ngx_lua,LuaJIT2.1,同時(shí)也有其他的功能強(qiáng)大的Nginx模塊和Lua函數(shù)庫(kù)。不要自己通過(guò)nginx去構(gòu)造這個(gè)模塊因?yàn)楹茈y能夠完全配置正確。同時(shí),nginx的內(nèi)核有各種各樣的局限性和長(zhǎng)期存在的bug會(huì)導(dǎo)致一些模塊的特性變得有缺陷,不能正常的工作或者運(yùn)行緩慢。

作為一種選擇,ngx_lua可以被手動(dòng)的編譯到Nginx中:

  1. 安裝LuaJIT 2.0或2.1(建議)或者Lua5.1(Lua 5.2目前被支持)。LuaJIT能夠從LuaJIT project website上下載下來(lái),Lua 5.1從 Lua project website。一些發(fā)布包管理器同樣可以分發(fā)LuaJIT和/或Lua。
  2. 下載最新版的ngx_devel_kit(NDK)模塊 HERE。
  3. 下載最新版的ngx_luaHERE。
  4. 下載最新版本的的NginxHERE (See Nginx Compatibility)

通過(guò)這個(gè)模塊來(lái)構(gòu)建源:


    wget 'http://nginx.org/download/nginx-1.11.2.tar.gz'
    tar -xzvf nginx-1.11.2.tar.gz
    cd nginx-1.11.2/
 
    # 告訴nginx的構(gòu)建系統(tǒng)從哪里去尋找LuaJIT 2.0:
    export LUAJIT_LIB=/path/to/luajit/lib
    export LUAJIT_INC=/path/to/luajit/include/luajit-2.0
 
    # 告訴nigix的構(gòu)建系統(tǒng)從哪里去尋找LuaJIT 2.1:
    export LUAJIT_LIB=/path/to/luajit/lib
    export LUAJIT_INC=/path/to/luajit/include/luajit-2.1
    
    # 或者要是使用Lua替代的話告訴從哪去找lua
    #export LUA_LIB=/path/to/lua/lib
    #export LUA_INC=/path/to/lua/include
    
    # 這里我們假設(shè)Nginx被安裝在/opt/nginx/目錄中
    ./configure --prefix=/opt/nginx \
            --with-ld-opt="-Wl,-rpath,/path/to/luajit-or-lua/lib" \
            --add-module=/path/to/ngx_devel_kit \
            --add-module=/path/to/lua-nginx-module
    
    # 注意,你可能還想添加在你當(dāng)前nginx構(gòu)建時(shí)使用到的`./configure`選項(xiàng)
    # 你可以通過(guò)命令nginx -V來(lái)得到這些選項(xiàng)
    
    # 你可以改變下面平行結(jié)構(gòu)里面的數(shù)字2來(lái)適應(yīng)你機(jī)器上的CPU核心數(shù)
    make -j2
    make install
 

作為一個(gè)動(dòng)態(tài)模塊構(gòu)建

從Nginx 1.9.11開始,你也可以將此模塊編譯成為一個(gè)動(dòng)態(tài)模塊,通過(guò)在上面的./configure命令行中使用--add-dynamic-module=PATH選項(xiàng)來(lái)替代--add-module=PATH。然后你可以在你的nginx.conf中通過(guò)直接使用load_module來(lái)明確的加載該模塊。如:


    load_module /path/to/modules/ndk_http_module.so; # 假設(shè)NDK也是作為一個(gè)動(dòng)態(tài)模塊構(gòu)建
    load_module /path/to/modules/ngx_http_lua_module.so;
    

C語(yǔ)言宏定義

無(wú)論是通過(guò)OpenResty還是通過(guò)Nginx內(nèi)核來(lái)構(gòu)建這個(gè)模塊,你都可以通過(guò)C編譯選項(xiàng)定義下面的C語(yǔ)言宏。

  • NGX_LUA_USE_ASSERT
    當(dāng)定義了這個(gè) ,會(huì)在ngix_lua的C代碼庫(kù)開啟斷言。在排除錯(cuò)誤或者測(cè)試構(gòu)建時(shí)推薦定義。當(dāng)開啟后會(huì)有一些小的時(shí)間開銷。這個(gè)宏在v0.9.10版本時(shí)被首先引進(jìn)。

  • NGX_LUA_ABORT_AT_PANIC
    當(dāng)Lua/LuaJIT VM極度匆忙時(shí),ngx_lua會(huì)在默認(rèn)狀況下通知當(dāng)前的nginx工作進(jìn)程優(yōu)雅的退出。通過(guò)指定這個(gè)C語(yǔ)言宏,ngx_lua會(huì)立即終止當(dāng)前的nginx工作進(jìn)程(通常會(huì)導(dǎo)致一個(gè)核心的轉(zhuǎn)儲(chǔ)文件)。這個(gè)現(xiàn)象通常在調(diào)試VM極度匆忙是有用。這個(gè)宏在v0.9.8版本時(shí)被首先引進(jìn)。

  • NGX_LUA_NO_FFI_API
    排除Nginx的FFI-based Lua API中的純C API(例如,在lua-resty-core需要)。開啟這個(gè)宏可以使結(jié)果二進(jìn)制代碼體積更小。

要啟用一個(gè)或者多個(gè)這些宏,只需要在NGINX或者OpenResty的./configure腳本中通過(guò)額外的c編譯選項(xiàng)。例如:


./configure --with-cc-opt="-DNGX_LUA_USE_ASSERT -DNGX_LUA_ABORT_AT_PANIC"

在Ubuntu 11.10上進(jìn)行安裝

注意,只要有可能,推薦使用LuaJIT 2.0或者LuaJIT 2.1來(lái)代替標(biāo)準(zhǔn)的Lua 5.1。

無(wú)論如何,如果需要標(biāo)準(zhǔn)的Lua 5.1解釋器,執(zhí)行下面的命令來(lái)從Ubuntu庫(kù)中安裝它:


    apt-get install -y lua5.1 liblua5.1-0 liblua5.1-0-dev
    

除了一個(gè)小調(diào)整外,一切都應(yīng)該被正確安裝。

庫(kù)名稱liblua.so在liblua5.1 包中被改變了,它僅提供了liblua5.1.so,需要符號(hào)連接到/usr/lib從而使其可以在配置過(guò)程中被發(fā)現(xiàn)。


 ln -s /usr/lib/x86_64-linux-gnu/liblua5.1.so /usr/lib/liblua.so

Lua/LuaJIT字節(jié)碼支持

v0.5.0rc32版本開始,所有的*_by_lua_file配置指令(例如content_by_lua_file)支持直接加載Lua5.1和LuaJIT 2.0/2.1原始字節(jié)碼文件。

請(qǐng)注意LuaJIT 2.0/2.1使用的字節(jié)碼格式和標(biāo)準(zhǔn)的Lua 5.1解釋器使用的不兼容。所以如果通過(guò)ngx_lua使用LuaJIT 2.0/2.1,LuaJIT 兼容字節(jié)碼文件必須通過(guò)這樣生成:


    /path/to/luajit/bin/luajit -b /path/to/input_file.lua /path/to/output_file.ljbc

可以使用-bg選項(xiàng)來(lái)在LuaJIT字節(jié)碼文件中包含debug信息:


    /path/to/luajit/bin/luajit -bg /path/to/input_file.lua /path/to/output_file.ljbc
 

請(qǐng)參閱官方LuaJIT文檔查看-b選項(xiàng)的更多細(xì)節(jié):

http://luajit.org/running.html#opt_b

另外,通過(guò)LuaJIT 2.1生成的字節(jié)碼文件并兼容LuaJIT 2.0,反之亦然。對(duì)LuaJIT 2.1字節(jié)碼的支持在ngx_lua v0.9.3版本時(shí)被首先支持。

同樣的,如果在ngx_lua中使用標(biāo)準(zhǔn)的Lua 5.1解釋器,Lua兼容的字節(jié)碼文件必須使用luac命令工具生成,如下所示:


    luac -o /path/to/output_file.luac /path/to/input_file.lua

和LuaJIT不同的是,debug信息在標(biāo)準(zhǔn)的Lua5.1字節(jié)碼文件中默認(rèn)被包含。這個(gè)能夠指定-s選項(xiàng)來(lái)剝離,如下所示:


    lua -s -o /path/to/output_file.luac /path/to/input_file.lua
    

嘗試將標(biāo)準(zhǔn)的Lua5.1字節(jié)碼文件加載到鏈接到 LuaJIT 2.0/2.1的ngx_lua實(shí)例,會(huì)導(dǎo)致一個(gè)錯(cuò)誤信息,反之亦然。錯(cuò)誤如下所示,被記錄到Nginx的error.log文件中:


[error] 13909#0: *1 failed to load Lua inlined code: bad byte-code header in /path/to/test_file.luac

通過(guò)Lua原語(yǔ)如likedofile來(lái)加載字節(jié)碼文件,會(huì)一直按照預(yù)期工作。

系統(tǒng)環(huán)境變量支持

如果你想訪問(wèn)系統(tǒng)變量,如:foo,在Lua中通過(guò)標(biāo)準(zhǔn)的Lua API os.getenv,然后你還應(yīng)該在你的nginx.conf文件中通過(guò)env directive列出這個(gè)環(huán)境變量的名字。例如:


 env foo;
 

HTTP 1.0 支持

HTTP 1.0 協(xié)議不支持分塊輸出并且在響應(yīng)體不為空時(shí)需要一個(gè)明確的Content-Length header 從而支持HTTP 1.0長(zhǎng)連接。所以當(dāng)一個(gè)HTTP 1.0請(qǐng)求了,并且lua_http10_buffering指令是on,ngx_lua會(huì)緩沖ngx.sayngx.print調(diào)用的輸出,并且推遲發(fā)送響應(yīng)headers直到所有的響應(yīng)body被接收到。在那個(gè)時(shí)候ngx_lua可以計(jì)算出body的總長(zhǎng)度并且構(gòu)建一個(gè)合適的Content-Length header來(lái)返回給HTTP 1.0客戶端。如果Content-Length響應(yīng)header在運(yùn)行Lua代碼中被設(shè)置了,然而,這種緩沖將被禁用,即使lua_http10_buffering設(shè)置被至為on

對(duì)于大型流輸出響應(yīng),重要的是禁用lua_http10_buffering設(shè)置來(lái)使內(nèi)存使用降到最低。

注意,常見(jiàn)的HTTP基準(zhǔn)工具例如abhttp_load默認(rèn)發(fā)出HTTP 1.0請(qǐng)求,要強(qiáng)制curl發(fā)出HTTP 1.0請(qǐng)求,使用-0選項(xiàng)。

靜態(tài)鏈接純lua模塊

當(dāng)使用的是LuaJIT 2.X版本時(shí),可以靜態(tài)鏈接純Lua模塊到Nginx可執(zhí)行文件。

基本上你使用luajit可執(zhí)行文件來(lái)編譯.lua Lua模塊文件成.o對(duì)象文件包含導(dǎo)出的字節(jié)碼數(shù)據(jù),并且在你Nginx構(gòu)建時(shí)直接鏈接.o文件。

下面是一個(gè)簡(jiǎn)單的例子來(lái)展示這一點(diǎn)??紤]到我們有以下.lua文件命名為foo.lua:

-- foo.lua
local _M  = {}

function _M.go()
    print("Hello from foo")
end

return _M

然后我們編譯這個(gè).lua文件成為foo.o文件:

/path/to/luajit/bin/luajit -bg foo.lua foo.o

這里重要的是.lua文件的名稱,這個(gè)決定了之后你在Lua里面怎么使用這個(gè)模塊。這個(gè)foo.o的文件名稱并不重要,除了這個(gè).o的文件拓展(告訴luajit什么使用什么輸出格式)。如果你想從結(jié)果字節(jié)碼中剝離Lua的調(diào)試信息,就可以在上面中指定-b選項(xiàng)來(lái)替代-bg。

然后當(dāng)構(gòu)建Nginx或者OpenResty時(shí),通過(guò)這個(gè)--with-ld-opt="foo.o"選項(xiàng)來(lái)配置./configure腳本:

  ./configure --with-ld-opt="/path/to/foo.o" ...

最終,你可以在ngx_lua模塊中在任何Lua代碼中向下面這樣做:

  local foo = require "foo"
  foo.go()

這段代碼不再依賴于外部的foo.lua文件了,因?yàn)樗呀?jīng)被編譯進(jìn)ngix可執(zhí)行部分了。

如果你想在調(diào)用require的時(shí)候在Lua模塊名稱中使用點(diǎn),例如:

    local foo = require "resty.foo"

那么你需要在命令行工具luajit將文件編譯成.o之前,先重新命名那個(gè)foo.lua文件成resty_foo.lua。

非常重要的一點(diǎn)是,當(dāng)你編譯.lua文件成為.o文件和編譯ngix + ngx_lua時(shí),要使用相同版本的LuaJIT。這是因?yàn)椴煌姹镜腖uaJIT字節(jié)碼文件可能是不兼容的。當(dāng)字節(jié)碼是不兼容的時(shí)候,你會(huì)看到一個(gè)Lua運(yùn)行時(shí)錯(cuò)誤說(shuō)沒(méi)有找到Lua模塊。

當(dāng)你有多個(gè).lua文件要編譯和鏈接時(shí),只需要在--with-ld-opt選項(xiàng)中同時(shí)指定他們的.o文件。例如:

  ./configure --with-ld-opt="/path/to/foo.o /path/to/bar.o" ...

如果你有太多的.o文件,那么將他們?cè)趩螚l命令中都列出來(lái)它們是不可行的。在這種情況下,你可以將你的那些.o文件構(gòu)建一個(gè)靜態(tài)library(或者archive),例如:

 ar rcus libmyluafiles.a *.o

然后你可以連接整個(gè)myluafiles archive到你的nginx可執(zhí)行部分。

  ./configure \
        --with-ld-opt="-L/path/to/lib -Wl,--whole-archive -libmyluafiles
        --Wl,--no-whole-archive"

其中/path/to/lib是包含libmyluafiles.a文件的路徑。應(yīng)該注意的是鏈接選項(xiàng)--whole-archive在這里是必須的否則我們的archive會(huì)被忽略,這是因?yàn)樵趎ginx可執(zhí)行文件的主要部分,我們archive里的符號(hào)沒(méi)有被提到。

在同一個(gè)Nginx Worker中數(shù)據(jù)共享

為了全局共享在同一個(gè)nginx worker進(jìn)程中處理的所有requests中的數(shù)據(jù),封裝數(shù)據(jù)到一個(gè)Lua模塊中,使用Lua的內(nèi)建指令require來(lái)導(dǎo)入這個(gè)模塊,然后在Lua中操縱這些共享數(shù)據(jù)。這是可行的,因?yàn)檫@個(gè)模塊只被加載一次,而且所有的協(xié)同程序(coroutines )會(huì)共享這個(gè)模塊的同樣的副本(代碼和數(shù)據(jù))。但是要注意,Lua全局變量(注意,不是模塊級(jí)變量)不會(huì)在request之間存在,這是由于one-coroutine-per-request的孤立設(shè)計(jì)。

這里是一個(gè)完整的小例子:


-- mydata.lua
local _M = {}

local data = {
    dog = 3,
    cat = 4,
    pig = 5,
}

function _M.get_age(name)
    return data[name]
end

return _M

然后從nginx.conf中訪問(wèn)它


    location /lua {
        content_by_lua_block {
            local mydata = require "mydata"
            ngx.say(mydata.get_age("dog"))
        }
    }

這個(gè)例子中的mydata模塊只會(huì)在運(yùn)行對(duì)定位/lua的第一次請(qǐng)求中加載和運(yùn)行,并且所有對(duì)相同的nginx工作進(jìn)程的后續(xù)請(qǐng)求將會(huì)使用已經(jīng)被加載的實(shí)例模塊以及相同的數(shù)據(jù)副本,直到一個(gè)HUP信號(hào)被發(fā)送到nginx主進(jìn)程中去強(qiáng)制重載。這個(gè)數(shù)據(jù)共享技術(shù)對(duì)基于這個(gè)模塊的Lua應(yīng)用程序的高性能表現(xiàn)是至關(guān)重要的。

要注意這個(gè)數(shù)據(jù)共享是基于每個(gè)進(jìn)程基礎(chǔ)上而不是每個(gè)服務(wù)基礎(chǔ)上的,也就是說(shuō),在一個(gè)nginx master下有多個(gè)nginx工作進(jìn)程,數(shù)據(jù)共享不能穿越這些工作進(jìn)程的邊界。

通常建議以這種方式共享只讀數(shù)據(jù)。你也可以在每個(gè)nginx工作進(jìn)程的所有并發(fā)請(qǐng)求中共享可變數(shù)據(jù),只要在你計(jì)算期間這里沒(méi)有非阻塞I/O操作(包括ngx.sleep)。只要你不給nginx事件循環(huán)和ngx_lua的線程調(diào)度程序(甚至是隱式的)控制返回,這里永遠(yuǎn)不會(huì)有競(jìng)爭(zhēng)條件。出于這個(gè)原因,當(dāng)你想在進(jìn)程級(jí)別共享可變數(shù)據(jù)時(shí)一定要小心。錯(cuò)誤的優(yōu)化可以很容易地導(dǎo)致難以調(diào)試的競(jìng)態(tài)條件。

如果需要服務(wù)器范圍的數(shù)據(jù)共享,那么使用下列一個(gè)或多個(gè)方法:

  1. 使用本模塊提供的ngx.shared.DICT API。
  2. 僅僅使用一個(gè)nginx進(jìn)程和一個(gè)server(但是當(dāng)這里在一個(gè)機(jī)器上有多個(gè)CPU核心或者多個(gè)CPU時(shí)不推薦這么做)
  3. 使用數(shù)據(jù)存儲(chǔ)機(jī)制例如memcached,redis,MySQL或者PostgreSQL。與這個(gè)模塊關(guān)聯(lián)的OpenResty包(http://openresty.org)提供了于這些數(shù)據(jù)存儲(chǔ)機(jī)制進(jìn)行接口調(diào)用的一組相伴的nginx模塊和lua庫(kù)。

已知的問(wèn)題

TCP套接字連接操作的問(wèn)題

這個(gè)tcpsock:connect方法可能表示成功(success)盡管出現(xiàn)連接錯(cuò)誤如Connection Refused錯(cuò)誤。

然而,后面試圖操縱cosocket會(huì)失敗并且返回連接失敗操作生成的實(shí)際錯(cuò)誤狀態(tài)信息。

這個(gè)問(wèn)題是由于Nginx事件模型的局限性并且只出現(xiàn)影響 Mac OS X.

Lua 協(xié)同程序(Coroutine) 掛起(Yielding)/恢復(fù)(Resuming)

  • 因?yàn)槟壳癓ua的dofilerequire內(nèi)置指令目前在Lua 5.1和LuaJIT 2.0/2.1中是作為C語(yǔ)言函數(shù)實(shí)現(xiàn)的 。如果在要被dofile或者require加載的Lua文件中在lua文件頂級(jí)范圍里面調(diào)用ngx.location.capture*, ngx.exec, ngx.exit,或者其他需要掛起(yielding )的API方法,那么將會(huì)引起“attempt to yield across C-call boundary”的錯(cuò)誤。為了避免這個(gè)問(wèn)題,將這些需要掛起(yielding )的調(diào)用放在lua文件中你自己的lua方法中,而不是放在文件的頂級(jí)范圍內(nèi)。
  • 由于標(biāo)準(zhǔn)的Lua 5.1解釋器的VM并非完全可恢復(fù)的,這些 ngx.location.capture, ngx.location.capture_multi, ngx.redirect, ngx.exec, 和 ngx.exit方法,不能在Lua的pcall() 或者 xpcall()的上下文中被使用,當(dāng)標(biāo)準(zhǔn)的Lua 5.1 解釋器被使用時(shí)也不能在for ... in ...聲明的第一行中被使用。否則會(huì)產(chǎn)生attempt to yield across metamethod/C-call boundary的錯(cuò)誤。請(qǐng)使用支持完全恢復(fù)VM的LuaJIT 2.x來(lái)避免這個(gè)錯(cuò)誤。

Lua變量作用域

必須注意引入模塊的時(shí)候需要使用下面這形式:


  local xxx = require('xxx')

而不是使用舊的棄用的形式:


  require('xxx')

這是原因:在設(shè)計(jì)的時(shí)候,全局變量和與之關(guān)聯(lián)的Nginx請(qǐng)求處理(request handler)擁有完全相同的生命周期,每一個(gè)請(qǐng)求處理都有他自己的一組Lua全局變量并且這就是請(qǐng)求隔離的概念。這個(gè)Lua模塊實(shí)際上是由第一個(gè)Nginx請(qǐng)求處理加載的并且被require()緩存,構(gòu)建在package.loaded table里以便后續(xù)的引用,并且在一些Lua模塊里使用內(nèi)置的module()函數(shù)會(huì)有一些副作用也就是在加載了模塊的表中設(shè)置了而一些全局變量。但是這些全局變量會(huì)在這個(gè)請(qǐng)求處理結(jié)束后被清除掉,并且每一個(gè)后續(xù)的請(qǐng)求處理都有他自己的(干凈的)全局變量。所以其中一個(gè)會(huì)得到訪問(wèn)到nil值的Lua異常。

在ngx_lua的上下文中使用Lua全局變量通常是不明智的,因?yàn)椋?/p>

  1. 在并發(fā)請(qǐng)求中濫用Lua的全局變量有有害的副作用,而這些變量應(yīng)該在local范圍中被使用。
  2. 在Lua的全局環(huán)境變量表中查找Lua全局變量會(huì)引起一次昂貴的計(jì)算。
  3. 一些Lua全局變量的引用可能包含輸入(typing)錯(cuò)誤,而這些錯(cuò)誤很難被debug

因此強(qiáng)烈建議總是要用一個(gè)適當(dāng)?shù)膌ocal作用域來(lái)聲明。


  -- Avoid
  foo = 123
  -- Recommended
  local foo = 123

  -- Avoid
  function foo() return 123 end
  -- Recommended
  local function foo() return 123 end

要在你的Lua代碼里面找到所有的Lua全局變量的實(shí)例,可以在所有.lua源文件里運(yùn)行lua-releng tool命令。

$ lua-releng
  Checking use of Lua global variables in file lib/foo/bar.lua ...
          1       [1489]  SETGLOBAL       7 -1    ; contains
          55      [1506]  GETGLOBAL       7 -3    ; setvar
          3       [1545]  GETGLOBAL       3 -4    ; varexpand

輸出表明在文件lib/foo/bar.lua的第1489行寫入了一個(gè)名為contains的全局變量,在1506行讀取了全局變量contains,在1545讀取了全局變量varexpand

這個(gè)工具可以確保在Lua模塊的方法中定義的局部變量都聲明了local關(guān)鍵字,否則拋出一個(gè)運(yùn)行時(shí)的錯(cuò)誤。它阻止了訪問(wèn)這些變量時(shí)的不良競(jìng)爭(zhēng)條件。查看[在同一個(gè)Nginx Worker中數(shù)據(jù)共享]了解背后的原因

由其他模塊的子請(qǐng)求規(guī)則配置的Location

ngx.location.capturengx.location.capture_multi 指令不能capture包含add_before_body, add_after_body, auth_request, echo_location, echo_location_async, echo_subrequest, 或者 echo_subrequest_async 指令的location。


location /foo {
   content_by_lua_block{
     res = ngx.location.capture("/bar")
   }
}
location /bar {
   echo_location /blah;
}
location /blah {
   echo "Success!";
}
  $ curl -i http://example.com/foo

將不能按照預(yù)期正常工作。

Cosockets 不是在每個(gè)地方都有效

由于nginx核心的內(nèi)部限制,cosocket API 在下面這些上下文中是被禁用的:set_by_lua*, log_by_lua*, header_filter_by_lua*, 和 body_filter_by_lua

Cosockets目前同樣在init_by_lua*init_worker_by_lua*指令上下文中是被禁用的但是我們有可能在未來(lái)支持這些上下文因?yàn)樵趎ginx核心里面沒(méi)有限制(或者限制有可能工作)

這里存在一個(gè)變通的方法,無(wú)論怎樣,當(dāng)原始的上下文 需要等待cosocket結(jié)果。就是說(shuō),通過(guò)ngx.timer.at的API創(chuàng)建以一個(gè)零延時(shí)的計(jì)時(shí)器并且在定時(shí)器處理程序中處理cosocket結(jié)果,從而為了在原始上下文中創(chuàng)建定時(shí)器來(lái)異步執(zhí)行。

特殊的轉(zhuǎn)義序列

注意v0.9.17版本之后,這個(gè)缺陷,可以通過(guò)使用*_by_lua_block {}配置指令來(lái)避免。

PCRE序列例如\d\s,或者\w,需要特別注意因?yàn)樵谧址A恐?,反斜杠字符?code>\,在Lua語(yǔ)言解析器和nginx配置文件解析器中(如果不是在一個(gè)*_by_lua_block {}指令中)處理之前都被剔除了。所以下面的片段不會(huì)按照預(yù)期的那樣工作:


  #nginx.conf
  ? location /test {
  ?     content_by_lua '
  ?         local regex = "\d+"  -- THIS IS WRONG OUTSIDE OF A *_by_lua_block DIRECTIVE
  ?         local m = ngx.re.match("hello, 1234", regex)
  ?         if m then ngx.say(m[0]) else ngx.say("not matched!") end
  ?     ';
  ? }
  # 計(jì)算結(jié)果為 "not matched!"

為了避免這種情況,兩次 轉(zhuǎn)義那個(gè)反斜線:


 # nginx.conf
 location /test {
     content_by_lua '
         local regex = "\\\\d+"
         local m = ngx.re.match("hello, 1234", regex)
         if m then ngx.say(m[0]) else ngx.say("not matched!") end
     ';
 }
 # 計(jì)算結(jié)果為 "1234"

這里,\\\\d+ 會(huì)被Nginx配置文件解析器精簡(jiǎn)為\\d+ 并且在運(yùn)行時(shí)被Lua語(yǔ)言解析器進(jìn)一步精簡(jiǎn)為 \d+。

作為另一種選擇,這個(gè)正則表達(dá)式模式可以在Lua中可以通過(guò)一個(gè)長(zhǎng)方括號(hào)字符串的方式展示,通過(guò)把它放到“長(zhǎng)方括號(hào)里”,[[...]],在這種情況下,反斜線只需要在nginx配置文件解析器中被轉(zhuǎn)義一次。


# nginx.conf
location /test{
    content_by_lua '
        local regex = [[\\d+]]
        local m = ngx.re.match("hello,1234",regex)
        if m then ngx.say(m[0]) else ngx.say("not matched!") end
    ';
}
# 計(jì)算結(jié)果為 “1234”

這里, [[\\d+]]在Nginx配置文件解析器里面被精簡(jiǎn)為[[\d+]]并且這是能夠被正確處理的。

注意如果正則表表達(dá)式里面包含[...]序列的話,需要使用一種更長(zhǎng)的形式的長(zhǎng)方括號(hào), [=[...]=]。如果需要的話,[=[...]=]可以作為默認(rèn)的形式。

# nginx.conf
location /test {
      content_by_lua '
          local regex = [=[[0-9]+]=]
          local m = ngx.re.match("hello,12345",regex)
          if m then ngx.say(m[0]) else ngx.say("not matched!") end
      ';
}
# 計(jì)算結(jié)果為“1234”

另外一種轉(zhuǎn)義PCRE序列的方法是確保Lua代碼被放置在外部的腳本文件里,并且使用各種*_by_lua_file指令執(zhí)行。使用這種方法,反斜杠只會(huì)給Lua語(yǔ)言解析器精簡(jiǎn)并且每個(gè)只需要被轉(zhuǎn)義一次。


-- test.lua
local regex = "\\d+"
local m = ngx.re.match("hello,1234",regex)
if m then ngx.say(m[0]) else ngx.say("not matched!") end
-- 計(jì)算結(jié)果是“1234”

在外部腳本文件里,PCRE序列以長(zhǎng)方括號(hào)的Lua字符常量提供則不需要修改。


-- test.lua
local regex = [[\d+]]
local m = ngx.re.match("hello,1234",regex)
if m then ngx.say(m[0]) else ngx.say("not matched!") end
-- 計(jì)算結(jié)果為“1234”

正如上面所提到的,PCRE序列在*_by_lua_block{}指令中提供的不需要修改(從v0.9.17版本之后可用)。


#nginx.conf
location /test {
    content_by_lua_block {
        local regex = "\d+"
        local m = ngx.re.match("hello,1234",regex)
        if m then ngx.say(m[0]) else ngx.say("not matched!") end
    }
}  
#計(jì)算結(jié)果為“1234”

不支持混合SSI

不支持在同一個(gè)Nginx請(qǐng)求中混合ngx_lua和SSI。只單一的使用ngx_lua。你在SSI上能做的任何事都可以在ngx_lua上實(shí)現(xiàn)并且當(dāng)使用ngx_lua的時(shí)候會(huì)更有效率。

不完全支持SPDY模式

某些ngx_lua提供的Lua API在Nginx的SPDY模式下不能工作:ngx.location.capture, ngx.location.capture_multi, 和 ngx.req.socket.

在短路請(qǐng)求中缺失數(shù)據(jù)

Nginx可能提前終止一些請(qǐng)求(至少):

  • 400 (Bad Request)
  • 405 (Not Allowed)
  • 408 (Request Timeout)
  • 414 (Request URI Too Large)
  • 494 (Request Headers Too Large)
  • 499 (Client Closed Request)
  • 500 (Internal Server Error)
  • 501 (Not Implemented)

這意味著會(huì)跳過(guò)正常執(zhí)行的階段,例如rewrite或access階段。這同時(shí)意味著不管后面階段的執(zhí)行,例如log_by_lua,將不會(huì)獲得通常在這些階段設(shè)置的信息。

測(cè)試套件

在運(yùn)行測(cè)試套件時(shí)需要下面的依賴:

在設(shè)置測(cè)試環(huán)境是也可以查看developer build script的更多細(xì)節(jié)。

在默認(rèn)的測(cè)試模式下運(yùn)行整個(gè)測(cè)試套件:


    cd /path/to/lua-nginx-module
    export PATH=/path/to/your/nginx/sbin:$path
    prove -I/path/to/test-nginx/lib -r t
  
``

要執(zhí)行特殊的測(cè)試文件:

```bash

    cd /path/to/lua-nginx-module
    export PATH = /path/to/your/nginx/sbin:$PATH
    prove -I/path/to/test-nginx/lib t/002-connect.t t/003-errors.t

要在一個(gè)特殊的測(cè)試文件里面運(yùn)行一個(gè)特定的測(cè)試模塊,添加 --- ONLY 這行到你想運(yùn)行的測(cè)試模塊中,并且使用prove工具運(yùn)行那個(gè).t文件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容