PAYATHON 2026

released software 应保留多少 traces 才能调试多数问题?

技术老齐

结论

没有通用标准规定发布版本必须保留多少条 traces,也没有可靠的 100 / 70 / 30 / 10 分配比例。

日志是否有价值,要看事件频率、单条日志大小、诊断用途和运行环境,不能只看代码里有多少条 log statements。生产版本一般需要保留能够反映系统状态、异常和关键业务结果的 INFO、WARN、ERROR 日志,并支持按 component、用户或请求临时开启 DEBUG/TRACE。实际输出量应根据性能和容量预算确定。

软件成熟后,也不建议直接把全局日志级别从 INFO 永久提高到 WARN 或 ERROR,否则可能丢失启动、配置变更、版本切换、任务完成等正常但重要的运行记录。

为什么不能按条数分配

假设两个 component 都定义了 100 个日志点:

  • Component A 的某条 INFO 只在每次启动时执行一次。
  • Component B 的某条 INFO 位于高频消息循环中,每秒可能执行数万次。

两者的日志点数量相同,实际产生的数据量以及 CPU、I/O 开销可能相差几个数量级。字段数量、字符串格式化成本、调用栈和同步写盘方式也会影响单条日志的性能开销。

更适合衡量日志规模的指标是:

日志带宽 = 平均事件频率 × 平均单条日志大小
每日容量 = 日志带宽 × 86400

同时还要测量日志对 CPU、内存、磁盘 I/O 和请求延迟的影响。

各 log level 应表达什么

log level 应反映事件的性质和严重程度,不能用来控制某个阶段要保留多少条日志。

TRACE

用于记录很细的执行过程,如状态机迁移、协议字段和函数内部步骤。默认关闭,只在有限范围内短时间启用。

DEBUG

用于记录开发和深入诊断所需的信息,如分支选择、缓存命中、重试过程和内部状态摘要。生产环境可以保留这些日志点,但通常不默认输出。

INFO

用于记录低频且对运维有用的正常事件,例如:

  • component 启动、停止和版本信息;
  • 配置加载或变更;
  • 后台任务开始、完成和结果摘要;
  • 外部依赖连接状态变化;
  • 重要业务操作成功,但不包含敏感内容。

INFO 不适合记录循环的每次迭代、每个消息包或每次函数调用。

WARN

表示发生了异常,但系统已经恢复、进入降级状态或仍可继续工作。例如,超时后重试成功、切换到备用服务,或者队列接近容量上限。

如果值班人员看到某条 WARN 后不需要采取行动,也无法借此诊断问题,这条日志可能更适合放在 INFO 或 DEBUG 级别。

ERROR

表示某项操作失败、数据可能丢失,或者问题需要人工关注。ERROR 应提供足够的上下文,但不要在异常向上传播的每一层重复记录同一个错误。

捕获异常后,如果当前代码只补充上下文并继续向上抛出,通常应由最终处理异常的边界统一记录一次。

建立可执行的日志规范

1. 先定义诊断场景

先列出发布版本中常见且影响较大的问题,例如:

  • component 无法启动;
  • 跨 component 请求超时;
  • 消息丢失或重复;
  • 状态不一致;
  • 外部依赖不可用;
  • 资源耗尽;
  • 某个用户请求执行失败。

再检查现有日志能否回答这些问题:

发生了什么?
发生在什么时候?
涉及哪个 component、实例和版本?
哪个请求或任务受到影响?
请求经过了哪些 components?
失败原因和最终处理结果是什么?

如果重复日志无法帮助回答这些问题,数量再多也没有太大用处。

2. 为跨 component 调用传递关联标识

在多 component 系统中,能否关联不同日志,往往比日志数量更重要。每个请求至少应携带稳定的 trace_id,必要时还可以加入 span_id、request_id 或 job_id。

public Result handle(Request request) {
    String traceId = request.getTraceId();

    logger.info(
        "task_started trace_id={} task_id={} component={}",
        traceId,
        request.getTaskId(),
        "scheduler"
    );

    try {
        Result result = worker.execute(request);

        logger.info(
            "task_completed trace_id={} task_id={} status={}",
            traceId,
            request.getTaskId(),
            result.status()
        );
        return result;
    } catch (TimeoutException e) {
        logger.error(
            "task_failed trace_id={} task_id={} dependency={} reason=timeout",
            traceId,
            request.getTaskId(),
            "worker-service",
            e
        );
        throw e;
    }
}

