Skip to content

可观测性:分布式日志系统

日志是带时间和上下文的离散事件记录。分布式日志系统负责把不同主机、容器和服务产生的日志统一采集、处理、存储和查询,使开发者能够从分散的局部记录中还原故障现场。

日志的目标不是“记录得越多越好”,而是在可接受的成本和安全边界内,为排障、审计和业务分析保留足够证据。

分布式日志为什么更难

  • 来源分散:同一个请求会跨越多个进程、主机、集群和区域。
  • 时钟不完全一致:机器时间偏差会让日志展示顺序与因果顺序不同。
  • 请求需要关联:必须用 trace_id、业务 ID 等字段关联上下文。
  • 吞吐具有突发性:故障时通常正是日志量最大、管道压力最高的时候。
  • 消息可能重复或丢失:Agent 重试、容器退出和存储限流都会影响投递。
  • 数据具有敏感性:日志可能无意记录 Token、密码、个人信息和内部拓扑。

因此,日志时间戳用于缩小范围,Trace 上下文用于表达请求关联;不能只依赖肉眼按时间拼接调用链。

一条日志应该包含什么

优先输出结构化 JSON,而不是把所有内容拼成一句文本。字段可按下面四类组织:

类型推荐字段说明
时间与级别timestampseverity_text使用带时区的 ISO 8601 时间
资源身份service.nameservice.versiondeployment.environment.namecloud.region描述是谁产生了日志
执行上下文trace_idspan_idrequest_id关联一次请求和 Trace
事件与业务event.namemessageorder.iderror.type描述发生了什么

示例:

json
{
  "timestamp": "2026-07-30T10:15:32.482Z",
  "severity_text": "INFO",
  "message": "订单创建成功",
  "event.name": "order.created",
  "service.name": "order-service",
  "service.version": "2.3.1",
  "deployment.environment.name": "production",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "order.id": "ord_8f72d1"
}

字段名应在团队内保持稳定。OpenTelemetry Log Data Model、Semantic Conventions 或 Elastic Common Schema 都可以作为起点;选定一种主规范后再映射到后端,不要让每种语言自行发明字段。

日志级别与事件选择

级别使用建议例子
ERROR当前操作失败,需要调查或触发告警候选数据写入最终失败
WARN操作已恢复或出现退化,需要关注趋势第一次调用超时,重试成功
INFO低频、关键的业务或生命周期事件发布完成、订单状态变更
DEBUG开发诊断细节,生产环境按需短时开启规则分支和中间计算结果

不要在每一层捕获同一异常后重复打印完整堆栈。通常由最了解业务结果的一层记录一次错误,上层补充状态,下层用 Span 事件或受控的 DEBUG 信息解释细节。

适合记录的事件包括:

  • 服务启动、关闭和配置版本变化;
  • 关键业务状态转换;
  • 外部依赖失败、重试和熔断状态变化;
  • 权限、认证和高风险管理操作;
  • 无法通过指标聚合表达的异常上下文。

不应直接记录密码、访问令牌、会话标识、连接串、密钥和不必要的个人敏感信息。确有排障需要时,应先做删除、掩码、哈希或加密,并配合最小权限和保留周期。

典型日志管道

图示参考:OpenTelemetry CollectorGrafana Loki:Ingesting Logs Using Alloy

小规模系统不一定需要独立消息队列,可以由采集器直接写后端。只有在流量突发、跨区域传输、多个下游订阅或后端经常限流时,额外缓冲才可能值得其运维成本。

应用输出

  • 容器应用通常写 stdout / stderr,由平台负责文件落盘、轮转和采集。
  • 不要让业务线程同步等待远程日志后端,否则可观测系统故障会拖垮业务。
  • 多行堆栈应在采集阶段正确合并,结构化日志则尽量保持“一行一事件”。

采集与处理

采集器负责追踪文件位置、解析格式、补充 Kubernetes 或云资源属性,并在出口失败时批处理和重试。要明确本地缓冲的大小、磁盘上限和溢出策略。

OpenTelemetry Collector、Grafana Alloy、Fluent Bit 和 Vector 属于采集或处理层,不是日志存储后端。

传输语义

