首先得聲明。這不是個人原創,是在網上搜索到的一篇文章,原著是誰也搞不清楚了。按風格應該是屬於章亦春的文章。php
整理花了很多時間,因此就暫寫成原創吧。html
一. 概述前端
Nginx是一個高性能。支持高併發的,輕量級的webserver。眼下,Apache依舊webserver中的老大,但是在全球前1000大的webserver中,Nginx的份額爲22.4%。Nginx採用模塊化的架構,官方版本號的Nginx中大部分功能都是經過模塊方式提供的,比方Http模塊、Mail模塊等。經過開發模塊擴展Nginx,可以將Nginx打形成一個全能的應用server,這樣可以將一些功能在前端Nginx反向代理層解決,比方登陸校驗、js合併、甚至數據庫訪問等等。 但是,Nginx模塊須要用C開發,而且必須符合一系列複雜的規則。最重要的用C開發模塊必須要熟悉Nginx的源碼。使得開發人員對其望而生畏。淘寶的agentzh和chaoslawful開發的ngx_lua模塊經過將lua解釋器集成進Nginx。可以採用lua腳本實現業務邏輯,由於lua的緊湊、高速以及內建協程,因此在保證高併發服務能力的同一時候極大地減小了業務邏輯實現成本。 本文向你們介紹ngx_lua,以及我在使用它開發項目的過程當中遇到的一些問題。
node
二. 準備mysql
首先,介紹一下Nginx的一些特性,便於後文介紹ngx_lua的相關特性。nginx
Nginx進程模型web
Nginx採用多進程模型,單Master—多Worker,由Master處理外部信號、配置文件的讀取及Worker的初始化。Worker進程採用單線程、非堵塞的事件模型(Event Loop,事件循環)來實現port的監聽及client請求的處理和響應,同一時候Worker還要處理來自Master的信號。redis
由於Worker使用單線程處理各類事件。因此必定要保證主循環是非堵塞的,不然會大大減小Worker的響應能力。sql
Nginx處理Http請求的過程數據庫
表面上看,當Nginx處理一個來自client的請求時,先依據請求頭的host、ip和port來肯定由哪一個server處理,肯定了server以後,再依據請求的uri找到相應的location。這個請求就由這個location處理。實際Nginx將一個請求的處理劃分爲若干個不一樣階段(phase)。這些階段依照先後順序依次運行。也就是說NGX_HTTP_POST_READ_PHASE在第一個,NGX_HTTP_LOG_PHASE在最後一個。
<span style="font-size:10px;">NGX_HTTP_POST_READ_PHASE, //0讀取請求phase NGX_HTTP_SERVER_REWRITE_PHASE,//1這個階段主要是處理全局的(server block)的rewrite NGX_HTTP_FIND_CONFIG_PHASE, //2這個階段主要是經過uri來查找相應的location,而後依據loc_conf設置r的相應變量 NGX_HTTP_REWRITE_PHASE, //3這個主要處理location的rewrite NGX_HTTP_POST_REWRITE_PHASE, //4postrewrite,這個主要是進行一些校驗以及收尾工做。以便於交給後面的模塊。NGX_HTTP_PREACCESS_PHASE, //5比方流控這樣的類型的access就放在這個phase,也就是說它主要是進行一些比較粗粒度的access。
NGX_HTTP_ACCESS_PHASE, //6這個比方存取控制,權限驗證就放在這個phase,通常來講處理動做是交給如下的模塊作的.這個主要是作一些細粒度的access NGX_HTTP_POST_ACCESS_PHASE, //7通常來講當上面的access模塊獲得access_code以後就會由這個模塊依據access_code來進行操做 NGX_HTTP_TRY_FILES_PHASE, //8try_file模塊,就是相應配置文件裏的try_files指令。可接收多個路徑做爲參數。當前一個路徑的資源沒法找到,則本身主動查找下一個路徑 NGX_HTTP_CONTENT_PHASE, //9內容處理模塊 NGX_HTTP_LOG_PHASE //10log模塊
每個階段上可以註冊handler。處理請求就是執行每個階段上註冊的handler。Nginx模塊提供的配置指令僅僅會通常僅僅會註冊並執行在當中的某一個處理階段。
比方,set指令屬於rewrite模塊的,執行在rewrite階段,deny和allow執行在access階段。
事實上在Nginx 世界裏有兩種類型的「請求」。一種叫作「主請求」(main request),而還有一種則叫作「子請求」(subrequest)。 所謂「主請求」。就是由 HTTP client從 Nginx 外部發起的請求。比方。從瀏覽器訪問Nginx就是一個「主請求」。 而「子請求」則是由 Nginx 正在處理的請求在 Nginx 內部發起的一種級聯請求。「子請求」在外觀上很是像 HTTP 請求,但實現上卻和 HTTP 協議乃至網絡通訊一點兒關係都沒有。它是 Nginx 內部的一種抽象調用,目的是爲了方便用戶把「主請求」的任務分解爲多個較小粒度的「內部請求」,併發或串行地訪問多個 location 接口。而後由這些 location 接口通力協做,共同完畢整個「主請求」。固然。「子請求」的概念是相對的,不論什麼一個「子請求」也可以再發起不少其它的「子子請求」。甚至可以玩遞歸調用(即本身調用本身)。
當一個請求發起一個「子請求」的時候,依照 Nginx 的術語,習慣把前者稱爲後者的「父請求」(parent request)。
location /main { echo_location /foo; # echo_location發送子請求到指定的location echo_location /bar; } location /foo { echo foo; } location /bar { echo bar; }
輸出:
$ curl location/main協程相似一種多線程,與多線程的差異有:
1. 協程並非os線程,因此建立、切換開銷比線程相對要小。
2. 協程與線程同樣有本身的棧、局部變量等,但是協程的棧是在用戶進程空間模擬的,因此建立、切換開銷很是小。
3. 多線程程序是多個線程併發運行。也就是說在一瞬間有多個控制流在運行。而協程強調的是一種多個協程間協做的關係,僅僅有當一個協程主動放棄運行權,還有一個協程才幹得到運行權,因此在某一瞬間,多個協程間僅僅有一個在運行。
4. 由於多個協程時僅僅有一個在執行,因此對於臨界區的訪問不需要加鎖。而多線程的狀況則必須加鎖。
5. 多線程程序由於有多個控制流。因此程序的行爲不可控,而多個協程的運行是由開發人員定義的因此是可控的。
Nginx的每個Worker進程都是在epoll或kqueue這種事件模型之上,封裝成協程,每個請求都有一個協程進行處理。這正好與Lua內建協程的模型是一致的,因此即便ngx_lua需要運行Lua,相對C有必定的開銷,但依舊能保證高併發能力。
三. 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!
# 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!") '; } } }輸出:
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 ...]
配置:
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; }輸出:
配置:
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
access_by_lua和access_by_lua_file
執行在access階段。用於訪問控制。Nginx原生的allow和deny是基於ip的。經過access_by_lua能完畢複雜的訪問控制。比方。訪問數據庫進行username、password驗證等。
配置:
location /auth { access_by_lua ' if ngx.var.arg_user == "ntes" then return else Ngx.exit(ngx.HTTP_FORBIDDEN) end '; echo'welcome ntes'; }輸出:
rewrite_by_lua和rewrite_by_lua_file
實現url重寫。在rewrite階段運行。
配置:location =/foo { rewrite_by_lua 'ngx.exec("/bar")'; echo'in foo'; } location =/bar { echo'in bar'; }輸出:
Contenthandler在content階段運行,生成http響應。由於content階段僅僅能有一個handler。因此在與echo模塊使用時,不能同一時候生效,我測試的結果是content_by_lua會覆蓋echo。這和以前的hello world的樣例是相似的。
location =/lua { content_by_lua 'ngx.say("Hello, Lua!")'; }
location =/hello { content_by_lua ' local who = ngx.var.arg_who ngx.say("Hello, ", who, "!") '; }
比方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)。
配置:
location =/other { ehco 'Hello, world!'; } # Lua非堵塞IO location =/lua { content_by_lua ' local res = ngx.location.capture("/other") if res.status == 200 then ngx.print(res.body) end '; }
# 同一時候發送多個子請求(subrequest) location =/moon { ehco 'moon'; } location =/earth { ehco 'earth'; } location =/lua { content_by_lua ' local res1,res2 = ngx.location.capture_multi({ {"/moon"}, {"earth"} }) if res1.status == 200 then ngx.print(res1.body) end ngx.print(",") if res2.status == 200 then ngx.print(res2.body) end '; }
配置:
location / { internal; root html; } location /capture { content_by_lua ' res = ngx.location.capture("/") echo res.body '; }經過標準lua io訪問磁盤文件:
location /luaio{ content_by_lua ' local io = require("io") local chunk_SIZE = 4096 local f = assert(io.open("html/index.html","r")) while true do local chunk = f:read(chunk) if not chunk then break end ngx.print(chunk) ngx.flush(true) end f:close() '; }
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。
配置:
#使用HttpMemcModule location =/memc { set $memc_cmd $arg_cmd; set $memc_key $arg_key; set $memc_value $arg_val; set $memc_exptime $arg_exptime; memc_pass '127.0.0.1:11211'; }輸出:
cmd=get&key=foo'
$ Hello
這就實現了memcached的訪問。如下看一下怎樣在lua中訪問memcached。
配置:
#在Lua中訪問Memcached location =/memc { internal; #僅僅能內部訪問 set $memc_cmd get; set $memc_key $arg_key; memc_pass '127.0.0.1:11211'; } location =/lua_memc { content_by_lua ' local res = ngx.location.capture("/memc", { args = { key = ngx.var.arg_key } }) if res.status == 200 then ngx.say(res.body) end '; }
key=foo'
$ Hello
經過lua訪問memcached。主要是經過子請求採用一種相似函數調用的方式實現。
首先。定義了一個memc location用於經過後端memcached通訊,就至關於memcached storage。
由於整個Memc模塊時非堵塞的。ngx.location.capture也是非堵塞的,因此整個操做非堵塞。
Redis
訪問redis需要HttpRedis2Module的支持,它也可以同redis進行非堵塞通行。只是,redis2的響應是redis的原生響應,因此在lua中使用時,需要解析這個響應。可以採用LuaRedisModule,這個模塊可以構建redis的原生請求。並解析redis的原生響應。#在Lua中訪問Redis location =/redis { internal; #僅僅能內部訪問 redis2_query get $arg_key; redis2_pass '127.0.0.1:6379'; } location =/lua_redis {#需要LuaRedisParser content_by_lua ' local parser = require("redis.parser") local res = ngx.location.capture("/redis", { args = { key = ngx.var.arg_key } }) if res.status == 200 then reply = parser.parse_reply(res.body) ngx.say(reply) end '; }
咱們可以採用ngx.location.capture_multi經過發送多個子請求給redis storage,而後在解析響應內容。
但是,這會有個限制,Nginx內核規定一次可以發起的子請求的個數不能超過50個。因此在key個數多於50時,這樣的方案再也不適用。
幸虧redis提供pipeline機制。可以在一次鏈接中運行多個命令,這樣可以下降屢次運行命令的往返時延。
client在經過pipeline發送多個命令後。redis順序接收這些命令並運行,而後依照順序把命令的結果輸出出去。在lua中使用pipeline需要用到redis2模塊的redis2_raw_queries進行redis的原生請求查詢。
配置:
#在Lua中訪問Redis location =/redis { internal; #僅僅能內部訪問 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’} } -- 構造原生的redis查詢。get one\r\nget two\r\n local raw_reqs={} for i, req in ipairs(reqs)do table.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的原生響應 local replies=parser.parse_replies(res.body, #reqs) for i, reply in ipairs(replies)do ngx.say(reply[1]) end end
Connection Pool
前面訪問redis和memcached的樣例中。在每次處理一個請求時。都會和後端的server創建鏈接。而後在請求處理完以後這個鏈接就會被釋放。這個過程當中,會有3次握手、timewait等一些開銷。這對於高併發的應用是不可容忍的。這裏引入connection pool來消除這個開銷。 鏈接池需要HttpUpstreamKeepaliveModule模塊的支持。
配置:
http { # 需要HttpUpstreamKeepaliveModule upstream redis_pool { server 127.0.0.1:6379; # 可以容納1024個鏈接的鏈接池 keepalive 1024 single; } server { location=/redis { … redis2_pass redis_pool; } } }
小結
這裏對memcached、redis的訪問作個小結。
1. Nginx提供了強大的編程模型。location至關於函數,子請求至關於函數調用,並且location還可以向本身發送子請求,這樣構成一個遞歸的模型,因此採用這樣的模型實現複雜的業務邏輯。 2. Nginx的IO操做必須是非堵塞的,假設Nginx在那阻着,則會大大減小Nginx的性能。因此在Lua中必須經過ngx.location.capture發出子請求將這些IO操做託付給Nginx的事件模型。
3. 在需要使用tcp鏈接時,儘可能使用鏈接池。
這樣可以消除大量的創建、釋放鏈接的開銷。
參考:
http://wiki.nginx.org/HttpUpstreamKeepaliveModule
http://wiki.nginx.org/HttpRedis2Module
http://wiki.nginx.org/HttpMemcModule
原文: