一、为什么需要Lua日志系统

在分布式系统中,调试和追踪问题就像是在大海捞针。服务分布在不同的机器上,调用链可能横跨多个节点,传统的打印日志方式根本不够用。想象一下,你正在处理一个线上问题,需要查看某个用户请求在整个系统中的流转情况,如果每个服务都各自记录日志,你要手动去几十台机器上收集日志,然后像拼图一样把它们拼起来,这效率简直低得令人发指。

Lua作为一种轻量级脚本语言,特别适合嵌入到各种服务中做日志采集和预处理。它的协程机制可以很好地处理高并发日志写入,而且资源占用极小。我们团队最近就用Lua重构了整个日志系统,单机日志处理能力提升了5倍,最重要的是终于能完整追踪一个请求的完整生命周期了。

二、Lua日志系统的核心设计

先来看一个最简单的Lua日志记录示例(技术栈:OpenResty + Lua):

-- 日志记录函数
local function log(level, message, request_id)
    -- 获取当前时间戳
    local timestamp = ngx.now() * 1000  -- 毫秒级时间戳
    
    -- 构造日志条目
    local log_entry = {
        timestamp = timestamp,
        level = level,
        message = message,
        service = "order-service",  -- 服务标识
        host = ngx.var.hostname,    -- 主机名
        request_id = request_id     -- 请求唯一ID
    }
    
    -- 将日志写入本地缓存
    local ok, err = ngx.shared.log_buffer:add(timestamp, cjson.encode(log_entry))
    if not ok then
        ngx.log(ngx.ERR, "Failed to buffer log: ", err)
    end
end

-- 示例调用
local request_id = ngx.var.request_id or ngx.md5(ngx.now()..ngx.worker.pid())
log("INFO", "用户订单创建开始", request_id)

这个示例展示了最基本的日志记录功能,但实际生产环境需要更多考虑。比如我们加入了日志分级、请求追踪ID、服务标识等关键字段。ngx.shared.log_buffer是OpenResty提供的共享内存字典,可以避免频繁的IO操作。

三、分布式追踪的关键实现

真正的挑战在于如何把分散的日志串联起来。我们采用了OpenTelemetry的TraceID标准,并在Nginx入口层统一生成请求ID。看看我们改进后的版本:

-- 分布式追踪增强版日志
local function trace_log(level, message, span_ctx)
    -- 从上下文获取追踪信息
    local trace_id = span_ctx.trace_id
    local span_id = span_ctx.span_id
    local parent_id = span_ctx.parent_id or ""
    
    -- 构造调用链信息
    local call_stack = {
        {service = "gateway", timestamp = ngx.now() - 0.1},  -- 上游服务
        {service = "order-service", timestamp = ngx.now()}   -- 当前服务
    }
    
    local log_entry = {
        trace_id = trace_id,
        span_id = span_id,
        parent_span_id = parent_id,
        call_stack = call_stack,
        -- 其他字段同上例...
    }
    
    -- 写入Kafka而不是本地文件
    local ok, err = kafka_producer:send("log_topic", nil, cjson.encode(log_entry))
    if not ok then
        ngx.log(ngx.ERR, "Kafka发送失败: ", err)
        -- 降级写入本地文件
        local file = io.open("/tmp/fallback.log", "a")
        file:write(cjson.encode(log_entry).."\n")
        file:close()
    end
end

这个版本有几个关键改进:

  1. 引入了完整的调用链追踪
  2. 日志不再写入本地文件,而是发送到Kafka
  3. 增加了降级处理机制
  4. 遵循了OpenTelemetry标准,方便与其他系统集成

四、日志收集与分析的完整方案

单纯的日志收集还不够,我们需要完整的处理流水线。下面是我们最终采用的架构:

-- 日志处理流水线示例
local function process_log_pipeline()
    -- 1. 从Kafka消费日志
    local consumer = kafka_consumer:new("log_group")
    while true do
        local messages = consumer:poll(100)  -- 批量获取
        
        -- 2. 日志解析和增强
        for _, msg in ipairs(messages) do
            local log_data = cjson.decode(msg.value)
            
            -- 添加地理位置信息
            log_data.geo = geoip.lookup(log_data.client_ip)
            
            -- 3. 敏感信息过滤
            log_data.message = filter_sensitive_data(log_data.message)
            
            -- 4. 写入Elasticsearch
            local ok, err = es_client:index{
                index = "logs-"..os.date("%Y.%m.%d"),
                body = log_data
            }
        end
        
        -- 提交消费位移
        consumer:commit()
    end
