一. 概述php
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,以及我在使用它开发项目的过程当中遇到的一些问题。
html
二. 准备前端
首先,介绍一下Nginx的一些特性,便于后文介绍ngx_lua的相关特性。node
Nginx进程模型mysql
Nginx采用多进程模型,单Master—多Worker,由Master处理外部信号、配置文件的读取及Worker的初始化,Worker进程采用单线程、非阻塞的事件模型(Event Loop,事件循环)来实现端口的监听及客户端请求的处理和响应,同时Worker还要处理来自Master的信号。因为Worker使用单线程处理各类事件,因此必定要保证主循环是非阻塞的,不然会大大下降Worker的响应能力。nginx
Nginx处理Http请求的过程web
表面上看,当Nginx处理一个来自客户端的请求时,先根据请求头的host、ip和port来肯定由哪一个server处理,肯定了server以后,再根据请求的uri找到对应的location,这个请求就由这个location处理。实际Nginx将一个请求的处理划分为若干个不一样阶段(phase),这些阶段按照先后顺序依次执行,也就是说NGX_HTTP_POST_READ_PHASE在第一个,NGX_HTTP_LOG_PHASE在最后一个。redis
每一个阶段上能够注册handler,处理请求就是运行每一个阶段上注册的handler。Nginx模块提供的配置指令只会通常只会注册并运行在其中的某一个处理阶段。sql
好比,set指令属于rewrite模块的,运行在rewrite阶段,deny和allow运行在access阶段。数据库
子请求(subrequest)
其实在Nginx 世界里有两种类型的“请求”,一种叫作“主请求”(main request),而另外一种则叫作“子请求”(subrequest)。 所谓“主请求”,就是由 HTTP 客户端从 Nginx 外部发起的请求。好比,从浏览器访问Nginx就是一个“主请求”。 而“子请求”则是由 Nginx 正在处理的请求在 Nginx 内部发起的一种级联请求。“子请求”在外观上很像 HTTP 请求,但实现上却和 HTTP 协议乃至网络通讯一点儿关系都没有。它是 Nginx 内部的一种抽象调用,目的是为了方便用户把“主请求”的任务分解为多个较小粒度的“内部请求”,并发或串行地访问多个 location 接口,而后由这些 location 接口通力协做,共同完成整个“主请求”。固然,“子请求”的概念是相对的,任何一个“子请求”也能够再发起更多的“子子请求”,甚至能够玩递归调用(即本身调用本身)。
当一个请求发起一个“子请求”的时候,按照 Nginx 的术语,习惯把前者称为后者的“父请求”(parent request)。
输出:
$ 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!
输出:
$ 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 ...]
配置:
输出:
$ curl 'localhost/adder?a=25&b=75'
$ 100
set_by_lua_file执行Nginx外部的lua脚本,能够避免在配置文件中使用大量的转义。
配置:
adder.lua:
输出:
$ curl 'localhost/adder?a=25&b=75
$ 100
access_by_lua和access_by_lua_file
运行在access阶段,用于访问控制。Nginx原生的allow和deny是基于ip的,经过access_by_lua能完成复杂的访问控制,好比,访问数据库进行用户名、密码验证等。
配置:
输出:
$ 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阶段执行。
配置:
输出:
$ curl 'localhost/lua'
$ Hello, Lua!
content_by_lua和content_by_lua_file
Contenthandler在content阶段执行,生成http响应。因为content阶段只能有一个handler,因此在与echo模块使用时,不能同时生效,我测试的结果是content_by_lua会覆盖echo。这和以前的hello world的例子是相似的。
配置(直接响应):
输出:
$ curl 'localhost/lua'
$ Hello, Lua!
配置(在Lua中访问Nginx变量):
输出:
$ 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)。
配置:
输出:
$ 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功能同样,能够并行的、非阻塞的发出多个子请求。这个方法在全部子请求处理完成后返回,而且整个方法的运行时间取决于运行时间最长的子请求,并非全部子请求的运行时间之和。
配置:
输出:
$ 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:
配置:
经过标准lua io访问磁盘文件:
配置:
这里经过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。
配置:
输出:
$ curl 'http://localhost/memc?cmd=set&key=foo&val=Hello'
$ STORED
$ curl 'http://localhost/memc?cmd=get&key=foo'
$ Hello
这就实现了memcached的访问,下面看一下如何在lua中访问memcached。
配置:
输出:
$ curl 'http://localhost/lua_memc?key=foo'
$ Hello
经过lua访问memcached,主要是经过子请求采用一种相似函数调用的方式实现。首先,定义了一个memc location用于经过后端memcached通讯,就至关于memcached storage。因为整个Memc模块时非阻塞的,ngx.location.capture也是非阻塞的,因此整个操做非阻塞。
访问redis须要HttpRedis2Module的支持,它也能够同redis进行非阻塞通行。不过,redis2的响应是redis的原生响应,因此在lua中使用时,须要解析这个响应。能够采用LuaRedisModule,这个模块能够构建redis的原生请求,并解析redis的原生响应。
配置:
输出:
$ 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的原生请求查询。
配置:
pipeline.lua
输出:
$ curl 'http://localhost/pipeline'
$ first
second
Connection Pool
前面访问redis和memcached的例子中,在每次处理一个请求时,都会和后端的server创建链接,而后在请求处理完以后这个链接就会被释放。这个过程当中,会有3次握手、timewait等一些开销,这对于高并发的应用是不可容忍的。这里引入connection pool来消除这个开销。 链接池须要HttpUpstreamKeepaliveModule模块的支持。
配置:
这个模块提供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://wiki.nginx.org/HttpUpstreamKeepaliveModule
http://wiki.nginx.org/HttpRedis2Module
http://wiki.nginx.org/HttpMemcModule
原文: