ngx.log(log_level, ...)
记录 OpenResty 的运行日志,用法很类似 Lua 的标准库函数 print
,可以接受任意多个参数,记录任意信息。OpenResty 同时替换了全局函数print
,它等价于ngx.log(ngx.NOTICE, ...)
。ngx.STDERR :日志直接打印到标准输出,最高级别
ngx.EMERG :紧急错误
ngx.ALERT :严重错误,需要报警给运维系统
ngx.CRIT :严重错误
ngx.ERR :普通错误
ngx.WARN :警告
ngx.NOTICE :提醒
ngx.INFO :一般信息
ngx.DEBUG :调试信息,debug版本才会生效
INFO
或 NOTICE
级别的日志来调试代码即可。ERR用来做错误异常捕获。error_log
设置,就可以开启低等级的日志。ngx.log(ngx.DEBUG,"debug log")
ngx.log(ngx.INFO,"info log")
ngx.log(ngx.NOTICE,"notice log")
ngx.log(ngx.WARN,"warn log")
ngx.log(ngx.ERR,"error log")
ngx.log(ngx.CRIT,"crit log")
ngx.log(ngx.ALERT,"alert log")
ngx.log(ngx.EMERG,"emerg log")
ngx.log(ngx.STDERR,"stderr log")
ngx.say("testlog")
location /testlog {
default_type text/html;
content_by_lua_file /openresty-test/testlog.lua;
}
error_log file level;
debug|info|notice|warn|error|crit|alert|emerg
worker_processes 1;
error_log logs/error.log debug;
events {
worker_connections 1024;
}
http {
server {
listen 80;
location ~ ^/(\w+) {
default_type text/html;
content_by_lua_file /openresty-test/$1.lua;
}
}
}
rewrite
access
content
等若干个阶段,每个阶段都可以指定单独的 lua 脚本来做处理。access
阶段,就可以对客户端的 IP 进行判定,来决定拒绝还是放行,而不用等到 content
阶段。ssl SSL/TLS安全通信和验证
prehead 在正式处理之前 预读数据,接受http请求头
rewrite 检查/改写url,实现跳转 重定向
access 访问权限控制
content 产生实际响应内容
filter 对content产生的内容进行过滤加工
log 请求处理完毕,记录日志,收尾
rewrite_by_lua_file --rewrite阶段,检查,改写uri
access_by_lua_file --检查权限,例如ip地址 限制访问次数
content_by_lua_file --主要的逻辑,产生内容,返回给客户端的。
body_filter_by_lua_file --filter阶段,对数据编码 加密 附加额外数据等
log_by_lua_file --可以向后端发送处理完成的回执
location ~ ^/(\w+) {
default_type text/html;i
rewrite_by_lua_file /openresty-test/rewrite.lua;
access_by_lua_file /openresty-test/access.lua;
content_by_lua_file /openresty-test/content.lua;
body_filter_by_lua_file /openresty-test/filter.lua;
log_by_lua_file /openresty-test/log.lua
}
# rewrite.lua
ngx.log(ngx.INFO,"rewrite")
# access.lua
ngx.log(ngx.INFO,"access")
# content.lua
ngx.log(ngx.INFO,"content")
# filter.lua
ngx.log(ngx.INFO,"filter")
# log.lua
ngx.log(ngx.INFO,"log")
access
阶段进行处理,实现 ip黑名单功能。修改 access.lua 文件如下:这个代码的功能是判断客户端的ip,如果是127.0.0.1
,就返回 403 Forbidden
,就是说禁止本机访问。ngx.log(ngx.INFO,"access")
-- 获取客户端IP
local clientIp = ngx.var.remote_addr
ngx.log(ngx.DEBUG, "clientIp: " .. clientIp)
-- 判断是否是黑名单
if clientIp == '127.0.0.1' then
return ngx.exit(ngx.HTTP_FORBIDDEN)
else
ngx.say(clientIp)
end
403 Forbidden
。当使用 IP 时,就可以正常访问。# 在server配置上面声明一个缓存,和upstream同级的位置进行配置
proxy_cache_path /usr/local/etc/openresty/cache_tmp levels=1:2 keys_zone=cache_tmp:100m inactive=7d max_size=10g;
# 说明:
# levels = 1:2 使用2级目录存储缓存,减少寻址消耗,防止一个目录存储缓存文件过多,导致查询缓存效率低下
# keys_zone nginx内存中开辟了100m的空间,存储缓存
# max_size : 文件系统最多存储10g,所有的文件存储超过10g,采用lru淘汰算法
# 在location中增加以下配置
# 指定proxy_cache_path定义的缓存空间
proxy_cache cache_tmp;
# 指定缓存的key
proxy_cache_key $uri;
# 只有200 206 304 302状态的请求才被缓存,其他不被缓存
proxy_cache_valid 200 206 304 302 7d;
# nginx 文件级别的缓存,缓存访问的是本地磁盘文件,效率比较低,从磁盘中读取数据,效率反而变低了....
特有的 ngx.location.capture_multi 功能让人印象深刻,其可以达到极大的减少浏览器的http连接数量,并且可以异步并发的访问后台 Java/PHP/Python 等等接口
。OpenResty 架构的web可以轻松超越Node.js的性能,并且对后端语言没有限制
,你可以使用Java/PHP/Python等等各种语言。OpenResty(nginx+lua)可以替代node.js的前端渲染的功能。# 在nginx的配置文件 nginx.conf 的 http 端下面加入指令:ngx_cache 为缓存的名称,可以自定义
lua_shared_dict ngx_cache 128m;
# 名称为 ngx_cache 大小为128m的内存用于缓存,注意该缓存是所有nginx work process所共享的
# 下面测试一下,首先在 nginx.conf的server端中加入:
location /cache {
content_by_lua_file lua/cache.lua;
}
local ngx_cache = ngx.shared.ngx_cache
local value = ngx_cache.get(key)
local succ, err, forcible = ngx_cache:set(key, value, expire)
function get_from_cache(key)
local cache_ngx = ngx.shared.my_cache
local value = cache_ngx:get(key)
return value
end
function set_to_cache(key,value,exptime)
if not exptime then
exptime = 0
end
local cache_ngx = ngx.shared.my_cache
local succ, err, forrcible = cache_ngx:set(key,value,exptime)
return succ
end
local args = ngx.req.get_uri_args()
local id = args["id"]
local item_model = get_from_cache("item_"..id)
if item_model == nil then
local resp = ngx.location.capture("/item/get?id="..id)
item_model = resp.body
ngx.log(ngx.ERR, resp.body)
set_to_cache("item_" .. id, item_model, 60)
end
ngx.say(item_model)
local redis = require "resty.redis"
local red = redis:new()
function set_to_cache(key, value, expire)
if not expire then
expire = 0
end
local ngx_cache = ngx.shared.ngx_cache
local succ, err, forcible = ngx_cache:set(key, value, expire)
return succ
end
function get_from_cache(key)
local ngx_cache = ngx.shared.ngx_cache
local value = ngx_cache:get(key)
if not value then
-- 注意此处新建的redv,不能使用上面的value,会冲突
local redv = get_from_redis(key)
if not redv then
ngx.say("redis cache not exists")
return
end
set_to_cache(key, redv, 60)
return redv
end
ngx.say("get from cache")
return value
end
function set_to_redis(key, value)
red:set_timeout(100000)
local ok, err = red:connect("192.168.254.128", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local ok, err = red:set(key, value)
if not ok then
ngx.say("failed set to redis: ", err)
return
end
return ok
end
function get_from_redis(key)
red:set_timeout(1000)
local ok, err = red:connect("192.168.254.128", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local res, err = red:get(key)
if not res then
ngx.say("failed get redis cache: ", err)
return ngx.null
end
ngx.say("get cache from redis.")
return res
end
set_to_redis('dog', 'Bob')
local rs = get_from_cache('dog')
ngx.say(rs)
and
和 or
作为逻辑操作符。比如 true and false
返回 false
,而 false or true
返回 true
。xx = xx or value
的语句。如果没有给 xx 入参指定值,它的取值为 nil,该语句就会赋值 value 给它。xx = (xx == nil) and xx or value
。实际跑下会发现,这个语句也跑不过 xx 为 false 的 case。这就是第二个坑了。a and b or c
模式是这些高仿品中的一员。这个模式其实包含两个表达式:先 a and b
得到 x,再执行 x or c
得到最终结果 y。在大多数时候,它表现得像是三元操作符,但可惜它不是。a and b
的执行结果恒假;如果 x 恒假,则 x or c
的执行结果恒为 c。所以只要 b 的值为假,那么最终结果恒为 c。expr and func1() or func2()
的形式,如果 func1 只是偶尔返回假值,一颗定时炸弹就埋下了。ngx.null
这个常量。ngx.null
是一个值为 NULL 的 userdata。$ resty -e 'print(tostring(ngx.null))'
userdata: NULL
ngx.null
的特殊性,可能会直接判断返回值是否为真(或者是否为 nil),然后就掉到坑里了。尤其是如果底层逻辑没有把 ngx.null 包装好,上层调用的人也许压根没想到除了 nil 和 value 之外,还有一个 ngx.null 的存在!local res, err = redis.get('key1')
if not res then
...
end
-- 大部分情况都是好的,直到有一天 key1 不存在…… 500 Internal Server Error!
-- res = res + 1
-- 正确做法
if res ~= ngx.null then
res = res + 1
#!/usr/bin/env luajit
local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
print(tostring(cdata_null))
if cdata_null == nil then
print('cdata:NULL is equal to nil')
end
if cdata_null then
print('...but it is not nil!')
end
cdata:NULL
就够了。我们可以利用 cdata:NULL
跟 nil
相等这一点:#!/usr/bin/env luajit
local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
if cdata_null == nil then
print('cdata:NULL found')
end
针对这样的情况,我们应该如何选择呢?
awesome-resty
的项目中逐个查找一遍,这其中就有专门的 Routing Libraries
: • lua-resty-route — A URL routing library for OpenResty supporting multiple route matchers, middleware, and HTTP and WebSockets handlers to mention a few of its features
• router.lua — A barebones router for Lua, it matches URLs and executes Lua functions
• lua-resty-r3 — libr3 OpenResty implementation, libr3 is a high-performance path dispatching library. It compiles your route paths into a prefix tree (trie). By using the constructed prefix trie in the start-up time, you may dispatch your routes with efficiency
• lua-resty-libr3 — High-performance path dispatching library base on libr3 for OpenResty
awesome-resty
中找到可用路由库的努力没有成功,但 libr3 的实现,还是给我们指引了一个新的方向:用 C 来实现前缀树以及 FFI 封装,这样应该可以接近时间复杂度和代码性能上的最优方案。local radix = require("resty.radixtree")
local rx = radix.new({
{
path = "/aa",
host = "foo.com",
method = {
"GET", "POST"},
remote_addr = "127.0.0.1",
},
{
path = "/bb*",
host = {
"*.bar.com", "gloo.com"},
method = {
"GET", "POST", "PUT"},
remote_addr = "fe80:fe80::/64",
vars = {
"arg_name", "jack"},
}
})
ngx.say(rx:match("/aa", {
host = "foo.com",
method = "GET",
remote_addr = "127.0.0.1"
}))
lua-resty-radixtree
支持根据 uri、host、http method、http header、Nginx 变量、IP 地址等多个维度,作为路由查找的条件;同时,基数树的时间复杂度为 O(K),性能远比现有 API 网关常用的“遍历 +hash 缓存”的方式,来得更为高效。lua-rapidjson
,就是非常好的一个选择。这部分你完全没有必要自己去写一个,json schema 已经足够强大了。下面就是一个简单的示例:local schema = {
type = "object",
properties = {
count = {
type = "integer", minimum = 0},
time_window = {
type = "integer", minimum = 0},
key = {
type = "string", enum = {
"remote_addr", "server_addr"}},
rejected_code = {
type = "integer", minimum = 200, maximum = 600},
},
additionalProperties = false,
required = {
"count", "time_window", "key", "rejected_code"},
}
rewrite
、access
、header filer
、body filter
和 log
阶段,甚至在 balancer
阶段也可以设置自己的负载均衡算法。所以,我们应该在 Nginx 的配置文件中暴露这些阶段,并在对插件的实现中预留好接口。# /usr/local/etc/openresty目录下
# vim conf/gray.conf
1;
error_log logs/error.log;
events{
worker_connections 1024;
}
http{
lua_package_path "$prefix/lualib/?.lua;;";
lua_package_cpath "$prefix/lualib/?.so;;";
upstream prod {
server 192.168.254.128:8080;
}
upstream pre {
server 192.168.254.128:8081;
}
server {
listen 80;
server_name localhost;
location /api {
content_by_lua_file lua/gray.lua;
}
location @prod {
proxy_pass http://prod;
}
location @pre {
proxy_pass http://pre;
}
}
server {
listen 8080;
location / {
content_by_lua_block {
ngx.say("I'm prod env");
}
}
}
server {
listen 8081;
location / {
content_by_lua_block {
ngx.say("I'm pre env");
}
}
}
}
# vim lua/gray.lua
local redis=require "resty.redis";
local red=redis:new();
red:set_timeout("1000");
local ok,err=red:connect("192.168.254.128",6379);
if not ok then
ngx.say("failed to connect redis",err);
return;
end
local ip=ngx.var.remote_addr;
local ip_lists=red:get("gray");
if string.find(ip_lists,ip) == nil then
ngx.exec("@prod");
else
ngx.exec("@pre");
end
local ok,err=red:close();
# ./redis-cli
127.0.0.1:6379> set gray 192.168.254.128
http://192.168.254.128/api
,返回结果是 I'm pre env
。# ./redis-cli
127.0.0.1:6379> set gray 192.168.254.130
http://192.168.254.128/api
,返回结果是 I'm prod env
。