openresty 日志输出的处理

最近出了个故障,有个接口的请求居然出现了长达几十秒的处理时间,由于日志缺乏,网络故障也解除了,就没法再重现这个故障了。为了可以在下次出现问题的时候能追查到问题,所以需要添加一些追踪日志。

添加这些追踪日志,我希望能够达到如下几点:

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,造成整个系统崩溃掉。

编辑于 2018-06-06

文章被以下专栏收录