end

-- 敏感信息过滤函数
local function filter_sensitive_data(text)
    -- 过滤身份证号
    text = string.gsub(text, "[1-9]%d%d%d%d%d%d%d%d%d%d%d%d%d%d[0-9Xx]", "***")
    -- 过滤手机号
    text = string.gsub(text, "1[3-9]%d%d%d%d%d%d%d%d", "****")
    return text
end

这个流水线完成了日志从收集到存储的全过程,特别注意了:

  1. 批量消费提高吞吐量
  2. 动态添加元数据
  3. 敏感信息过滤
  4. 按日期分索引存储

五、技术选型的深度思考

为什么选择Lua而不是其他语言?我们做过详细的对比测试:

  1. 性能方面:在Nginx环境下,LuaJIT的性能接近C,远超其他脚本语言
  2. 资源占用:一个Lua协程只需要2KB内存,而Java线程至少需要1MB
  3. 嵌入能力:Lua可以轻松嵌入到Nginx、Redis等各种服务中
  4. 热更新:不需要重启服务就能更新日志处理逻辑

但Lua也有明显的缺点:

  • 缺乏成熟的日志生态(对比Java的Log4j2)
  • 调试工具不够完善
  • 不适合复杂的日志处理逻辑

我们的解决方案是:用Lua做采集和初步处理,复杂的分析交给后端的Java/Go服务。

六、生产环境中的实战经验

在实际部署中,我们踩过不少坑,这里分享几个关键经验:

  1. 日志量控制:一定要有采样机制,否则Kafka会爆
-- 采样率控制
local sample_rate = 0.1  -- 10%采样
if math.random() < sample_rate then
    -- 记录详细日志
else
    -- 只记录关键字段
end
  1. 日志分级策略
  • DEBUG:开发环境全量,生产环境关闭
  • INFO:记录关键路径
  • WARN:预期内的异常
  • ERROR:需要立即处理的异常
  1. 日志轮转:即使使用Kafka,本地也要有fallback
# crontab每天轮转
0 0 * * * /usr/sbin/logrotate /etc/logrotate.d/our_app
  1. 监控告警:对ERROR日志要有实时告警
-- 错误日志实时告警
if log_level == "ERROR" then
    local alert_msg = string.format("[%s]%s", service_name, message)
    alert_center:send(alert_msg)
end

七、未来演进方向

现有的系统已经运行良好,但我们还在规划几个增强方向:

  1. 智能日志分析:用机器学习自动聚类相似错误
  2. 动态调试:在不重启服务的情况下动态调整日志级别
  3. 日志压缩:对重复日志进行压缩存储
  4. 边缘计算:在设备端就完成初步的日志过滤和分析

比如动态调试的实现思路:

-- 动态日志级别控制
local dynamic_log_level = "INFO"  -- 默认级别

-- 通过HTTP API动态修改
location /admin/log_level {
    content_by_lua_block {
        dynamic_log_level = ngx.req.get_uri_args()["level"]
        ngx.say("Log level updated to "..dynamic_log_level)
    }
}

-- 使用时检查级别
local function smart_log(level, message)
    if level_priority[level] >= level_priority[dynamic_log_level] then
        -- 记录日志
    end
end

八、总结与建议

经过一年的实践,我们总结出分布式日志系统的几个黄金法则:

  1. 唯一请求ID:必须贯穿整个调用链
  2. 结构化日志:方便后续分析
  3. 异步写入:绝不能阻塞业务逻辑
  4. 分级存储:热数据存Elasticsearch,冷数据转HDFS
  5. 容灾设计:网络中断时要有降级方案

对于中小团队,我建议从最简单的版本开始,逐步迭代。可以先实现请求追踪,再完善分析功能。记住:一个能解决问题的简单方案,远胜过设计完美但迟迟不能上线的复杂系统。

最后分享一个我们正在使用的日志查询技巧,在Kibana中用如下查询可以快速定位问题:

trace_id:"abc123" AND level:ERROR AND service:("order-service" OR "payment-service")

这种查询能立即展示一个错误请求在所有相关服务中的日志,极大提升了排查效率。