在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
首先得声明。这不是我的原创,是在网上搜索到的一篇文章,原著是谁也搞不清楚了。按风格应该是属于章亦春的文章。 整理花了不少时间,所以就暂写成原创吧。 一. 概述 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,以及我在使用它开发项目的过程中遇到的一些问题。 二. 准备 首先,介绍一下Nginx的一些特性,便于后文介绍ngx_lua的相关特性。 Nginx进程模型 Nginx採用多进程模型,单Master—多Worker,由Master处理外部信号、配置文件的读取及Worker的初始化。Worker进程採用单线程、非堵塞的事件模型(Event Loop,事件循环)来实现port的监听及client请求的处理和响应,同一时候Worker还要处理来自Master的信号。 因为Worker使用单线程处理各种事件。所以一定要保证主循环是非堵塞的,否则会大大减少Worker的响应能力。 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。 配置指令:在Nginx中使用,和set指令和pass_proxy指令用法一样。每一个指令都有使用的context。 Nginx API:用于在Lua脚本中訪问Nginx变量,调用Nginx提供的函数。 以下举例说明常见的指令和API。
配置: 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。 能够通过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() '; }
假设优化一下。可能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'
#在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' 首先。定义了一个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时,这样的方案不再适用。 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
|
请发表评论