爲什麼要使用ngx_lua

一. 概述

        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的相關特性。

1. Nginx進程模型

        Nginx採用多進程模型,單Master—多Worker,由Master處理外部信號、配置文件的讀取及Worker的初始化,Worker進程採用 單線程、非阻塞的事件模型(Event Loop,事件循環)來實現端口的監聽及客戶端請求的處理和響應,同時Worker還要處理來自Master的信號。由於Worker使用單線程處理各種 事件,所以一定要保證主循環是非阻塞的,否則會大大降低Worker的響應能力。

圖1

 

2. Nginx處理Http請求的過程

        表面上看,當Nginx處理一個來自客戶端的請求時,先根據請求頭的host、ip和port來確定由哪個server處理,確定了server之後,再 根據請求的uri找到對應的location,這個請求就由這個location處理。實際Nginx將一個請求的處理劃分爲若干個不同階段 (phase),這些階段按照前後順序依次執行,也就是說NGX_HTTP_POST_READ_PHASE在第一 個,NGX_HTTP_LOG_PHASE在最後一個。

  1. NGX_HTTP_POST_READ_PHASE,      //0讀取請求phase         
  2. NGX_HTTP_SERVER_REWRITE_PHASE,//1這個階段主要是處理全局的(server block)的rewrite   
  3. NGX_HTTP_FIND_CONFIG_PHASE,   //2這個階段主要是通過uri來查找對應的location,然後根據loc_conf設置r的相應變量     
  4. NGX_HTTP_REWRITE_PHASE,       //3這個主要處理location的rewrite   
  5. NGX_HTTP_POST_REWRITE_PHASE,  //4postrewrite,這個主要是進行一些校驗以及收尾工作,以便於交給後面的模塊。   
  6. NGX_HTTP_PREACCESS_PHASE,     //5比如流控這種類型的access就放在這個phase,也就是說它主要是進行一些比較粗粒度的access。   
  7. NGX_HTTP_ACCESS_PHASE,        //6這個比如存取控制,權限驗證就放在這個phase,一般來說處理動作是交給下面的模塊做的.這個主要是做一些細粒度的access      
  8. NGX_HTTP_POST_ACCESS_PHASE,   //7一般來說當上面的access模塊得到access_code之後就會由這個模塊根據access_code來進行操作   
  9. NGX_HTTP_TRY_FILES_PHASE,     //8try_file模塊,就是對應配置文件中的try_files指令,可接收多個路徑作爲參數,當前一個路徑的資源無法找到,則自動查找下一個路徑   
  10. NGX_HTTP_CONTENT_PHASE,       //9內容處理模塊   
  11. NGX_HTTP_LOG_PHASE            //10log模塊     

        每個階段上可以註冊handler,處理請求就是運行每個階段上註冊的handler。Nginx模塊提供的配置指令只會一般只會註冊並運行在其中的某一 個處理階段。比如,set指令屬於rewrite模塊的,運行在rewrite階段,deny和allow運行在access階段。

3. 子請求(subrequest)

        其實在Nginx 世界裏有兩種類型的“請求”,一種叫做“主請求”(main request),而另一種則叫做“子請求”(subrequest)。

        所謂“主請求”,就是由 HTTP 客戶端從 Nginx 外部發起的請求。比如,從瀏覽器訪問Nginx就是一個“主請求”。

        而“子請求”則是由 Nginx 正在處理的請求在 Nginx 內部發起的一種級聯請求。“子請求”在外觀上很像 HTTP 請求,但實現上卻和 HTTP 協議乃至網絡通信一點兒關係都沒有。它是 Nginx 內部的一種抽象調用,目的是爲了方便用戶把“主請求”的任務分解爲多個較小粒度的“內部請求”,併發或串行地訪問多個 location 接口,然後由這些 location 接口通力協作,共同完成整個“主請求”。當然,“子請求”的概念是相對的,任何一個“子請求”也可以再發起更多的“子子請求”,甚至可以玩遞歸調用(即自 己調用自己)。當一個請求發起一個“子請求”的時候,按照 Nginx 的術語,習慣把前者稱爲後者的“父請求”(parent request)。 

[plain] view plain copy print ?
  1. location /main {  
  2.     echo_location /foo;     # echo_location發送子請求到指定的location  
  3.     echo_location /bar;  
  4. }  
  5. location /foo {  
  6.     echo foo;  
  7. }  
  8. location /bar {  
  9.     echo bar;  
  10. }  

       輸出:

 

[plain] view plain copy print ?
  1. $ curl location/main  
  2. $ foo  
  3.   bar  

這裏,main location就是發送2個子請求,分別到foo和bar,這就類似一種函數調用。

        “子請求”方式的通信是在同一個虛擬主機內部進行的,所以 Nginx 核心在實現“子請求”的時候,就只調用了若干個 C 函數,完全不涉及任何網絡或者 UNIX 套接字(socket)通信。我們由此可以看出“子請求”的執行效率是極高的。

