首先得声明。这不是个人原创,是在网上搜索到的一篇文章,原著是谁也搞不清楚了。按风格应该是属于章亦春的文章。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
原文: