最近出了个故障,有个接口的请求居然出现了长达几十秒的处理时间,由于日志缺乏,网络故障也解除了,就没法再重现这个故障了。为了可以在下次出现问题的时候能追查到问题,所以需要添加一些追踪日志。
添加这些追踪日志,我希望能够达到如下几点:
1、只有请求超过一定时间才记录,不然请求太多,系统扛不住
2、添加的代码可以尽量的少
3、对接口的影响尽量小,比如不影响实际时延,甚至记录日志时出现了错误,也不影响系统正常运行
openresty这套工具,可以在nginx处理请求的每一个阶段介入,编写代码进行逻辑处理。其可介入的流程如下图:
log Phase这个阶段,就是openresty能处理的最后阶段。到这个阶段的时候,实际上请求的响应已经发送给客户端了。所以使用 log_by_lua (知乎真特么蛋疼啊,左右下划线就自动斜体,还没提供转义功能)
log Phase这个阶段,就是openresty能处理的最后阶段。到这个阶段的时候,实际上请求的响应已经发送给客户端了。另外我也测试过了,即使在这个阶段发生了错误,如 io 错误,也不会影响接口的正常响应,所以使用 log_by_lua 很是符合需求。
好处不止如此, log_by_lua是一个请求的最后处理阶段,那么只要请求正常进行,比如会走到这一步,因此,在这一步,我们就知道了这个请求的耗时了。另外,则是我们的代码里有不少的 ngx.exit ,如果是在业务逻辑处理的时候就记录日志,那么每个出现 ngx.exit 的地方,都需要插入写日志到硬盘的操作,大大增加了代码量。
写日志到硬盘的这一步操作,可以在 log_by_lua 这个阶段来完成,剩下的另一个问题就是每一步记录的日志如何传递到 log_by_lua 这一阶段来了。
我处理的方式是使用ngx.ctx, 每一个请求,都会有自己独立的 ngx.ctx, 这个 ngx.ctx 会贯穿整个请求的始终,简单的log函数如下:
logger.lua
--------------------------
local _M = {}
function _M.log(format, ...)
if ngx.ctx.log_slot == nil then
ngx.ctx.log_slot = {}
end
arg = {...}
local logstr = ""
if arg == nil then
logstr = format
else
logstr = string.format(format, unpack(arg))
end
logstr = logstr .. "\t" .. ngx.now()
table.insert(ngx.ctx.log_slot, logstr)
end
return _M
到了 log_by_lua 阶段要把追踪日志写入到硬盘里,处理代码如下:
log_slot.lua
---------------------
local request_time = ngx.var.request_time
if request_time < 1 then
return --- 小于1秒的请求不记录
end
local slot = ngx.ctx.log_slot
if slot == nil or type(slot) ~= "table" then
return
end
local logs = table.concat(slot, "\n")
local f = assert(io.open("/logs/trace", "a"))
f:write(logs .. "\n")
f:close()
log_by_lua 可以用在 http 模块,也可以用在server模块,也能直接精确到location模块,即只到某个请求。所以你可以在nginx.conf 里的http里添加:
http{
log_by_lua_file '/code/log_slot.lua';
}
也可以在server的配置里添加:
server {
log_by_lua_file '/code/log_slot.lua';
}
更能直接在某个接口里添加:
/v1/test {
content_by_lua_file '/code/v1/test.lua';
log_by_lua_file '/code/log_slot.lua';
}
http里添加,则对所有的server; server里添加,则只针对此server;location里添加,就只针对这个接口。
但是,比较坑爹的是,log_by_lua 不像 access log,可以多层级使用。log_by_lua 在某层使用了之后,上层的 log_by_lua 就对此一层无效了。比如 /v1/test 接口添加了 log_by_lua, 那么 http 或者 server 里添加的 log_by_lua 在接受/v1/test接口的请求时都不会被用到。
正是因为这个坑,浪费了我不少的时间来解决。我们的系统里,http 模块是配置了 log_by_lua 的,用来做接口监控,监控返回的错误码,处理的时延等。如果我在 /v1/test 里添加了只针对 /v1/test 的追踪日志,那么接口监控就无法正常运行了。
不过天无绝人之路,我想到了一个处理方法如下:
monitor_log.lua
---------------------
local _M = {}
function _M.monitor_log()
local f = _M.api_monitor_log_func
if f == nil then
f, err = loadfile("/code/monitor.lua")
if f == nil then
ngx.log(ngx.ERR, "/code/monitor.lua, ", err)
--- 如果不存在接口监控,直接给一个空函数
f = function() end
end
_M.api_monitor_log_func = f
end
local status, err = pcall(f)
if not status then
ngx.log(ngx.ERR, "run api monitor /code/monitor.lua failed", err)
end
end
return _M
修改log_slot.lua代码如下:
local logger = require "code.monitor"
local request_time = ngx.var.request_time
logger.monitor_log()
if request_time < 1 then
return --- 小于1秒的请求不记录
end
local slot = ngx.ctx.log_slot
if slot == nil or type(slot) ~= "table" then
return
end
local logs = table.concat(slot, "\n")
local f = assert(io.open("/logs/trace", "a"))
f:write(logs .. "\n")
f:close()
如上,就可以进行其他层级的 log_by_lua 代码运行了,皆大欢喜,问题解决了。
当系统并发请求较低的时候,worker够用,则使用 log_by_lua 可以说是毫无坏处。当然,一旦 log_by_lua 出现了故障,如死循环,则会长时间占用worker,造成整个系统崩溃掉。