可观测性:分布式日志系统
日志是带时间和上下文的离散事件记录。分布式日志系统负责把不同主机、容器和服务产生的日志统一采集、处理、存储和查询,使开发者能够从分散的局部记录中还原故障现场。
日志的目标不是“记录得越多越好”,而是在可接受的成本和安全边界内,为排障、审计和业务分析保留足够证据。
分布式日志为什么更难
- 来源分散:同一个请求会跨越多个进程、主机、集群和区域。
- 时钟不完全一致:机器时间偏差会让日志展示顺序与因果顺序不同。
- 请求需要关联:必须用
trace_id、业务 ID 等字段关联上下文。 - 吞吐具有突发性:故障时通常正是日志量最大、管道压力最高的时候。
- 消息可能重复或丢失:Agent 重试、容器退出和存储限流都会影响投递。
- 数据具有敏感性:日志可能无意记录 Token、密码、个人信息和内部拓扑。
因此,日志时间戳用于缩小范围,Trace 上下文用于表达请求关联;不能只依赖肉眼按时间拼接调用链。
一条日志应该包含什么
优先输出结构化 JSON,而不是把所有内容拼成一句文本。字段可按下面四类组织:
| 类型 | 推荐字段 | 说明 |
|---|---|---|
| 时间与级别 | timestamp、severity_text | 使用带时区的 ISO 8601 时间 |
| 资源身份 | service.name、service.version、deployment.environment.name、cloud.region | 描述是谁产生了日志 |
| 执行上下文 | trace_id、span_id、request_id | 关联一次请求和 Trace |
| 事件与业务 | event.name、message、order.id、error.type | 描述发生了什么 |
示例:
{
"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 Collector 与 Grafana Loki:Ingesting Logs Using Alloy
小规模系统不一定需要独立消息队列,可以由采集器直接写后端。只有在流量突发、跨区域传输、多个下游订阅或后端经常限流时,额外缓冲才可能值得其运维成本。
应用输出
- 容器应用通常写
stdout/stderr,由平台负责文件落盘、轮转和采集。 - 不要让业务线程同步等待远程日志后端,否则可观测系统故障会拖垮业务。
- 多行堆栈应在采集阶段正确合并,结构化日志则尽量保持“一行一事件”。
采集与处理
采集器负责追踪文件位置、解析格式、补充 Kubernetes 或云资源属性,并在出口失败时批处理和重试。要明确本地缓冲的大小、磁盘上限和溢出策略。
OpenTelemetry Collector、Grafana Alloy、Fluent Bit 和 Vector 属于采集或处理层,不是日志存储后端。
传输语义
日志管道通常只能在成本和可靠性之间取舍:
- 进程内异步缓冲延迟低,但进程崩溃时可能丢失未刷新数据。
- 磁盘队列更可靠,但要处理容量、损坏和 I/O 竞争。
- 至少一次投递可以降低丢失概率,但重试可能产生重复日志。
- 端到端“绝不丢失且绝不重复”成本很高,不能因为使用了 Kafka 就默认成立。
图示参考:OpenTelemetry Collector Resiliency 与 OpenTelemetry Logging Specification
对审计日志等强可靠数据,应把持久化、访问控制、完整性检测和归档作为独立需求,而不是与普通调试日志共用默认策略。
常见存储方案如何选择
| 方案 | 数据组织与查询特点 | 更适合 |
|---|---|---|
| Elasticsearch / OpenSearch | 倒排索引和全文检索能力强,字段查询灵活 | 复杂检索、已有 Elastic 生态 |
| Loki | 只索引流标签,日志内容在查询时过滤 | 已使用 Grafana、标签维度稳定、控制索引成本 |
| ClickHouse | 列式存储,SQL 聚合与压缩能力强 | 大规模结构化日志分析、自建数据平台 |
| 托管日志服务 | 与云权限、告警和基础设施集成紧密 | 希望减少自建运维、接受平台成本与绑定 |
选型前先用真实数据验证:
- 每日写入量、峰值吞吐和平均日志大小;
- 常用查询是全文关键词、字段过滤还是聚合分析;
- 查询时间范围和可接受的响应时间;
- 热数据、冷数据的保留周期与恢复要求;
- 多租户隔离、访问审计和总成本。
Loki 标签的常见误区
Loki 的标签用于定位日志流,应选择值集合有限且长期稳定的字段,例如 service_name、namespace、region。不要把 trace_id、order_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 也可能访问多个业务实体。
故障排查的查询顺序
面对一次线上错误,可以按以下顺序收敛:
- 用时间范围、环境、服务和版本找到候选日志。
- 从一条错误日志取得
trace_id,查看完整调用路径。 - 回到慢 Span 对应的日志,检查业务参数、重试和异常类型。
- 用业务 ID 查找异步后续事件,确认最终状态。
- 对照发布和配置变更事件,验证问题是否与版本相关。
如果只能全文搜索错误字符串,说明日志数据模型仍然不足;如果能找到错误却关联不到 Trace,优先检查上下文注入和异步传播。
上线检查清单
- [ ] 日志采用稳定的结构化格式,所有服务统一时间和级别字段。
- [ ] 服务名、版本、环境、区域等资源属性可过滤。
- [ ] Trace 激活时自动注入
trace_id和span_id。 - [ ] 已定义敏感字段清单,并在应用或采集器侧过滤。
- [ ] 文件轮转、内存/磁盘队列、重试和溢出策略有明确上限。
- [ ] 采集器拒收、丢弃、导出失败和队列饱和有独立监控。
- [ ] 保留周期、访问权限和删除流程符合业务与合规要求。
- [ ] 用后端不可用、磁盘写满和重复投递场景做过演练。
学习自测
- 为什么写入
stdout不等于日志已经可靠进入中央存储? - 至少一次投递对日志查询会带来什么副作用?
- 为什么
trace_id适合保存在日志字段中,却不适合做 Loki 流标签? - 审计日志与普通 DEBUG 日志为什么不应使用完全相同的策略?
参考资料
- OpenTelemetry Specification:Logs
- OpenTelemetry:Collector Resiliency
- OpenTelemetry:Service Semantic Conventions
- Elastic:ECS Logging
- Grafana Loki:Understand Labels
- Grafana Loki:Ingesting Logs Using Alloy
- OWASP Cheat Sheet Series:Logging Cheat Sheet