用lua扩展你的Nginx(写的非常好)
一. 概述
Nginx是一個高性能,支持高并發的,輕量級的web服務器。目前,Apache依然web服務器中的老大,但是在全球前1000大的web服務器中,Nginx的份額為22.4%。Nginx采用模塊化的架構,官方版本的Nginx中大部分功能都是通過模塊方式提供的,比如Http模塊、Mail模塊等。通過開發模塊擴展Nginx,可以將Nginx打造成一個全能的應用服務器,這樣可以將一些功能在前端Nginx反向代理層解決,比如登錄校驗、js合并、甚至數據庫訪問等等。 ? ? 但是,Nginx模塊需要用C開發,而且必須符合一系列復雜的規則,最重要的用C開發模塊必須要熟悉Nginx的源代碼,使得開發者對其望而生畏。淘寶的agentzh和chaoslawful開發的ngx_lua模塊通過將lua解釋器集成進Nginx,可以采用lua腳本實現業務邏輯,由于lua的緊湊、快速以及內建協程,所以在保證高并發服務能力的同時極大地降低了業務邏輯實現成本。 ? ? 本文向大家介紹ngx_lua,以及我在使用它開發項目的過程中遇到的一些問題。二. 準備
首先,介紹一下Nginx的一些特性,便于后文介紹ngx_lua的相關特性。
Nginx進程模型
Nginx采用多進程模型,單Master—多Worker,由Master處理外部信號、配置文件的讀取及Worker的初始化,Worker進程采用單線程、非阻塞的事件模型(Event Loop,事件循環)來實現端口的監聽及客戶端請求的處理和響應,同時Worker還要處理來自Master的信號。由于Worker使用單線程處理各種事件,所以一定要保證主循環是非阻塞的,否則會大大降低Worker的響應能力。
Nginx處理Http請求的過程
表面上看,當Nginx處理一個來自客戶端的請求時,先根據請求頭的host、ip和port來確定由哪個server處理,確定了server之后,再根據請求的uri找到對應的location,這個請求就由這個location處理。實際Nginx將一個請求的處理劃分為若干個不同階段(phase),這些階段按照前后順序依次執行,也就是說NGX_HTTP_POST_READ_PHASE在第一個,NGX_HTTP_LOG_PHASE在最后一個。[plain]?view plaincopy
每個階段上可以注冊handler,處理請求就是運行每個階段上注冊的handler。Nginx模塊提供的配置指令只會一般只會注冊并運行在其中的某一個處理階段。
比如,set指令屬于rewrite模塊的,運行在rewrite階段,deny和allow運行在access階段。
子請求(subrequest)
其實在Nginx 世界里有兩種類型的“請求”,一種叫做“主請求”(main request),而另一種則叫做“子請求”(subrequest)。 所謂“主請求”,就是由 HTTP 客戶端從 Nginx 外部發起的請求。比如,從瀏覽器訪問Nginx就是一個“主請求”。 而“子請求”則是由 Nginx 正在處理的請求在 Nginx 內部發起的一種級聯請求。“子請求”在外觀上很像 HTTP 請求,但實現上卻和 HTTP 協議乃至網絡通信一點兒關系都沒有。它是 Nginx 內部的一種抽象調用,目的是為了方便用戶把“主請求”的任務分解為多個較小粒度的“內部請求”,并發或串行地訪問多個 location 接口,然后由這些 location 接口通力協作,共同完成整個“主請求”。當然,“子請求”的概念是相對的,任何一個“子請求”也可以再發起更多的“子子請求”,甚至可以玩遞歸調用(即自己調用自己)。
當一個請求發起一個“子請求”的時候,按照 Nginx 的術語,習慣把前者稱為后者的“父請求”(parent request)。
[plain]?view plaincopy輸出:
$ curl location/main$ foo ? 03. ?bar
這里,main location就是發送2個子請求,分別到foo和bar,這就類似一種函數調用。
“子請求”方式的通信是在同一個虛擬主機內部進行的,所以 Nginx 核心在實現“子請求”的時候,就只調用了若干個 C 函數,完全不涉及任何網絡或者 UNIX 套接字(socket)通信。我們由此可以看出“子請求”的執行效率是極高的。
協程(Coroutine)
協程類似一種多線程,與多線程的區別有:?
1. 協程并非os線程,所以創建、切換開銷比線程相對要小。?
2. 協程與線程一樣有自己的棧、局部變量等,但是協程的棧是在用戶進程空間模擬的,所以創建、切換開銷很小。
3. 多線程程序是多個線程并發執行,也就是說在一瞬間有多個控制流在執行。而協程強調的是一種多個協程間協作的關系,只有當一個協程主動放棄執行權,另一個協程才能獲得執行權,所以在某一瞬間,多個協程間只有一個在運行。?
4. 由于多個協程時只有一個在運行,所以對于臨界區的訪問不需要加鎖,而多線程的情況則必須加鎖。?
5. 多線程程序由于有多個控制流,所以程序的行為不可控,而多個協程的執行是由開發者定義的所以是可控的。?
Nginx的每個Worker進程都是在epoll或kqueue這樣的事件模型之上,封裝成協程,每個請求都有一個協程進行處理。這正好與Lua內建協程的模型是一致的,所以即使ngx_lua需要執行Lua,相對C有一定的開銷,但依然能保證高并發能力。
三. ngx_lua
原理ngx_lua將Lua嵌入Nginx,可以讓Nginx執行Lua腳本,并且高并發、非阻塞的處理各種請求。Lua內建協程,這樣就可以很好的將異步回調轉換成順序調用的形式。ngx_lua在Lua中進行的IO操作都會委托給Nginx的事件模型,從而實現非阻塞調用。開發者可以采用串行的方式編寫程序,ngx_lua會自動的在進行阻塞的IO操作時中斷,保存上下文;然后將IO操作委托給Nginx事件處理機制,在IO操作完成后,ngx_lua會恢復上下文,程序繼續執行,這些操作都是對用戶程序透明的。 每個NginxWorker進程持有一個Lua解釋器或者LuaJIT實例,被這個Worker處理的所有請求共享這個實例。每個請求的Context會被Lua輕量級的協程分割,從而保證各個請求是獨立的。 ngx_lua采用“one-coroutine-per-request”的處理模型,對于每個用戶請求,ngx_lua會喚醒一個協程用于執行用戶代碼處理請求,當請求處理完成這個協程會被銷毀。每個協程都有一個獨立的全局環境(變量空間),繼承于全局共享的、只讀的“comman data”。所以,被用戶代碼注入全局空間的任何變量都不會影響其他請求的處理,并且這些變量在請求處理完成后會被釋放,這樣就保證所有的用戶代碼都運行在一個“sandbox”(沙箱),這個沙箱與請求具有相同的生命周期。 得益于Lua協程的支持,ngx_lua在處理10000個并發請求時只需要很少的內存。根據測試,ngx_lua處理每個請求只需要2KB的內存,如果使用LuaJIT則會更少。所以ngx_lua非常適合用于實現可擴展的、高并發的服務。
典型應用
官網上列出:?
· 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!
[plain]?view plaincopy
$ curl 'localhost/lua'
Hello,Lua!
這樣就實現了一個很簡單的ngx_lua應用,如果這么簡單的模塊要是用C來開發的話,代碼量估計得有100行左右,從這就可以看出ngx_lua的開發效率。
Benchmark
通過和nginx訪問靜態文件還有nodejs比較,來看一下ngx_lua提供的高并發能力。 返回的內容都是”Hello World!”,151bytes 通過.ab -n 60000 ? 取10次平均
從圖表中可以看到,在各種并發條件下ngx_lua的rps都是最高的,并且基本維持在10000rps左右,nginx讀取靜態文件因為會有磁盤io所以性能略差一些,而nodejs是相對最差的。通過這個簡單的測試,可以看出ngx_lua的高并發能力。 ngx_lua的開發者也做過一個測試對比nginx+fpm+php和nodejs,他得出的結果是ngx_lua可以達到28000rps,而nodejs有10000多一點,php則最差只有6000。可能是有些配置我沒有配好導致ngx_lua rps沒那么高。
ngx_lua安裝
ngx_lua安裝可以通過下載模塊源碼,編譯Nginx,但是推薦采用openresty。Openresty就是一個打包程序,包含大量的第三方Nginx模塊,比如HttpLuaModule,HttpRedis2Module,HttpEchoModule等。省去下載模塊,并且安裝非常方便。 ngx_openresty bundle: openresty ./configure --with-luajit&& make && make install 默認Openresty中ngx_lua模塊采用的是標準的Lua5.1解釋器,通過--with-luajit使用LuaJIT。ngx_lua的用法
ngx_lua模塊提供了配置指令和Nginx API。 配置指令:在Nginx中使用,和set指令和pass_proxy指令使用方法一樣,每個指令都有使用的context。 ? ? ? ?Nginx API:用于在Lua腳本中訪問Nginx變量,調用Nginx提供的函數。 下面舉例說明常見的指令和API。
配置指令
set_by_lua和set_by_lua_file
和set指令一樣用于設置Nginx變量并且在rewrite階段執行,只不過這個變量是由lua腳本計算并返回的。
語法:set_by_lua$res <lua-script-str> [$arg1 $arg2 ...]
配置:
[plain]?view plaincopy$ curl 'localhost/adder?a=25&b=75'
$ 100
set_by_lua_file執行Nginx外部的lua腳本,可以避免在配置文件中使用大量的轉義。
配置:
[plain]?view plaincopy
adder.lua:
[plain]?view plaincopy輸出:
$ curl 'localhost/adder?a=25&b=75
$ 100
access_by_lua和access_by_lua_file
運行在access階段,用于訪問控制。Nginx原生的allow和deny是基于ip的,通過access_by_lua能完成復雜的訪問控制,比如,訪問數據庫進行用戶名、密碼驗證等。配置:
[plain]?view plaincopy$ 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
實現url重寫,在rewrite階段執行。
配置:[plain]?view plaincopy
$ curl 'localhost/lua'
$ Hello, Lua!
content_by_lua和content_by_lua_file
Contenthandler在content階段執行,生成http響應。由于content階段只能有一個handler,所以在與echo模塊使用時,不能同時生效,我測試的結果是content_by_lua會覆蓋echo。這和之前的hello world的例子是類似的。
[plain]?view plaincopy
輸出:
$ curl 'localhost/lua'
$ Hello, Lua!
配置(在Lua中訪問Nginx變量):
[plain]?view plaincopy
輸出:
$ curl 'localhost/hello?who=world
$ Hello, world!
Nginx API
Nginx API被封裝ngx和ndk兩個package中。比如ngx.var.NGX_VAR_NAME可以訪問Nginx變量。這里著重介紹一下ngx.location.capture和ngx.location.capture_multi。
ngx.location.capture
語法:res= ngx.location.capture(uri, options?) ? ? 用于發出一個同步的,非阻塞的Nginxsubrequest(子請求)。可以通過Nginx subrequest向其它location發出非阻塞的內部請求,這些location可以是配置用于讀取文件夾的,也可以是其它的C模塊,比如ngx_proxy, ngx_fastcgi, ngx_memc, ngx_postgres, ngx_drizzle甚至是ngx_lua自己。 ? ? Subrequest只是模擬Http接口,并沒有額外的Http或者Tcp傳輸開銷,它在C層次上運行,非常高效。Subrequest不同于Http 301/302重定向,以及內部重定向(通過ngx.redirection)。
配置:
[plain]?view plaincopy
輸出:
$ curl ?'http://localhost/lua'
$ Hello, world!
實際上,location可以被外部的Http請求調用,也可以被內部的子請求調用。每個location相當于一個函數,而發送子請求就類似于函數調用,而且這種調用是非阻塞的,這就構造了一個非常強大的變成模型,后面我們會看到如何通過location和后端的memcached、redis進行非阻塞通信。
ngx.location.capture_multi
語法:res1,res2, ... = ngx.location.capture_multi({ {uri, options?}, {uri, options?}, ...}) ? ? 與ngx.location.capture功能一樣,可以并行的、非阻塞的發出多個子請求。這個方法在所有子請求處理完成后返回,并且整個方法的運行時間取決于運行時間最長的子請求,并不是所有子請求的運行時間之和。
配置:
[plain]?view plaincopy
輸出:
$ curl ?'http://localhost/lua'
$ moon,earth
注意
在Lua代碼中的網絡IO操作只能通過Nginx Lua API完成,如果通過標準Lua API會導致Nginx的事件循環被阻塞,這樣性能會急劇下降。 ? ? 在進行數據量相當小的磁盤IO時可以采用標準Lua io庫,但是當讀寫大文件時這樣是不行的,因為會阻塞整個NginxWorker進程。為了獲得更大的性能,強烈建議將所有的網絡IO和磁盤IO委托給Nginx子請求完成(通過ngx.location.capture)。 ? ? 下面通過訪問/html/index.html這個文件,來測試將磁盤IO委托給Nginx和通過Lua io直接訪問的效率。 ? ? 通過ngx.location.capture委托磁盤IO:
配置:
[plain]?view plaincopy配置:
[plain]?view plaincopy
這里通過ab去壓,在各種并發條件下,分別返回151bytes、151000bytes的數據,取10次平均,得到兩種方式的rps。 ? ? 靜態文件:151bytes
1000 3000 5000 7000 10000 ?capture ?11067 8880 8873 8952 9023 ?Lua io ? ? 11379 9724 8938 9705 9561
靜態文件:151000bytes,在10000并發下內存占用情況太嚴重,測不出結果 ? ? ? ?這種情況下,文件較小,通過Nginx訪問靜態文件需要額外的系統調用,性能略遜于ngx_lua。
1000 3000 5000 7000 ? ?10000 ?capture ? ?3338 3435 3178 3043 ? ? ? ? / ?Lua io ? ? ?3174 3094 3081 2916 ? ? ? ? /
在大文件的情況,capture就要略好于ngx_lua。 ? ? ?這里沒有對Nginx讀取靜態文件進行優化配置,只是采用了sendfile。如果優化一下,可能nginx讀取靜態文件的性能會更好一些,這個目前還不熟悉。所以,在Lua中進行各種IO時,都要通過ngx.location.capture發送子請求委托給Nginx事件模型,這樣可以保證IO是非阻塞的。
四. 小結
這篇文章簡單介紹了一下ngx_lua的基本用法,后一篇會對ngx_lua訪問redis、memcached已經連接池進行詳細介紹。五. 進階
在之前的文章中,已經介紹了ngx_lua的一些基本介紹,這篇文章主要著重討論一下如何通過ngx_lua同后端的memcached、redis進行非阻塞通信。Memcached
在Nginx中訪問Memcached需要模塊的支持,這里選用HttpMemcModule,這個模塊可以與后端的Memcached進行非阻塞的通信。我們知道官方提供了Memcached,這個模塊只支持get操作,而Memc支持大部分Memcached的命令。 Memc模塊采用入口變量作為參數進行傳遞,所有以$memc_為前綴的變量都是Memc的入口變量。memc_pass指向后端的Memcached Server。配置:
[plain]?view plaincopy
$ curl ?'http://localhost/memc?cmd=set&key=foo&val=Hello'
$ STORED
$ curl ?'http://localhost/memc?cmd=get&key=foo'
$ Hello
這就實現了memcached的訪問,下面看一下如何在lua中訪問memcached。
配置:
[plain]?view plaincopy
輸出:
$ curl ?'http://localhost/lua_memc?key=foo'
$ Hello
通過lua訪問memcached,主要是通過子請求采用一種類似函數調用的方式實現。首先,定義了一個memc location用于通過后端memcached通信,就相當于memcached storage。由于整個Memc模塊時非阻塞的,ngx.location.capture也是非阻塞的,所以整個操作非阻塞。
Redis
訪問redis需要HttpRedis2Module的支持,它也可以同redis進行非阻塞通行。不過,redis2的響應是redis的原生響應,所以在lua中使用時,需要解析這個響應。可以采用LuaRedisModule,這個模塊可以構建redis的原生請求,并解析redis的原生響應。配置:
[plain]?view plaincopy
輸出:
$ curl ?'http://localhost/lua_redis?key=foo'
$ Hello
和訪問memcached類似,需要提供一個redis storage專門用于查詢redis,然后通過子請求去調用redis。
Redis Pipeline
在實際訪問redis時,有可能需要同時查詢多個key的情況。我們可以采用ngx.location.capture_multi通過發送多個子請求給redis storage,然后在解析響應內容。但是,這會有個限制,Nginx內核規定一次可以發起的子請求的個數不能超過50個,所以在key個數多于50時,這種方案不再適用。
幸好redis提供pipeline機制,可以在一次連接中執行多個命令,這樣可以減少多次執行命令的往返時延。客戶端在通過pipeline發送多個命令后,redis順序接收這些命令并執行,然后按照順序把命令的結果輸出出去。在lua中使用pipeline需要用到redis2模塊的redis2_raw_queries進行redis的原生請求查詢。
配置:
[plain]?view plaincopypipeline.lua
[plain]?view plaincopy輸出:
$ curl ?'http://localhost/pipeline'
$ first
? second
Connection Pool
前面訪問redis和memcached的例子中,在每次處理一個請求時,都會和后端的server建立連接,然后在請求處理完之后這個連接就會被釋放。這個過程中,會有3次握手、timewait等一些開銷,這對于高并發的應用是不可容忍的。這里引入connection pool來消除這個開銷。 連接池需要HttpUpstreamKeepaliveModule模塊的支持。配置:
[plain]?view plaincopy
這個模塊提供keepalive指令,它的context是upstream。我們知道upstream在使用Nginx做反向代理時使用,實際upstream是指“上游”,這個“上游”可以是redis、memcached或是mysql等一些server。upstream可以定義一個虛擬server集群,并且這些后端的server可以享受負載均衡。keepalive 1024就是定義連接池的大小,當連接數超過這個大小后,后續的連接自動退化為短連接。連接池的使用很簡單,直接替換掉原來的ip和端口號即可。 ? ? ?有人曾經測過,在沒有使用連接池的情況下,訪問memcached(使用之前的Memc模塊),rps為20000。在使用連接池之后,rps一路飆到140000。在實際情況下,這么大的提升可能達不到,但是基本上100-200%的提高還是可以的。
小結
這里對memcached、redis的訪問做個小結。 1. Nginx提供了強大的編程模型,location相當于函數,子請求相當于函數調用,并且location還可以向自己發送子請求,這樣構成一個遞歸的模型,所以采用這種模型實現復雜的業務邏輯。 2. Nginx的IO操作必須是非阻塞的,如果Nginx在那阻著,則會大大降低Nginx的性能。所以在Lua中必須通過ngx.location.capture發出子請求將這些IO操作委托給Nginx的事件模型。 3. 在需要使用tcp連接時,盡量使用連接池。這樣可以消除大量的建立、釋放連接的開銷。
來源:http://blog.csdn.net/xu_ya_fei/article/details/41964495
總結
以上是生活随笔為你收集整理的用lua扩展你的Nginx(写的非常好)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql返回#1114 – The t
- 下一篇: nginx整合php+lua+oracl