所有 components 应统一字段名,避免一个 component 使用 requestId,另一个却使用 req_no。

3. 控制高频路径

循环、消息处理和重试路径很容易引发日志洪泛。可以通过聚合、采样或限速控制输出,同时保留必要的诊断信息。

long failedCount = 0;
long lastReportTime = System.currentTimeMillis();

for (Message message : messages) {
    try {
        process(message);
    } catch (RecoverableException e) {
        failedCount++;
        logger.debug(
            "message_processing_failed message_id={} reason={}",
            message.id(),
            e.getMessage()
        );
    }

    long now = System.currentTimeMillis();
    if (now - lastReportTime >= 60_000) {
        logger.warn(
            "message_failures_summary failed_count={} window_seconds=60",
            failedCount
        );
        failedCount = 0;
        lastReportTime = now;
    }
}

这样设置后,默认级别只记录一分钟内的失败摘要。需要调查具体问题时,再临时开启 DEBUG 查看单个失败事件。

审计记录、计费结果、安全事件以及无法从其他数据中恢复的失败信息,不应随意采样。

4. 避免关闭级别后仍执行昂贵计算

有些日志框架支持延迟格式化参数,但昂贵的对象转换仍可能在日志方法调用前执行。必要时应先检查对应级别是否启用:

if (logger.isDebugEnabled()) {
    logger.debug(
        "scheduler_state task_id={} state={}",
        taskId,
        buildDetailedStateSnapshot()
    );
}

优先使用参数化日志:

logger.debug("queue_depth component={} depth={}", componentName, queueDepth);

避免提前拼接字符串:

logger.debug("queue_depth component=" + componentName + " depth=" + queueDepth);

具体开销取决于使用的 logging framework,应针对实际版本和配置进行基准测试。

5. 设定资源预算

日志规范应规定资源预算,不应规定各级别所占的百分比。例如:

logging_budget:
  steady_state:
    max_bytes_per_second: <根据容量测试确定>
    max_cpu_overhead_percent: <根据性能目标确定>
  burst:
    max_duration_seconds: <根据存储和告警能力确定>
  retention:
    production_days: <根据业务、合规和成本要求确定>

尖括号中的数值必须通过实际负载测试确定,不能直接照搬其他系统的数据。测试需要覆盖正常流量、峰值流量、依赖故障和重试风暴。

推荐的发布策略

可以采用以下发布策略:

  1. 在代码中保留必要的 TRACE 和 DEBUG 日志点。
  2. 发布版本默认输出低频 INFO 以及全部有效的 WARN、ERROR。
  3. 支持按 component 动态调整日志级别,并尽量避免因调整级别而重启。
  4. 允许按 trace_id、用户、任务或实例限时开启诊断。
  5. 使用内存 ring buffer 保存最近一段时间的详细事件,在发生错误时导出,容量按内存预算确定。
  6. 对重复错误进行限速和聚合,保留首次出现时间、累计次数和最后一次出现时间。
  7. 设置日志轮转、压缩、保留期限和磁盘上限。
  8. 将日志开销纳入持续性能测试,防止新增日志点逐步突破预算。

需要特别注意的问题

日志中不要记录密码、访问令牌、私钥、完整 session、支付数据或不必要的个人信息。用户输入、协议报文和对象序列化结果也要设置长度上限,避免单个异常请求产生大量日志。

还应避免以下问题:

  • 在多个调用层重复记录同一个异常;
  • 将预期的输入错误记为 ERROR;
  • WARN 长期大量出现,却没有对应的告警或处理动作;
  • 只记录“操作失败”,没有 component、操作对象、错误原因和关联标识;
  • 为减少日志数量而删除关键上下文,导致剩余日志无法关联;
  • 使用日志记录所有指标。吞吐量、延迟、错误率和资源使用更适合交给 metrics 系统统计;
  • 使用日志代替审计记录。审计数据通常需要独立的完整性保障、访问控制和保留策略。

日志点应足以重建关键事件链,再通过级别、动态开关、关联标识、采样、聚合和容量限制控制实际输出。日志是否足够,应由故障演练和性能测试验证,不能根据固定比例推导。

备注:内容仅供参考。