微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

使用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,0); background-color:inherit">//7一般来说当上面的access模块得到access_code之后就会由这个模块根据access_code来进行操作  
  9. NGX_HTTP_TRY_FILES_PHASE,0); background-color:inherit">//8try_file模块,就是对应配置文件中的try_files指令,可接收多个路径作为参数,当前一个路径的资源无法找到,则自动查找下一个路径  
  10. NGX_HTTP_CONTENT_PHASE,0); background-color:inherit">//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]  ?
    location /main {  
  1.     echo_location /foo;     # echo_location发送子请求到指定的location  
  2.     echo_location /bar;  
  3. }  
  4. location /foo {  
  5.     echo foo;  
  6. }  
  7. location /bar {  
  8.     echo bar;  
  9. }  

       输出

?
    $ curl location/main  
  1. $ foo  
  2.   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,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!

        配置:

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

        输出

?
    $ curl 'localhost/lua'  
  1. 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 ...]

        配置:

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

        输出

?
    $ curl 'localhost/adder?a=25&b=75'  
  1. $ 100  

        set_by_lua_file执行Nginx外部的lua脚本,可以避免在配置文件中使用大量的转义。

        配置:
?
    location = /fib {  
  1.         set_by_lua_file $res "conf/adder.lua" $arg_n;  
  2.    
  3.         echo $res;  
  4. }  
        adder.lua:

?
    local a = tonumber(ngx.arg[1])  
  1. local b = tonumber(ngx.arg[2])  
  2. return a + b  

        输出

?
    $ curl 'localhost/adder?a=25&b=75  
  1. $ 100  

        b. access_by_lua和access_by_lua_file

        运行在access阶段,用于访问控制。Nginx原生的allow和deny是基于ip的,通过access_by_lua能完成复杂的访问控制,比如,访问数据库进行用户名、密码验证等。

        配置:

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

        输出

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

        c. rewrite_by_lua和rewrite_by_lua_file

        实现url重写,在rewrite阶段执行。

        配置:

?
    location = /foo {  
  1.         rewrite_by_lua 'ngx.exec("/bar")';  
  2.     echo 'in foo';  
  3. location = /bar {  
  4.         echo 'in bar';  
  5. }  

        输出

?
    $ Hello, Lua!  
         d. content_by_lua和content_by_lua_file

        Contenthandler在content阶段执行,生成http响应。由于content阶段只能有一个handler,所以在与echo模块使用时,不能同时生效,我测试的结果是content_by_lua会覆盖echo。这和之前的hello world的例子是类似的。

        配置(直接响应):

?
    location = /lua {  
  1.         content_by_lua 'ngx.say("Hello, Lua!")';  
  2. }  
        输出

?
            配置(在Lua中访问Nginx变量):

    ?
      location = /hello {  
    1.         content_by_lua 'local who = ngx.var.arg_who  
    2.         ngx.say("Hello, ", who, "!")';  
    3. }  
            输出

    ?
      $ curl 'localhost/hello?who=world  
    1. 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 ?
        location = /other {  
      1.     ehco 'Hello, world!';  
      2. }  
      3.       
      4. # Lua非阻塞IO  
      5. location = /lua {  
      6.     content_by_lua '  
      7.         local res = ngx.location.capture("/other")  
      8.         if res.status == 200 then  
      9.             ngx.print(res.body)  
      10.         end  
      11.     ';  
      12. }  
              输出

      ?
        $ curl  'http://localhost/lua'  
      1. 调用,也可以被内部的子请求调用。每个location相当于一个函数,而发送子请求就类似于函数调用,而且这种调用是非阻塞的,这就构造了一个非常强大的变成模型,后面我们会看到如何通过location和后端的memcached、redis进行非阻塞通信。

                b. ngx.location.capture_multi

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

                与ngx.location.capture功能一样,可以并行的、非阻塞的发出多个子请求。这个方法在所有子请求处理完成后返回,并且整个方法的运行时间取决于运行时间最长的子请求,并不是所有子请求的运行时间之和。

                配置:

        ?
          # 同时发送多个子请求(subrequest)  
        1. location = /moon {  
        2.     ehco 'moon';  
        3. }  
        4. location = /earth {  
        5.     ehco 'earth';  
        6.        
        7. location = /lua {  
        8.     content_by_lua '  
        9.         local res1,res2 = ngx.location.capture_multi({ {"/moon"}, {"earth"} })  
        10.         if res1.status == 200 then  
        11.             ngx.print(res1.body)  
        12.         end  
        13.         ngx.print(",")  
        14.         if res2.status == 200 then  
        15.             ngx.print(res2.body)  
        16.     ';  
        17. }  

                输出

        ?
          $ 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:

                配置:

        ?
          location / {   
        1.     internal;  
        2.     root html;  
        3. location /capture {  
        4.     content_by_lua '  
        5.         res = ngx.location.capture("/")  
        6.         echo res.body  
        7. }  

                通过标准lua io访问磁盘文件

                配置:

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

                这里通过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已经连接池进行详细介绍。

        版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

        相关推荐