用lua扩展你的Nginx(整理)
首先得聲明。這不是我的原創(chuàng),是在網(wǎng)上搜索到的一篇文章,原著是誰(shuí)也搞不清楚了。按風(fēng)格應(yīng)該是屬于章亦春的文章。
整理花了不少時(shí)間,所以就暫寫成原創(chuàng)吧。
?
一. 概述
Nginx是一個(gè)高性能。支持高并發(fā)的,輕量級(jí)的webserver。眼下,Apache依舊webserver中的老大,可是在全球前1000大的webserver中,Nginx的份額為22.4%。Nginx採(cǎi)用模塊化的架構(gòu),官方版本號(hào)的Nginx中大部分功能都是通過(guò)模塊方式提供的,比方Http模塊、Mail模塊等。通過(guò)開發(fā)模塊擴(kuò)展Nginx,能夠?qū)ginx打造成一個(gè)全能的應(yīng)用server,這樣能夠?qū)⒁恍┕δ茉谇岸薔ginx反向代理層解決,比方登錄校驗(yàn)、js合并、甚至數(shù)據(jù)庫(kù)訪問(wèn)等等。 ? ? 可是,Nginx模塊需要用C開發(fā),并且必須符合一系列復(fù)雜的規(guī)則。最重要的用C開發(fā)模塊必需要熟悉Nginx的源碼。使得開發(fā)人員對(duì)其望而生畏。淘寶的agentzh和chaoslawful開發(fā)的ngx_lua模塊通過(guò)將lua解釋器集成進(jìn)Nginx。能夠採(cǎi)用lua腳本實(shí)現(xiàn)業(yè)務(wù)邏輯,因?yàn)閘ua的緊湊、高速以及內(nèi)建協(xié)程,所以在保證高并發(fā)服務(wù)能力的同一時(shí)候極大地減少了業(yè)務(wù)邏輯實(shí)現(xiàn)成本。
? ? 本文向大家介紹ngx_lua,以及我在使用它開發(fā)項(xiàng)目的過(guò)程中遇到的一些問(wèn)題。
?
二. 準(zhǔn)備
首先,介紹一下Nginx的一些特性,便于后文介紹ngx_lua的相關(guān)特性。
?
Nginx進(jìn)程模型
Nginx採(cǎi)用多進(jìn)程模型,單Master—多Worker,由Master處理外部信號(hào)、配置文件的讀取及Worker的初始化。Worker進(jìn)程採(cǎi)用單線程、非堵塞的事件模型(Event Loop,事件循環(huán))來(lái)實(shí)現(xiàn)port的監(jiān)聽及client請(qǐng)求的處理和響應(yīng),同一時(shí)候Worker還要處理來(lái)自Master的信號(hào)。
因?yàn)閃orker使用單線程處理各種事件。所以一定要保證主循環(huán)是非堵塞的,否則會(huì)大大減少Worker的響應(yīng)能力。
Nginx處理Http請(qǐng)求的過(guò)程
表面上看,當(dāng)Nginx處理一個(gè)來(lái)自client的請(qǐng)求時(shí),先依據(jù)請(qǐng)求頭的host、ip和port來(lái)確定由哪個(gè)server處理,確定了server之后,再依據(jù)請(qǐng)求的uri找到相應(yīng)的location。這個(gè)請(qǐng)求就由這個(gè)location處理。
實(shí)際Nginx將一個(gè)請(qǐng)求的處理劃分為若干個(gè)不同階段(phase)。這些階段依照前后順序依次運(yùn)行。也就是說(shuō)NGX_HTTP_POST_READ_PHASE在第一個(gè),NGX_HTTP_LOG_PHASE在最后一個(gè)。
<span style="font-size:10px;">NGX_HTTP_POST_READ_PHASE, //0讀取請(qǐng)求phase NGX_HTTP_SERVER_REWRITE_PHASE,//1這個(gè)階段主要是處理全局的(server block)的rewrite NGX_HTTP_FIND_CONFIG_PHASE, //2這個(gè)階段主要是通過(guò)uri來(lái)查找相應(yīng)的location,然后依據(jù)loc_conf設(shè)置r的相應(yīng)變量 NGX_HTTP_REWRITE_PHASE, //3這個(gè)主要處理location的rewrite NGX_HTTP_POST_REWRITE_PHASE, //4postrewrite,這個(gè)主要是進(jìn)行一些校驗(yàn)以及收尾工作。以便于交給后面的模塊。NGX_HTTP_PREACCESS_PHASE, //5比方流控這樣的類型的access就放在這個(gè)phase,也就是說(shuō)它主要是進(jìn)行一些比較粗粒度的access。
NGX_HTTP_ACCESS_PHASE, //6這個(gè)比方存取控制,權(quán)限驗(yàn)證就放在這個(gè)phase,一般來(lái)說(shuō)處理動(dòng)作是交給以下的模塊做的.這個(gè)主要是做一些細(xì)粒度的access NGX_HTTP_POST_ACCESS_PHASE, //7一般來(lái)說(shuō)當(dāng)上面的access模塊得到access_code之后就會(huì)由這個(gè)模塊依據(jù)access_code來(lái)進(jìn)行操作 NGX_HTTP_TRY_FILES_PHASE, //8try_file模塊,就是相應(yīng)配置文件里的try_files指令??山邮斩鄠€(gè)路徑作為參數(shù)。當(dāng)前一個(gè)路徑的資源無(wú)法找到,則自己主動(dòng)查找下一個(gè)路徑 NGX_HTTP_CONTENT_PHASE, //9內(nèi)容處理模塊 NGX_HTTP_LOG_PHASE //10log模塊
每一個(gè)階段上能夠注冊(cè)handler。處理請(qǐng)求就是執(zhí)行每一個(gè)階段上注冊(cè)的handler。Nginx模塊提供的配置指令僅僅會(huì)一般僅僅會(huì)注冊(cè)并執(zhí)行在當(dāng)中的某一個(gè)處理階段。
比方,set指令屬于rewrite模塊的,執(zhí)行在rewrite階段,deny和allow執(zhí)行在access階段。
子請(qǐng)求(subrequest)
事實(shí)上在Nginx 世界里有兩種類型的“請(qǐng)求”。一種叫做“主請(qǐng)求”(main request),而還有一種則叫做“子請(qǐng)求”(subrequest)。 所謂“主請(qǐng)求”。就是由 HTTP client從 Nginx 外部發(fā)起的請(qǐng)求。比方。從瀏覽器訪問(wèn)Nginx就是一個(gè)“主請(qǐng)求”。 而“子請(qǐng)求”則是由 Nginx 正在處理的請(qǐng)求在 Nginx 內(nèi)部發(fā)起的一種級(jí)聯(lián)請(qǐng)求。“子請(qǐng)求”在外觀上非常像 HTTP 請(qǐng)求,但實(shí)現(xiàn)上卻和 HTTP 協(xié)議乃至網(wǎng)絡(luò)通信一點(diǎn)兒關(guān)系都沒(méi)有。它是 Nginx 內(nèi)部的一種抽象調(diào)用,目的是為了方便用戶把“主請(qǐng)求”的任務(wù)分解為多個(gè)較小粒度的“內(nèi)部請(qǐng)求”,并發(fā)或串行地訪問(wèn)多個(gè) location 接口。然后由這些 location 接口通力協(xié)作,共同完畢整個(gè)“主請(qǐng)求”。當(dāng)然。“子請(qǐng)求”的概念是相對(duì)的,不論什么一個(gè)“子請(qǐng)求”也能夠再發(fā)起很多其它的“子子請(qǐng)求”。甚至能夠玩遞歸調(diào)用(即自己調(diào)用自己)。
當(dāng)一個(gè)請(qǐng)求發(fā)起一個(gè)“子請(qǐng)求”的時(shí)候,依照 Nginx 的術(shù)語(yǔ),習(xí)慣把前者稱為后者的“父請(qǐng)求”(parent request)。
location /main {echo_location /foo; # echo_location發(fā)送子請(qǐng)求到指定的locationecho_location /bar; } location /foo {echo foo; } location /bar {echo bar; }輸出:
$ curl location/main
$ foo ? 03. ?bar
這里,main location就是發(fā)送2個(gè)子請(qǐng)求,分別到foo和bar。這就類似一種函數(shù)調(diào)用。
“子請(qǐng)求”方式的通信是在同一個(gè)虛擬主機(jī)內(nèi)部進(jìn)行的。所以 Nginx 核心在實(shí)現(xiàn)“子請(qǐng)求”的時(shí)候,就僅僅調(diào)用了若干個(gè) C 函數(shù),全然不涉及不論什么網(wǎng)絡(luò)或者 UNIX 套接字(socket)通信。我們由此能夠看出“子請(qǐng)求”的運(yùn)行效率是極高的。
協(xié)程(Coroutine)
協(xié)程類似一種多線程,與多線程的差別有:?
1. 協(xié)程并不是os線程,所以創(chuàng)建、切換開銷比線程相對(duì)要小。?
2. 協(xié)程與線程一樣有自己的棧、局部變量等,可是協(xié)程的棧是在用戶進(jìn)程空間模擬的,所以創(chuàng)建、切換開銷非常小。
3. 多線程程序是多個(gè)線程并發(fā)運(yùn)行。也就是說(shuō)在一瞬間有多個(gè)控制流在運(yùn)行。而協(xié)程強(qiáng)調(diào)的是一種多個(gè)協(xié)程間協(xié)作的關(guān)系,僅僅有當(dāng)一個(gè)協(xié)程主動(dòng)放棄運(yùn)行權(quán),還有一個(gè)協(xié)程才干獲得運(yùn)行權(quán),所以在某一瞬間,多個(gè)協(xié)程間僅僅有一個(gè)在運(yùn)行。?
4. 因?yàn)槎鄠€(gè)協(xié)程時(shí)僅僅有一個(gè)在執(zhí)行,所以對(duì)于臨界區(qū)的訪問(wèn)不須要加鎖。而多線程的情況則必須加鎖。
?
5. 多線程程序因?yàn)橛卸鄠€(gè)控制流。所以程序的行為不可控,而多個(gè)協(xié)程的運(yùn)行是由開發(fā)人員定義的所以是可控的。?
Nginx的每一個(gè)Worker進(jìn)程都是在epoll或kqueue這種事件模型之上,封裝成協(xié)程,每一個(gè)請(qǐng)求都有一個(gè)協(xié)程進(jìn)行處理。這正好與Lua內(nèi)建協(xié)程的模型是一致的,所以即使ngx_lua須要運(yùn)行Lua,相對(duì)C有一定的開銷,但依舊能保證高并發(fā)能力。
?
三. ngx_lua
原理
ngx_lua將Lua嵌入Nginx,能夠讓Nginx運(yùn)行Lua腳本,而且高并發(fā)、非堵塞的處理各種請(qǐng)求。Lua內(nèi)建協(xié)程。這樣就能夠非常好的將異步回調(diào)轉(zhuǎn)換成順序調(diào)用的形式。ngx_lua在Lua中進(jìn)行的IO操作都會(huì)托付給Nginx的事件模型。從而實(shí)現(xiàn)非堵塞調(diào)用。開發(fā)人員能夠採(cǎi)用串行的方式編敲代碼,ngx_lua會(huì)自己主動(dòng)的在進(jìn)行堵塞的IO操作時(shí)中斷。保存上下文;然后將IO操作托付給Nginx事件處理機(jī)制。在IO操作完畢后,ngx_lua會(huì)恢復(fù)上下文,程序繼續(xù)運(yùn)行,這些操作都是對(duì)用戶程序透明的。
每一個(gè)NginxWorker進(jìn)程持有一個(gè)Lua解釋器或者LuaJIT實(shí)例,被這個(gè)Worker處理的全部請(qǐng)求共享這個(gè)實(shí)例。
每一個(gè)請(qǐng)求的Context會(huì)被Lua輕量級(jí)的協(xié)程切割,從而保證各個(gè)請(qǐng)求是獨(dú)立的。 ngx_lua採(cǎi)用“one-coroutine-per-request”的處理模型。對(duì)于每一個(gè)用戶請(qǐng)求,ngx_lua會(huì)喚醒一個(gè)協(xié)程用于執(zhí)行用戶代碼處理請(qǐng)求,當(dāng)請(qǐng)求處理完畢這個(gè)協(xié)程會(huì)被銷毀。
?
每一個(gè)協(xié)程都有一個(gè)獨(dú)立的全局環(huán)境(變量空間),繼承于全局共享的、僅僅讀的“comman data”。所以。被用戶代碼注入全局空間的不論什么變量都不會(huì)影響其它請(qǐng)求的處理。而且這些變量在請(qǐng)求處理完畢后會(huì)被釋放,這樣就保證全部的用戶代碼都執(zhí)行在一個(gè)“sandbox”(沙箱),這個(gè)沙箱與請(qǐng)求具有同樣的生命周期。 得益于Lua協(xié)程的支持。ngx_lua在處理10000個(gè)并發(fā)請(qǐng)求時(shí)僅僅須要非常少的內(nèi)存。依據(jù)測(cè)試,ngx_lua處理每一個(gè)請(qǐng)求僅僅須要2KB的內(nèi)存,假設(shè)使用LuaJIT則會(huì)更少。所以ngx_lua非常適合用于實(shí)現(xiàn)可擴(kuò)展的、高并發(fā)的服務(wù)。
典型應(yīng)用
官網(wǎng)上列出:?
?
· Mashup’ing and processing outputs of various nginx upstream outputs(proxy, drizzle, postgres, redis, memcached, and etc) in Lua, · doing arbitrarily complex access control and security checks in Luabefore requests actually reach the upstream backends, · manipulating response headers in an arbitrary way (by Lua) · fetching backend information from external storage backends (likeredis, memcached, mysql, postgresql) and use that information to choose whichupstream backend to access on-the-fly, · coding up arbitrarily complex web applications in a content handlerusing synchronous but still non-blocking access to the database backends andother storage, · doing very complex URL dispatch in Lua at rewrite phase, · using Lua to implement advanced caching mechanism for nginxsubrequests and arbitrary locations.Hello Lua!
?
# nginx.conf worker_processes 4;events {worker_connections 1024; } http {server {listen 80;server_name localhost;location=/lua {content_by_lua ‘ngx.say("Hello, Lua!")';}} }輸出:
$ curl 'localhost/lua'
Hello,Lua。
這樣就實(shí)現(xiàn)了一個(gè)非常easy的ngx_lua應(yīng)用。假設(shè)這么簡(jiǎn)單的模塊要是用C來(lái)開發(fā)的話,代碼量預(yù)計(jì)得有100行左右。從這就能夠看出ngx_lua的開發(fā)效率。
Benchmark
通過(guò)和nginx訪問(wèn)靜態(tài)文件還有nodejs比較,來(lái)看一下ngx_lua提供的高并發(fā)能力。 返回的內(nèi)容都是”Hello World!”,151bytes 通過(guò).ab -n 60000 ? 取10次平均
從圖表中能夠看到,在各種并發(fā)條件下ngx_lua的rps都是最高的。而且基本維持在10000rps左右,nginx讀取靜態(tài)文件由于會(huì)有磁盤io所以性能略差一些,而nodejs是相對(duì)最差的。通過(guò)這個(gè)簡(jiǎn)單的測(cè)試,能夠看出ngx_lua的高并發(fā)能力。 ngx_lua的開發(fā)人員也做過(guò)一個(gè)測(cè)試對(duì)照nginx+fpm+php和nodejs,他得出的結(jié)果是ngx_lua能夠達(dá)到28000rps。而nodejs有10000多一點(diǎn)。php則最差僅僅有6000??赡苁怯行┡渲梦覜](méi)有配好導(dǎo)致ngx_lua rps沒(méi)那么高。
?
?
ngx_lua安裝
ngx_lua安裝能夠通過(guò)下載模塊源代碼,編譯Nginx??墒峭扑]採(cǎi)用openresty。Openresty就是一個(gè)打包程序,包括大量的第三方Nginx模塊,比方HttpLuaModule,HttpRedis2Module,HttpEchoModule等。省去下載模塊。而且安裝很方便。
ngx_openresty bundle: openresty ./configure --with-luajit&& make && make install 默認(rèn)Openresty中ngx_lua模塊採(cǎi)用的是標(biāo)準(zhǔn)的Lua5.1解釋器。通過(guò)--with-luajit使用LuaJIT。
ngx_lua的使用方法
ngx_lua模塊提供了配置指令和Nginx API。
配置指令:在Nginx中使用,和set指令和pass_proxy指令用法一樣。每一個(gè)指令都有使用的context。 ? ? ? ?Nginx API:用于在Lua腳本中訪問(wèn)Nginx變量,調(diào)用Nginx提供的函數(shù)。 以下舉例說(shuō)明常見的指令和API。
?
配置指令
set_by_lua和set_by_lua_file
和set指令一樣用于設(shè)置Nginx變量而且在rewrite階段運(yùn)行,僅僅只是這個(gè)變量是由lua腳本計(jì)算并返回的。
語(yǔ)法:set_by_lua$res <lua-script-str> [$arg1 $arg2 ...]
配置:
location =/adder {set_by_lua $res"local a = tonumber(ngx.arg[1])local b = tonumber(ngx.arg[2])return a + b"$arg_a$arg_b;echo$res; }輸出:
$ curl 'localhost/adder?a=25&b=75'
$ 100
set_by_lua_file運(yùn)行Nginx外部的lua腳本,能夠避免在配置文件里使用大量的轉(zhuǎn)義。
?
配置:
location =/fib {set_by_lua_file $res "conf/adder.lua" $arg_n;echo $res; }</span>?
adder.lua:
local a=tonumber(ngx.arg[1]) local b=tonumber(ngx.arg[2]) return a + b
輸出:
$ curl 'localhost/adder?a=25&b=75
$ 100
?
access_by_lua和access_by_lua_file
執(zhí)行在access階段。用于訪問(wèn)控制。
Nginx原生的allow和deny是基于ip的。通過(guò)access_by_lua能完畢復(fù)雜的訪問(wèn)控制。比方。訪問(wèn)數(shù)據(jù)庫(kù)進(jìn)行username、password驗(yàn)證等。
配置:
location /auth {access_by_lua 'if ngx.var.arg_user == "ntes" thenreturnelseNgx.exit(ngx.HTTP_FORBIDDEN)end';echo'welcome ntes'; }輸出:
$ curl 'localhost/auth?user=sohu'
$ Welcome ntes
$ curl 'localhost/auth?user=ntes'
$ <html>
<head><title>403 Forbidden</title></heda>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>ngx_openresty/1.0.10.48</center>
</body>
</html>
?
rewrite_by_lua和rewrite_by_lua_file
實(shí)現(xiàn)url重寫。在rewrite階段運(yùn)行。
配置:
location =/foo {rewrite_by_lua 'ngx.exec("/bar")';echo'in foo'; }location =/bar {echo'in bar'; }輸出:
$ curl 'localhost/lua'
$ Hello, Lua!
content_by_lua和content_by_lua_file
?
Contenthandler在content階段運(yùn)行,生成http響應(yīng)。因?yàn)閏ontent階段僅僅能有一個(gè)handler。所以在與echo模塊使用時(shí),不能同一時(shí)候生效,我測(cè)試的結(jié)果是content_by_lua會(huì)覆蓋echo。這和之前的hello world的樣例是類似的。
?
配置(直接響應(yīng)):
location =/lua {content_by_lua 'ngx.say("Hello, Lua!")'; }
輸出:
$ curl 'localhost/lua'
$ Hello, Lua!
配置(在Lua中訪問(wèn)Nginx變量):
輸出:
$ curl 'localhost/hello?who=world
$ Hello, world!
Nginx API
Nginx API被封裝ngx和ndk兩個(gè)package中。
比方ngx.var.NGX_VAR_NAME能夠訪問(wèn)Nginx變量。這里著重介紹一下ngx.location.capture和ngx.location.capture_multi。
ngx.location.capture
語(yǔ)法:res= ngx.location.capture(uri, options?) ? ? 用于發(fā)出一個(gè)同步的,非堵塞的Nginxsubrequest(子請(qǐng)求)。
能夠通過(guò)Nginx subrequest向其他location發(fā)出非堵塞的內(nèi)部請(qǐng)求。這些location能夠是配置用于讀取目錄的,也能夠是其他的C模塊,比方ngx_proxy, ngx_fastcgi, ngx_memc, ngx_postgres, ngx_drizzle甚至是ngx_lua自己。 ? ? Subrequest僅僅是模擬Http接口,并沒(méi)有額外的Http或者Tcp傳輸開銷,它在C層次上執(zhí)行,很高效。Subrequest不同于Http 301/302重定向,以及內(nèi)部重定向(通過(guò)ngx.redirection)。
配置:
輸出:
$ curl ?'http://localhost/lua'
$ Hello, world!
實(shí)際上,location能夠被外部的Http請(qǐng)求調(diào)用,也能夠被內(nèi)部的子請(qǐng)求調(diào)用。每一個(gè)location相當(dāng)于一個(gè)函數(shù),而發(fā)送子請(qǐng)求就類似于函數(shù)調(diào)用。并且這樣的調(diào)用是非堵塞的,這就構(gòu)造了一個(gè)很強(qiáng)大的變成模型,后面我們會(huì)看到怎樣通過(guò)location和后端的memcached、redis進(jìn)行非堵塞通信。
ngx.location.capture_multi
語(yǔ)法:res1,res2, ... = ngx.location.capture_multi({ {uri, options?}, {uri, options?}, ...}) ? ? 與ngx.location.capture功能一樣,能夠并行的、非堵塞的發(fā)出多個(gè)子請(qǐng)求。這種方法在全部子請(qǐng)求處理完畢后返回。而且整個(gè)方法的執(zhí)行時(shí)間取決于執(zhí)行時(shí)間最長(zhǎng)的子請(qǐng)求,并非全部子請(qǐng)求的執(zhí)行時(shí)間之和。
配置:
輸出:
$ curl ?'http://localhost/lua'
$ moon,earth
注意
在Lua代碼中的網(wǎng)絡(luò)IO操作僅僅能通過(guò)Nginx Lua API完畢。假設(shè)通過(guò)標(biāo)準(zhǔn)Lua API會(huì)導(dǎo)致Nginx的事件循環(huán)被堵塞,這樣性能會(huì)急劇下降。 ? ? 在進(jìn)行數(shù)據(jù)量相當(dāng)小的磁盤IO時(shí)能夠採(cǎi)用標(biāo)準(zhǔn)Lua io庫(kù),可是當(dāng)讀寫大文件時(shí)這樣是不行的,由于會(huì)堵塞整個(gè)NginxWorker進(jìn)程。為了獲得更大的性能。強(qiáng)烈建議將全部的網(wǎng)絡(luò)IO和磁盤IO托付給Nginx子請(qǐng)求完畢(通過(guò)ngx.location.capture)。 ? ? 以下通過(guò)訪問(wèn)/html/index.html這個(gè)文件。來(lái)測(cè)試將磁盤IO托付給Nginx和通過(guò)Lua io直接訪問(wèn)的效率。 ? ? 通過(guò)ngx.location.capture托付磁盤IO:
?
配置:
location / {internal;root html; }location /capture {content_by_lua 'res = ngx.location.capture("/")echo res.body'; }通過(guò)標(biāo)準(zhǔn)lua io訪問(wèn)磁盤文件:
配置:
這里通過(guò)ab去壓,在各種并發(fā)條件下,分別返回151bytes、151000bytes的數(shù)據(jù),取10次平均,得到兩種方式的rps。 ? ? 靜態(tài)文件:151bytes
1000 3000 5000 7000 10000 ?capture ?11067 8880 8873 8952 9023 ?Lua io ? ? 11379 9724 8938 9705 9561
靜態(tài)文件:151000bytes。在10000并發(fā)下內(nèi)存占用情況太嚴(yán)重。測(cè)不出結(jié)果 ? ? ? ?這樣的情況下,文件較小,通過(guò)Nginx訪問(wèn)靜態(tài)文件須要額外的系統(tǒng)調(diào)用,性能略遜于ngx_lua。
1000 3000 5000 7000 ? ?10000 ?capture ? ?3338 3435 3178 3043 ? ? ? ? / ?Lua io ? ? ?3174 3094 3081 2916 ? ? ? ? /
在大文件的情況。capture就要略好于ngx_lua。 ? ? ?這里沒(méi)有對(duì)Nginx讀取靜態(tài)文件進(jìn)行優(yōu)化配置。僅僅是採(cǎi)用了sendfile。
假設(shè)優(yōu)化一下??赡躰ginx讀取靜態(tài)文件的性能會(huì)更好一些,這個(gè)眼下還不熟悉。
所以,在Lua中進(jìn)行各種IO時(shí)。都要通過(guò)ngx.location.capture發(fā)送子請(qǐng)求托付給Nginx事件模型,這樣能夠保證IO是非堵塞的。
?
四. 小結(jié)
這篇文章簡(jiǎn)介了一下ngx_lua的基本使用方法。后一篇會(huì)對(duì)ngx_lua訪問(wèn)redis、memcached已經(jīng)連接池進(jìn)行具體介紹。
?
五. 進(jìn)階
在之前的文章中。已經(jīng)介紹了ngx_lua的一些基本介紹,這篇文章主要著重討論一下怎樣通過(guò)ngx_lua同后端的memcached、redis進(jìn)行非堵塞通信。
Memcached
在Nginx中訪問(wèn)Memcached須要模塊的支持,這里選用HttpMemcModule,這個(gè)模塊能夠與后端的Memcached進(jìn)行非堵塞的通信。我們知道官方提供了Memcached,這個(gè)模塊僅僅支持get操作。而Memc支持大部分Memcached的命令。 Memc模塊採(cǎi)用入口變量作為參數(shù)進(jìn)行傳遞。全部以$memc_為前綴的變量都是Memc的入口變量。
memc_pass指向后端的Memcached Server。
配置:
輸出:
$ curl ?'http://localhost/memc?cmd=set&key=foo&val=Hello'
$ STORED
$ curl ?'http://localhost/memc?
cmd=get&key=foo'
$ Hello
這就實(shí)現(xiàn)了memcached的訪問(wèn)。以下看一下怎樣在lua中訪問(wèn)memcached。
配置:
輸出:
$ curl ?'http://localhost/lua_memc?
key=foo'
$ Hello
通過(guò)lua訪問(wèn)memcached。主要是通過(guò)子請(qǐng)求採(cǎi)用一種類似函數(shù)調(diào)用的方式實(shí)現(xiàn)。
首先。定義了一個(gè)memc location用于通過(guò)后端memcached通信,就相當(dāng)于memcached storage。
?
因?yàn)檎麄€(gè)Memc模塊時(shí)非堵塞的。ngx.location.capture也是非堵塞的,所以整個(gè)操作非堵塞。
?
Redis
訪問(wèn)redis須要HttpRedis2Module的支持,它也能夠同redis進(jìn)行非堵塞通行。只是,redis2的響應(yīng)是redis的原生響應(yīng),所以在lua中使用時(shí),須要解析這個(gè)響應(yīng)。能夠採(cǎi)用LuaRedisModule,這個(gè)模塊能夠構(gòu)建redis的原生請(qǐng)求。并解析redis的原生響應(yīng)。
配置:
輸出:
$ curl ?'http://localhost/lua_redis?key=foo'
$ Hello
和訪問(wèn)memcached類似。須要提供一個(gè)redis storage專門用于查詢r(jià)edis,然后通過(guò)子請(qǐng)求去調(diào)用redis。
Redis Pipeline
在實(shí)際訪問(wèn)redis時(shí),有可能須要同一時(shí)候查詢多個(gè)key的情況。
我們能夠採(cǎi)用ngx.location.capture_multi通過(guò)發(fā)送多個(gè)子請(qǐng)求給redis storage,然后在解析響應(yīng)內(nèi)容。
可是,這會(huì)有個(gè)限制,Nginx內(nèi)核規(guī)定一次能夠發(fā)起的子請(qǐng)求的個(gè)數(shù)不能超過(guò)50個(gè)。所以在key個(gè)數(shù)多于50時(shí),這樣的方案不再適用。
幸好redis提供pipeline機(jī)制。能夠在一次連接中運(yùn)行多個(gè)命令,這樣能夠降低多次運(yùn)行命令的往返時(shí)延。
?
client在通過(guò)pipeline發(fā)送多個(gè)命令后。redis順序接收這些命令并運(yùn)行,然后依照順序把命令的結(jié)果輸出出去。在lua中使用pipeline須要用到redis2模塊的redis2_raw_queries進(jìn)行redis的原生請(qǐng)求查詢。
?
配置:
#在Lua中訪問(wèn)Redis location =/redis {internal; #僅僅能內(nèi)部訪問(wèn)redis2_raw_queries $args$echo_request_body;redis2_pass '127.0.0.1:6379'; }location =/pipeline {content_by_lua 'conf/pipeline.lua'; }pipeline.lua
-- conf/pipeline.lua file local parser=require(‘redis.parser’) local reqs={{‘get’, ‘one’}, {‘get’, ‘two’} } -- 構(gòu)造原生的redis查詢。get one\r\nget two\r\n local raw_reqs={} for i, req in ipairs(reqs)dotable.insert(raw_reqs, parser.build_query(req)) end local res=ngx.location.capture(‘/redis?’..#reqs, {body=table.concat(raw_reqs, ‘’)}) if res.status and res.body then -- 解析redis的原生響應(yīng) local replies=parser.parse_replies(res.body, #reqs) for i, reply in ipairs(replies)do ngx.say(reply[1]) end end
輸出:
$ curl ?'http://localhost/pipeline'
$ first
? second
?
Connection Pool
前面訪問(wèn)redis和memcached的樣例中。在每次處理一個(gè)請(qǐng)求時(shí)。都會(huì)和后端的server建立連接。然后在請(qǐng)求處理完之后這個(gè)連接就會(huì)被釋放。
這個(gè)過(guò)程中,會(huì)有3次握手、timewait等一些開銷。這對(duì)于高并發(fā)的應(yīng)用是不可容忍的。這里引入connection pool來(lái)消除這個(gè)開銷。 連接池須要HttpUpstreamKeepaliveModule模塊的支持。
配置:
這個(gè)模塊提供keepalive指令。它的context是upstream。我們知道upstream在使用Nginx做反向代理時(shí)使用。實(shí)際upstream是指“上游”。這個(gè)“上游”能夠是redis、memcached或是mysql等一些server。upstream能夠定義一個(gè)虛擬server集群,而且這些后端的server能夠享受負(fù)載均衡。keepalive 1024就是定義連接池的大小,當(dāng)連接數(shù)超過(guò)這個(gè)大小后,興許的連接自己主動(dòng)退化為短連接。連接池的使用非常easy,直接替換掉原來(lái)的ip和port號(hào)就可以。 ? ? ?有人以前測(cè)過(guò),在沒(méi)有使用連接池的情況下,訪問(wèn)memcached(使用之前的Memc模塊),rps為20000。在使用連接池之后,rps一路飆到140000。在實(shí)際情況下。這么大的提升可能達(dá)不到,可是基本上100-200%的提高還是能夠的。
?
小結(jié)
這里對(duì)memcached、redis的訪問(wèn)做個(gè)小結(jié)。
1. Nginx提供了強(qiáng)大的編程模型。location相當(dāng)于函數(shù),子請(qǐng)求相當(dāng)于函數(shù)調(diào)用,而且location還能夠向自己發(fā)送子請(qǐng)求,這樣構(gòu)成一個(gè)遞歸的模型,所以採(cǎi)用這樣的模型實(shí)現(xiàn)復(fù)雜的業(yè)務(wù)邏輯。 2. Nginx的IO操作必須是非堵塞的,假設(shè)Nginx在那阻著,則會(huì)大大減少Nginx的性能。所以在Lua中必須通過(guò)ngx.location.capture發(fā)出子請(qǐng)求將這些IO操作托付給Nginx的事件模型。
3. 在須要使用tcp連接時(shí),盡量使用連接池。
這樣能夠消除大量的建立、釋放連接的開銷。
?
參考:
?
http://wiki.nginx.org/HttpUpstreamKeepaliveModule
http://wiki.nginx.org/HttpRedis2Module
http://wiki.nginx.org/HttpMemcModule
?
原文:
http://blog.angryfox.com/?p=2063
分類:?Nginx&Openresty
總結(jié)
以上是生活随笔為你收集整理的用lua扩展你的Nginx(整理)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: shell if else 用法 syn
- 下一篇: perl中q,qq,qw,qr的区别。