一、OpenResty日志切割问题的由来
作为一个基于Nginx和Lua的高性能Web平台,OpenResty在日志处理方面继承了Nginx的特性。但很多开发者在实际使用中经常会遇到日志文件膨胀的问题,特别是在高并发场景下,单个日志文件很快就会变得巨大无比。
这个问题我去年就遇到过。当时我们一个电商项目在促销期间,access.log一天就涨到了20GB,不仅占满磁盘空间,还导致日志分析工具直接崩溃。更糟的是,由于没有配置日志切割,我们不得不手动清理日志,结果不小心把当天的关键日志也给删了。
二、Nginx原生日志切割方案
先说说最基础的解决方案 - 使用Linux自带的logrotate工具。这是大多数Nginx用户的首选方案,配置起来也确实简单:
# /etc/logrotate.d/nginx 配置示例
/var/log/nginx/*.log {
daily # 每天轮转
missingok # 如果日志丢失也不报错
rotate 30 # 保留30天的日志
compress # 使用gzip压缩旧日志
delaycompress # 延迟压缩前一个日志文件
notifempty # 如果日志为空就不轮转
create 640 nginx adm # 设置新日志文件的权限
sharedscripts
postrotate
[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`
endscript
}
这个方案有几个明显的缺点:
- 切割不是实时的,最小粒度是天
- 依赖外部工具,增加了系统复杂性
- 切割时可能丢失部分日志
- 对于OpenResty的特殊日志格式支持不够友好
三、OpenResty的Lua日志切割方案
既然OpenResty支持Lua,我们何不直接用Lua来实现更灵活的日志切割?下面是我在实际项目中使用的方案:
-- lua/log_rotate.lua
local function setup_log_rotation(premature)
if premature then return end
-- 获取当前时间
local now = ngx.time()
local today = os.date("%Y%m%d", now)
-- 定义日志路径
local log_dir = "/var/log/openresty/"
local access_log = log_dir .. "access.log"
local error_log = log_dir .. "error.log"
-- 只在新的一天执行切割
if not ngx.shared.log_date or ngx.shared.log_date ~= today then
ngx.shared.log_date = today
-- 重命名当前日志文件
os.rename(access_log, access_log .. "." .. today)
os.rename(error_log, error_log .. "." .. today)
-- 重新打开日志文件
local ok, err = os.execute("kill -USR1 " .. ngx.worker.pid())
if not ok then
ngx.log(ngx.ERR, "failed to reopen logs: ", err)
end
end
end
-- 每60秒检查一次是否需要切割
local ok, err = ngx.timer.every(60, setup_log_rotation)
if not ok then
ngx.log(ngx.ERR, "failed to create timer: ", err)
end
然后在nginx.conf中加载这个Lua脚本:
http {
lua_shared_dict log_date 1m;
init_worker_by_lua_file lua/log_rotate.lua;
server {
access_log /var/log/openresty/access.log;
error_log /var/log/openresty/error.log;
}
}
这个方案的优势很明显:
- 切割粒度可以精确到秒
- 完全集成在OpenResty内部,不依赖外部工具
- 可以灵活处理各种日志格式
- 切割过程不会丢失日志
四、进阶方案:按大小切割日志
对于特别高流量的场景,按天切割可能还不够。我们可以改进上面的方案,实现按日志大小切割:
-- lua/size_based_rotation.lua
local MAX_LOG_SIZE = 100 * 1024 * 1024 -- 100MB
local function check_log_size()
local access_log = "/var/log/openresty/access.log"
local f = io.open(access_log, "r")
if f then
local size = f:seek("end")
f:close()
if size > MAX_LOG_SIZE then
local timestamp = os.date("%Y%m%d%H%M%S")
os.rename(access_log, access_log .. "." .. timestamp)
os.execute("kill -USR1 " .. ngx.worker.pid())
end
end
end
local function log_rotation(premature)
if premature then return end
check_log_size()
end
ngx.timer.every(60, log_rotation) -- 每分钟检查一次
五、注意事项和最佳实践
在实际使用这些方案时,有几个关键点需要注意:
- 文件权限问题:确保OpenResty工作进程有权限读写日志目录
- 共享内存大小:lua_shared_dict需要足够大来存储状态
- 定时器间隔:不要设置得太频繁,避免影响性能
- 日志压缩:可以考虑在切割后自动压缩旧日志
- 异常处理:要妥善处理文件操作可能出现的错误
这里还有一个完整的生产环境配置示例:
http {
lua_shared_dict log_rotation 10m;
init_worker_by_lua_block {
local MAX_SIZE = 200 * 1024 * 1024 -- 200MB
local ROTATE_INTERVAL = 300 -- 5分钟
local function rotate_log(log_path)
local timestamp = os.date("%Y%m%d%H%M%S")
local new_path = log_path .. "." .. timestamp
os.rename(log_path, new_path)
-- 异步压缩旧日志
local cmd = "gzip " .. new_path .. " &"
os.execute(cmd)
end
local function check_logs()
local logs = {
"/var/log/openresty/access.log",
"/var/log/openresty/error.log"
}
for _, log in ipairs(logs) do
local f = io.open(log, "r")
if f then
local size = f:seek("end")
f:close()
if size > MAX_SIZE then
rotate_log(log)
os.execute("kill -USR1 " .. ngx.worker.pid())
end
end
end
end
local function log_rotation(premature)
if premature then return end
check_logs()
end
ngx.timer.every(ROTATE_INTERVAL, log_rotation)
}
}
六、性能优化技巧
在高并发环境下,日志切割可能会成为性能瓶颈。这里分享几个优化经验:
- 使用ngx.timer.at代替ngx.timer.every,实现更精确的控制
- 将文件操作放在单独的定时器中,避免阻塞worker进程
- 使用ngx.thread.spawn来并行处理多个日志文件
- 对于超大规模部署,可以考虑将日志直接发送到syslog或ELK
这里是一个优化后的版本:
local function async_rotate(log_path)
local co = ngx.thread.spawn(function()
local timestamp = os.date("%Y%m%d%H%M%S")
local ok, err = os.rename(log_path, log_path.."."..timestamp)
if not ok then
ngx.log(ngx.ERR, "failed to rotate log: ", err)
end
return ok
end)
return co
end
local function optimized_rotation()
local logs = {
"/var/log/openresty/access.log",
"/var/log/openresty/error.log"
}
local threads = {}
for _, log in ipairs(logs) do
table.insert(threads, async_rotate(log))
end
for _, thread in ipairs(threads) do
ngx.thread.wait(thread)
end
os.execute("kill -USR1 " .. ngx.worker.pid())
end
七、总结
OpenResty的日志切割看似简单,但要实现一个稳定可靠的方案需要考虑很多因素。通过本文介绍的几种方法,你可以根据实际需求选择合适的方案:
- 对于小型应用,简单的logrotate可能就足够了
- 中型应用建议使用Lua定时器方案
- 大型高并发应用则需要考虑更复杂的优化策略
无论选择哪种方案,都要记得在实际环境中充分测试,特别是要模拟高并发场景下的日志写入情况。另外,完善的监控和告警机制也很重要,这样在日志系统出现问题时能及时发现和处理。
评论