4. 協程(Coroutine)

         協程類似一種多線程,與多線程的區別有:
        1. 協程並非os線程,所以創建、切換開銷比線程相對要小。
        2. 協程與線程一樣有自己的棧、局部變量等,但是協程的棧是在用戶進程空間模擬的,所以創建、切換開銷很小。
        3. 多線程程序是多個線程併發執行,也就是說在一瞬間有多個控制流在執行。而協程強調的是一種多個協程間協作的關係,只有當一個協程主動放棄執行權,另一個協程才能獲得執行權,所以在某一瞬間,多個協程間只有一個在運行。
        4. 由於多個協程時只有一個在運行,所以對於臨界區的訪問不需要加鎖,而多線程的情況則必須加鎖。
        5. 多線程程序由於有多個控制流,所以程序的行爲不可控,而多個協程的執行是由開發者定義的所以是可控的。
        Nginx的每個Worker進程都是在epoll或kqueue這樣的事件模型之上,封裝成協程,每個請求都有一個協程進行處理。這正好與Lua內建協 程的模型是一致的,所以即使ngx_lua需要執行Lua,相對C有一定的開銷,但依然能保證高併發能力。

三. ngx_lua

1. 原理

        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非常適合用於實現可擴展的、高併發的服務。

2. 典型應用

        官網上列出:

·  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.

3. Hello Lua!

        配置:

[plain] view plain copy print ?
  1. # nginx.conf      
  2. worker_processes 4;   
  3.   
  4. events {  
  5.      worker_connections 1024;   
  6. }  
  7. http {  
  8.   
  9.     server {  
  10.         listen 80;   
  11.         server_name localhost;  
  12.           
  13.         location = /lua {  
  14.             content_by_lua ‘   
  15.                 ngx.say("Hello, Lua!")  
  16.             ';  
  17.         }  
  18.     }  
  19. }  

        輸出:

[plain] view plain copy print ?
  1. $ curl 'localhost/lua'  
  2. Hello,Lua!  

        這樣就實現了一個很簡單的ngx_lua應用,如果這麼簡單的模塊要是用C來開發的話,代碼量估計得有100行左右,從這就可以看出ngx_lua的開發效率。

4. Benchmark

        通過和nginx訪問靜態文件還有nodejs比較,來看一下ngx_lua提供的高併發能力。

        返回的內容都是”Hello World!”,151bytes

        通過.ab -n 60000   取10次平均

 

  1000 3000 5000 7000 10000
nginx 靜態文件 11351 9653 8929 8997 9722
nodejs 10846 9510 8898 8387 7820
ngx_lua 13839 10174 9523 10309 10711

        從圖表中可以看到,在各種併發條件下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沒那麼高。

5. 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。

 

6. ngx_lua的用法

        ngx_lua模塊提供了配置指令和Nginx API。

        配置指令:在Nginx中使用,和set指令和pass_proxy指令使用方法一樣,每個指令都有使用的context。      

        Nginx API:用於在Lua腳本中訪問Nginx變量,調用Nginx提供的函數。

        下面舉例說明常見的指令和API。

7. 配置指令

        a. set_by_lua和set_by_lua_file

        和set指令一樣用於設置Nginx變量並且在rewrite階段執行,只不過這個變量是由lua腳本計算並返回的。

        語法:set_by_lua$res <lua-script-str> [$arg1 $arg2 ...]

        配置:

[plain] view plain copy print ?
  1. location = /adder {  
  2.     set_by_lua $res "  
  3.             local a = tonumber(ngx.arg[1])  
  4.                 local b = tonumber(ngx.arg[2])  
  5.                 return a + b" $arg_a $arg_b;  
  6.    
  7.         echo $res;  
  8. }  

        輸出:

[plain] view plain copy print ?
  1. $ curl 'localhost/adder?a=25&b=75'  
  2. $ 100  

        set_by_lua_file執行Nginx外部的lua腳本,可以避免在配置文件中使用大量的轉義。

        配置:

[plain] view plain copy print ?
  1. location = /fib {  
  2.         set_by_lua_file $res "conf/adder.lua" $arg_n;  
  3.    
  4.         echo $res;  
  5. }  

        adder.lua:

[plain] view plain copy print ?
  1. local a = tonumber(ngx.arg[1])  
  2. local b = tonumber(ngx.arg[2])  
  3. return a + b  

        輸出:

[plain] view plain copy print ?
  1. $ curl 'localhost/adder?a=25&b=75  
  2. $ 100  

        b. access_by_lua和access_by_lua_file

        運行在access階段,用於訪問控制。Nginx原生的allow和deny是基於ip的,通過access_by_lua能完成複雜的訪問控制,比如,訪問數據庫進行用戶名、密碼驗證等。

        配置:

[plain] view plain copy print ?
  1. location /auth {  
  2.     access_by_lua '  
  3.         if ngx.var.arg_user == "ntes" then  
  4.             return  
  5.         else   
  6.             Ngx.exit(ngx.HTTP_FORBIDDEN)  
  7.         end  
  8.     ';  
  9.     echo 'welcome ntes';  
  10. }  

        輸出:

