在之前的文章中,已经介绍了ngx_lua的一些基本介绍,这篇文章主要着重讨论一下如何通过ngx_lua同后端的memcached、redis进行非阻塞通信。
1. 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';
- }
输出:
- $ curl 'http://localhost/memc?cmd=set&key=foo&val=Hello'
- $ STORED
- $ curl 'http://localhost/memc?cmd=get&key=foo'
- $ Hello
配置:
- #在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
- ';
- }
- $ curl 'http://localhost/lua_memc?key=foo'
- $ Hello
2. 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
- ';
- }
- $ curl 'http://localhost/lua_redis?key=foo'
- $ Hello
3. 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的原生请求查询。
配置:
- #在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';
- }
- -- 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
- $ curl 'http://localhost/pipeline'
- $ first
- second
4. 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(使用之前的Memc模块),rps为20000。在使用连接池之后,rps一路飙到140000。在实际情况下,这么大的提升可能达不到,但是基本上100-200%的提高还是可以的。
5. 小结
[list] 如何获取HTTP请求头?
直接在 ngx_lua 中访问 NginX 内置变量 ngx.var.http_HEADER 即可获得请求头 HEADER 的内容。对于常见的特殊头(Content-Type、Cookie 等),NginX 还使用了特殊的变量来独立保存,例如“Content-Type”头可以通过 ngx.var.content_type 变量取得。
如何获取GET参数?
在 ngx_lua 中访问 NginX 内置变量 ngx.var.arg_PARAMETER 即可获得GET参数PARAMETER的内容。
如何获取POST请求体数据?
要获得完整的POST请求体数据,可以访问 NginX 内置变量 ngx.var.request_body(注意:由于 NginX 默认在处理请求前不自动读取 request body,所以目前必须显式借助 form-input-nginx 模块才能从该变量得到请求体,否则该变量内容始终为空!)。如果想获取 POST 方式提交的表单参数,还可以借助 form-input-nginx 模块省去解析过程。例如:
- location /form {
- set_form_input $name;
- content_by_lua '
- local name = ngx.var.name or "";
- local say = ngx.say
- say("My name is: ", name)
- ';
- }
location /form { set_form_input $name; content_by_lua ' local name = ngx.var.name or ""; local say = ngx.say say("My name is: ", name) '; }
如何设置/获取HTTP响应头?
我们已经设计了对应的API接口,近期即会予以实现。
如何使用 Lua 外部模块?
通过 require 引用即可,和在普通的 Lua 代码里一样。需要注意的一点是,通过 require 引用外部模块一般有 2 种写法。老的写法是:
require("xxx")
这样会将模块命名空间表直接导入当前全局环境内;而新的写法是:
local xxx = require("xxx")
这样的写法将模块命名空间表缓存在同名局部变量中,访问更快,也不会污染当前全局环境。但最重要的一点是: 老的写法在 ngx_lua 中会出现模块导入后无法访问的现象!这是由 ngx_lua 实现原理决定的。ngx_lua 使用每请求一个 coroutine 的方式运行用户代码,coroutine 的全局环境是重新关联的,因此用户代码相当于运行在一个沙盒中,请求处理结束后用户代码产生的所有全局环境修改都会被舍弃,避免多个请求之间产生交叉影响,也降低了因滥用全局环境产生内存泄漏的风险。而 require 利用了全局共享的 package.loaded 表缓存已载入模块的数据,以达到避免重复加载模块的目的。很明显,这种结构必然会使首个请求中通过 require 注入全局环境的模块命名空间表在后续请求中无法访问,因为后续请求中 package.loaded 表内已经有之前加载模块的数据,故 require 不会再次将命名空间表注入当前全局环境,使得以后所有依赖于模块的操作都失败。
鉴于这一问题, 我们推荐开发人员总是使用新的 require 写法(即使用局部变量缓存模块表),对于那些因为某种原因无法更新 require 写法的代码,可以通过在开始处理请求前清空 package.loaded 表中对应模块数据的方式强制加载模块并注入全局环境(注意每次都加载模块可能产生性能瓶颈!),例如:
package.loaded.xxx = nil require("xxx")