首先得声明,这不是我的原创,是在网上搜索到的一篇文章,原著是谁也搞不清楚了,按风格应该是属于章亦春的文章。
整理花了不少时间,所以就暂写成原创吧。
一. 概述
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的相关特性。
Nginx进程模型
Nginx采用多进程模型,单Master—多Worker,由Master处理外部信号、配置文件的读取及Worker的初始化,Worker进程采用单线程、非阻塞的事件模型(Event Loop,事件循环)来实现端口的监听及客户端请求的处理和响应,同时Worker还要处理来自Master的信号。由于Worker使用单线程处理各种事件,所以一定要保证主循环是非阻塞的,否则会大大降低Worker的响应能力。
Nginx处理Http请求的过程
表面上看,当Nginx处理一个来自客户端的请求时,先根据请求头的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 客户端从 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三. ngx_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。配置:
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能完成复杂的访问控制,比如,访问数据库进行用户名、密码验证等。配置:
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, "!") '; }
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() '; }
四. 小结
这篇文章简单介绍了一下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'; }输出:
#在Lua中访问Memcached location =/memc { internal; #只能内部访问 set$memc_cmdget; 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 '; }
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 '; }
配置:
#在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连接时,尽量使用连接池。这样可以消除大量的建立、释放连接的开销。用lua扩展你的Nginx(整理),布布扣,bubuko.com
原文地址:http://blog.csdn.net/jiao_fuyou/article/details/36010691