openresty入门
最近调研日志大数据分析相关的架构,接触到openresty项目,它的作用是通过lua语法写nginx扩展,从而基于nginx进程实现高性能的轻量业务处理。
这个openresty的优势就是内嵌在nginx里,简单不复杂的业务逻辑可以直接写在里面,就避免再去开发独立的服务了。
参考
我很久以前做过lua+c,但是现在已经忘的一干二净了。
幸亏有一位360公司的同学总结了一份教程,通过学习教程可以直接上手openresty:《OpenResty 最佳实践》。
实践
我的目标是接收客户端上报的一些统计日志,然后写到磁盘文件中去。
openresty框架提供了输出nginx error log的方法,但是将业务统计日志打印到error log里,会增加后续从error log中提取统计日志的额外工作。
因此,我需要自己封装日志输出方法,把统计日志单独输出到一个目录下,与nginx error日志完全隔离开来。
DEMO地址:https://github.com/owenliang/log-analyze/tree/master/openresty。
nginx配置接口地址与lua处理脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
worker_processes 4; #nginx worker 数量 error_log logs/error.log; #指定错误日志文件路径 events { worker_connections 10240; } http { lua_code_cache on; lua_need_request_body on; lua_package_path '$prefix/lua/?.lua;;'; server { listen 6699; location = /collect { set_by_lua_block $collect_path { return ngx.config.prefix() .. "/collect" } content_by_lua_file lua/content_by.lua; log_by_lua_file lua/log_by.lua; } } } |
对外暴露了/collect接口,内部指令含义如下:
- set_by_lua_block:相当于Nginx的set,可以用lua脚本动态设置一个nginx变量(var);我这里是为后面的lua脚本指定了统计日志的保存目录,也就是将日志保存在nginx工作目录下的collect子目录中。
- content_by_lua_file:相当于处理Nginx收到的请求,这里主要是把上报的日志进行一些规范化。
- log_by_lua_file:Nginx专门给扩展打印日志的一个回调时机,此时应答已经发回给了调用者,所以不会影响请求的响应时间。
content_by_lua_file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
local log = require("log") local json = require("cjson") -- 读取body local raw_msg = ngx.req.get_body_data() if not raw_msg or string.len(raw_msg) == 0 then -- 上报内容为空, 直接返回 ngx.exit(ngx.HTTP_OK) return end -- json数组解开为多条日志 local raw_arr = json.decode(raw_msg) -- 获取时间 local timestamp = os.date("%Y%m%d%H", os.time()) local log_arr = {} -- 拆分后的json行数组 for idx, log_item in pairs(raw_arr) do log_item.ip = ngx.var.remote_addr log_item.timestamp = timestamp local log_item_str = json.encode(log_item) table.insert(log_arr, log_item_str) end -- 结束应答, 在log_by_lua_file阶段输出日志 ngx.ctx.log_arr = log_arr |
读取请求body,做一些格式解析,最后将要输出的统计日志保存到nginx.ctx变量中。
nginx.ctx是请求的上下文,可以在log_by_lua_file阶段提取出来,从而打印统计日志到文件中。
log_by_lua_file
1 2 3 4 5 6 |
local log = require("log") -- 将日志顺序写到磁盘上 for _, log_item in ipairs(ngx.ctx.log_arr) do log.write(log_item) end |
在这个阶段,就是遍历content_by_lua_file保存的日志数组,逐条调用Log模块输出到文件中去。
log模块是我自己开发的日志模块,接下来看一下实现。
封装log模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
local _M = {} local file = nil local file_path = '' local file_idx = 0 local function build_file_path(ts) return ngx.var.collect_path .. "/collect." .. os.date("%Y%m%d%H", ts) .. ".log" end function _M.rotate_file() local now = os.time() local new_idx = math.floor(now / 3600) -- 判断是否需要轮转文件 if file and new_idx == file_idx then return end -- 打开新文件 local new_path = build_file_path(now) local new_file = io.open(new_path, 'a') if not new_file then -- 打开失败, 则保持旧文件继续写入 return end -- 关闭新文件 if file then file:close() end -- 替换为新文件 file = new_file file_path = new_path file_idx = new_idx end function _M.write(msg) _M.rotate_file() if not file then ngx.log(ngx.ERR, '文件无法打开: ' .. file_path) return end file:write(msg .. '\n') file:flush() end return _M |
在模块里通过file保存了当前打开的日志文件句柄,每小时都会切换到新的文件继续输出日志。
连续的多个请求不需要重复打开文件,因为openresty提供的lua执行虚拟环境(VM)是全局唯一的,就是说上述log模块是一次加载、持久缓存的。
所以我们完全可以在模块内保存局部变量file、file_path等,供当前连接的后续请求以及其他连接的请求共享,这一点在开发时需要特别注意,避免写出不可以共享执行的代码。
最后
上述流程在collect目录下生成了统计日志:https://github.com/owenliang/log-analyze/tree/master/openresty/collect。
openrestsy项目在源码中内置了nginx,lua,只需要下载源码一键编译即可。
为了使用openresty,我们应该直接使用它编译的nginx环境,若希望和现有线上nginx做结合,建议直接替换线上nginx或者做nginx反向代理到openresty nginx。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

连续的多个请求不需要重复打开文件,因为openresty提供的lua执行虚拟环境(VM)是全局唯一的,就是说上述log模块是一次加载、持久缓存的。
这个是在 lua_code_cache on 的时候才生效吧