日志管道通常只能在成本和可靠性之间取舍:

  • 进程内异步缓冲延迟低,但进程崩溃时可能丢失未刷新数据。
  • 磁盘队列更可靠,但要处理容量、损坏和 I/O 竞争。
  • 至少一次投递可以降低丢失概率,但重试可能产生重复日志。
  • 端到端“绝不丢失且绝不重复”成本很高,不能因为使用了 Kafka 就默认成立。

图示参考:OpenTelemetry Collector ResiliencyOpenTelemetry Logging Specification

对审计日志等强可靠数据,应把持久化、访问控制、完整性检测和归档作为独立需求,而不是与普通调试日志共用默认策略。

常见存储方案如何选择

方案数据组织与查询特点更适合
Elasticsearch / OpenSearch倒排索引和全文检索能力强,字段查询灵活复杂检索、已有 Elastic 生态
Loki只索引流标签,日志内容在查询时过滤已使用 Grafana、标签维度稳定、控制索引成本
ClickHouse列式存储,SQL 聚合与压缩能力强大规模结构化日志分析、自建数据平台
托管日志服务与云权限、告警和基础设施集成紧密希望减少自建运维、接受平台成本与绑定

选型前先用真实数据验证:

  • 每日写入量、峰值吞吐和平均日志大小;
  • 常用查询是全文关键词、字段过滤还是聚合分析;
  • 查询时间范围和可接受的响应时间;
  • 热数据、冷数据的保留周期与恢复要求;
  • 多租户隔离、访问审计和总成本。

Loki 标签的常见误区

Loki 的标签用于定位日志流,应选择值集合有限且长期稳定的字段,例如 service_namenamespaceregion。不要把 trace_idorder_id、用户 ID、时间戳或 Pod 唯一标识全部做成标签,否则会产生高基数和大量碎片化日志流。

高基数字段仍可以保留在日志正文或 structured metadata 中,需要时再过滤。

Trace、日志与业务 ID 如何关联

  • trace_id:关联同一次分布式请求中的日志和 Span。
  • span_id:定位日志发生在哪个具体操作内。
  • request_id:网关或应用自己的请求 ID,可保留用于兼容旧系统。
  • 业务 ID:如订单号,适合查询跨越多个异步流程的业务生命周期。

同步调用通常传播当前 Trace Context;消息队列还要把上下文写入消息属性。消费端如果启动新的 Trace,可以通过 Span Link 保留与生产端的因果关系。业务 ID 不应替代 Trace ID,因为一次业务流程可能包含多条 Trace,一条 Trace 也可能访问多个业务实体。

图示参考:OpenTelemetry Logging:Log Correlation

故障排查的查询顺序

面对一次线上错误,可以按以下顺序收敛:

  1. 用时间范围、环境、服务和版本找到候选日志。
  2. 从一条错误日志取得 trace_id,查看完整调用路径。
  3. 回到慢 Span 对应的日志,检查业务参数、重试和异常类型。
  4. 用业务 ID 查找异步后续事件,确认最终状态。
  5. 对照发布和配置变更事件,验证问题是否与版本相关。

如果只能全文搜索错误字符串,说明日志数据模型仍然不足;如果能找到错误却关联不到 Trace,优先检查上下文注入和异步传播。

上线检查清单

  • [ ] 日志采用稳定的结构化格式,所有服务统一时间和级别字段。
  • [ ] 服务名、版本、环境、区域等资源属性可过滤。
  • [ ] Trace 激活时自动注入 trace_idspan_id
  • [ ] 已定义敏感字段清单,并在应用或采集器侧过滤。
  • [ ] 文件轮转、内存/磁盘队列、重试和溢出策略有明确上限。
  • [ ] 采集器拒收、丢弃、导出失败和队列饱和有独立监控。
  • [ ] 保留周期、访问权限和删除流程符合业务与合规要求。
  • [ ] 用后端不可用、磁盘写满和重复投递场景做过演练。

学习自测

  1. 为什么写入 stdout 不等于日志已经可靠进入中央存储?
  2. 至少一次投递对日志查询会带来什么副作用?
  3. 为什么 trace_id 适合保存在日志字段中,却不适合做 Loki 流标签?
  4. 审计日志与普通 DEBUG 日志为什么不应使用完全相同的策略?

参考资料