[plain] view plain copy print ?
  1. $ curl 'localhost/auth?user=sohu'  
  2. $ Welcome ntes  
  3.   
  4.   
  5. $ curl 'localhost/auth?user=ntes'  
  6. $ <html>  
  7. <head><title>403 Forbidden</title></heda>  
  8. <body bgcolor="white">  
  9. <center><h1>403 Forbidden</h1></center>  
  10. <hr><center>ngx_openresty/1.0.10.48</center>  
  11. </body>  
  12. </html>  

        c. rewrite_by_lua和rewrite_by_lua_file

        實現url重寫,在rewrite階段執行。

        配置:

[plain] view plain copy print ?
  1. location = /foo {  
  2.         rewrite_by_lua 'ngx.exec("/bar")';  
  3.     echo 'in foo';  
  4. }  
  5.   
  6. location = /bar {  
  7.         echo 'in bar';  
  8. }  

        輸出:

[plain] view plain copy print ?
  1. $ curl 'localhost/lua'  
  2. $ Hello, Lua!  

        d. content_by_lua和content_by_lua_file

        Contenthandler在content階段執行,生成http響應。由於content階段只能有一個handler,所以在與echo模塊使用 時,不能同時生效,我測試的結果是content_by_lua會覆蓋echo。這和之前的hello world的例子是類似的。

        配置(直接響應):

[plain] view plain copy print ?
  1. location = /lua {  
  2.         content_by_lua 'ngx.say("Hello, Lua!")';  
  3. }  

        輸出:

[plain] view plain copy print ?
  1. $ curl 'localhost/lua'  
  2. $ Hello, Lua!  

        配置(在Lua中訪問Nginx變量):

[plain] view plain copy print ?
  1. location = /hello {  
  2.         content_by_lua 'local who = ngx.var.arg_who  
  3.         ngx.say("Hello, ", who, "!")';  
  4. }  

        輸出:

[plain] view plain copy print ?
  1. $ curl 'localhost/hello?who=world  
  2. $ Hello, world!  

8. Nginx API

        Nginx API被封裝ngx和ndk兩個package中。比如ngx.var.NGX_VAR_NAME可以訪問Nginx變量。這裏着重介紹一下ngx.location.capture和ngx.location.capture_multi。

        a. 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 plain copy print ?
  1. location = /other {  
  2.     ehco 'Hello, world!';  
  3. }  
  4.       
  5. # Lua非阻塞IO  
  6. location = /lua {  
  7.     content_by_lua '  
  8.         local res = ngx.location.capture("/other")  
  9.         if res.status == 200 then  
  10.             ngx.print(res.body)  
  11.         end  
  12.     ';  
  13. }  
        輸出:
[plain] view plain copy print ?
  1. $ curl  'http://localhost/lua'  
  2. $ Hello, world!  
        實際上,location可以被外部的Http請求調用,也可以被內部的子請求調用。每個location相當於一個函數,而發送子請求就類似於函數調 用,而且這種調用是非阻塞的,這就構造了一個非常強大的變成模型,後面我們會看到如何通過location和後端的memcached、redis進行非 阻塞通信。

        b. ngx.location.capture_multi

        語法:res1,res2, ... = ngx.location.capture_multi({ {uri, options?}, {uri, options?}, ...})

        與ngx.location.capture功能一樣,可以並行的、非阻塞的發出多個子請求。這個方法在所有子請求處理完成後返回,並且整個方法的運行時間取決於運行時間最長的子請求,並不是所有子請求的運行時間之和。

        配置:

[plain] view plain copy print ?
  1. # 同時發送多個子請求(subrequest)  
  2. location = /moon {  
  3.     ehco 'moon';  
  4. }  
  5. location = /earth {  
  6.     ehco 'earth';  
  7. }  
  8.        
  9. location = /lua {  
  10.     content_by_lua '  
  11.         local res1,res2 = ngx.location.capture_multi({ {"/moon"}, {"earth"} })  
  12.         if res1.status == 200 then  
  13.             ngx.print(res1.body)  
  14.         end  
  15.         ngx.print(",")  
  16.         if res2.status == 200 then  
  17.             ngx.print(res2.body)  
  18.         end  
  19.     ';  
  20. }  

        輸出:

[plain] view plain copy print ?
  1. $ curl  'http://localhost/lua'  
  2. $ moon,earth  
        c. 注意

        在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 plain copy print ?
  1. location / {   
  2.     internal;  
  3.     root html;  
  4. }  
  5.   
  6. location /capture {  
  7.     content_by_lua '  
  8.         res = ngx.location.capture("/")  
  9.         echo res.body  
  10.     ';  
  11. }  

        通過標準lua io訪問磁盤文件:

        配置:

[plain] view plain copy print ?
  1. location /luaio{   
  2.     content_by_lua '      
  3.         local io = require("io")  
  4.         local chunk_SIZE = 4096  
  5.         local f = assert(io.open("html/index.html","r"))  
  6.         while true do  
  7.             local chunk = f:read(chunk)  
  8.             if not chunk then  
  9.                 break  
  10.             end  
  11.             ngx.print(chunk)  
  12.             ngx.flush(true)  
  13.         end  
  14.         f:close()  
  15.     ';  
  16. }  

        這裏通過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是非阻 塞的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章