一句话总结
日志回答排障的最后一问--"为什么".但只有结构化日志(机器可解析的键值对,而非供人阅读的自然语言句子)才能被高效检索,聚合与关联;再为每条日志附上 Trace ID,即可将日志与指标,追踪拼接为完整的故障分析链路.
前置回顾
第 01 篇确立了排障主线:指标发现"出事了",追踪定位"在哪个环节",日志归因"为什么";同时也指出了日志的特点--细节最丰富,但成本最高,结构最杂乱.阶段二已将指标这一支柱讲透.现在进入日志阶段:本篇先解决"日志本身应如何编写才能有效用于排障",第 05 篇再解决"海量日志如何存储与查询".
非结构化日志的局限
先看一条典型的传统日志:
|
人类阅读没有问题.但在排障过程中,实际需要回答的是以下问题:
- "过去 1 小时内,所有支付超时的错误共有多少条?"
- "用户 88291 今天所有的操作日志,按时间排序"
- "哪个服务的订单失败率最高?"
- "这条错误对应的那次请求,其完整调用链路是什么?"
面对上述纯文本日志,这些问题都难以高效回答--因为信息嵌入在自然语言句子中.机器要回答"所有支付超时",需要用正则表达式匹配 "payment gateway timeout" 这个字符串;要按用户聚合,需要从 "User 88291" 中解析数字.一旦日志格式不统一(不同开发者写法各异),正则匹配将难以维护.非结构化日志的根本缺陷在于:其设计目标是人类阅读,而非机器查询.
结构化日志:将句子拆解为字段
结构化日志(Structured Logging)的做法:不再拼接自然语言句子,而是输出机器可直接解析的键值对,通常采用 JSON 格式.同一条日志改写为:
|
上述问题现在全部转化为简单的字段过滤:
- "所有支付超时" →
error = "payment_gateway_timeout" - "用户 88291 的操作" →
user_id = "88291" - "按服务看失败率" →
group by service - "这次请求的完整链路" → 使用 trace_id 去追踪系统查询(详见下文)
非结构化日志如同一叠手写便签:每张写法不同,查找信息只能逐张翻阅,凭经验推断.
结构化日志如同填写规范的表格:字段对齐,列名统一,筛选,排序,统计均可通过查询语句直接完成.第 05 篇介绍的日志系统,本质上就是"能对这张表格执行查询的数据库".
核心原则:日志面向机器查询,而非面向人类阅读
这是从"传统日志"到"可观测性日志"最重要的观念转变.在分布式系统中,几乎不会逐行阅读日志--日志量巨大到根本无法人工通读.实际排障流程总是先查询和聚合(过滤出相关的几十条),再细看具体内容.
基于这一事实,日志的首要服务对象是查询引擎,而非人眼.结构化正是为高效查询而设计.
日志等级:控制信息密度的第一道过滤
日志等级(Log Level)决定一条日志的重要程度,是控制噪声的第一道机制.常用等级从轻到重:
| 等级 | 含义 | 典型用途 |
|---|---|---|
| DEBUG | 调试细节 | 开发期排查,生产环境一般关闭 |
| INFO | 正常的关键事件 | 服务启动,请求处理完成,关键状态变更 |
| WARN | 异常但可恢复 | 重试成功,降级,配置缺省回退 |
| ERROR | 出错,需要关注 | 请求失败,依赖不可用 |
| FATAL | 致命,进程即将终止 | 无法启动,不可恢复的崩溃 |
关键实践:生产环境的日志等级应当支持动态调整.日常运行时使用 INFO 级别,排查特定问题时临时切换至 DEBUG 获取详细数据,查完后恢复.这样既避免了日常被 DEBUG 日志充斥(同时也节省存储成本,详见第 05 篇),又能在需要时获取完整的调试信息.
两种常见的等级误用
- 所有日志都使用 INFO:将本属于 DEBUG 级别的细节也以 INFO 输出,生产日志被大量低价值信息淹没,真正重要的事件反而难以发现.
- 用 ERROR 记录"预期内的失败":例如用户输入错误密码属于正常业务流程,却以 ERROR 级别记录,导致 ERROR 告警频繁误触发,最终运维人员不再关注 ERROR 级别的告警--这是"告警疲劳"在日志层面的体现(第 08 篇将详细讨论).
日志等级应反映真实的严重程度,而非随意指定.
字段规范:团队需要统一的字段命名标准
结构化日志的能力建立在字段名统一的基础上.如果服务 A 使用 user_id,服务 B 使用 userId,服务 C 使用 uid,那么"按用户聚合所有服务的日志"这一操作将无法实现.团队需要制定一份字段命名约定.以下为几乎必须具备的核心字段:
| 字段 | 作用 |
|---|---|
timestamp |
时间(使用 ISO 8601/RFC3339 格式,带时区,避免本地格式) |
level |
日志等级 |
service |
产生日志的服务名称(微服务架构中至关重要) |
msg |
人类可读的简短描述 |
trace_id / span_id |
关联追踪信息(下一节详述) |
| 业务字段 | user_id / order_id 等,按约定命名 |
使用上下文注入避免重复编写
每条日志都需手动填写 service,trace_id 等字段吗?不必.成熟的日志库支持上下文注入(Context Injection):在请求入口处将 trace_id,user_id 等绑定到日志上下文(通常借助 Go 的 context,Java 的 MDC 等机制),此后该请求范围内的所有日志自动附带这些字段.
开发者只需编写业务相关信息,公共字段由框架统一注入--这也是保证字段一致性的工程手段.
Trace ID:连接三大支柱的关键纽带
这是本篇与整个系列衔接的核心章节.回到第 01 篇的排障场景:通过指标发现"下单延迟异常",通过追踪定位到"延迟发生在支付服务".现在需要查看"支付服务中那一次具体请求的日志细节"--即找到那次特定请求在支付服务中产生的日志.
如何查找?如果每条日志都带有 Trace ID(同一次请求在所有服务间共享的唯一标识,其生成与传播机制将在第 06 篇详述),问题就变得简单:
|
|
Trace ID 是缝合三大支柱的连接线
第 01 篇指出三支柱的痛点是"数据割裂,需依赖人工进行关联".Trace ID 正是将其串联起来的那根线:指标图表上的一个异常点 → 关联到具体的 trace_id → 使用同一个 trace_id 既能在追踪系统查看调用瀑布图,又能在日志系统查看每个调用跳的详细日志.
没有这根线,就只能通过"时间戳大致匹配"来猜测关联关系,效率极低.因此,"每条日志携带 trace_id"并非可选优化,而是现代可观测性的基本要求--第 08 篇将进一步展开这一主题.
采样与成本:日志的取舍策略
第 01 篇指出日志"成本最高".具体来说,一个中等规模的系统每天产生TB 级别的日志是常见情况.全量存储并建立全量索引的成本非常可观(第 05 篇将看到这是 EFK 方案的主要痛点).因此需要有明确的取舍策略:
- 提高日志等级:生产环境关闭 DEBUG 级别,是最直接的减量手段
- 采样(Sampling):对高频,重复性日志(如健康检查,正常的 INFO 日志),仅保留一部分.但错误日志(ERROR)绝不能采样--它们数量稀少且信息密度极高,丢失一条即可能丢失关键线索
- 分级存储:近期日志存放在可快速查询的热存储中,历史日志归档至低成本的冷存储(对象存储)
采样的基本原则:保留异常,稀释正常
采样并非"无差别地丢弃一半数据".正确的策略是按信息价值区别对待:正常路径的日志(成功请求,健康检查)信息密度低且量大,可以大胆采样;错误,慢请求,异常路径的日志信息密度极高,应全量保留.
这一"保留异常,稀释正常"的思路,在第 07 篇追踪的"尾部采样(Tail-Based Sampling)"中将再次出现--它是可观测性应对海量数据的通用策略.
快速回顾
- 非结构化日志的缺陷:面向人类阅读,信息嵌入在句子中,机器难以过滤/聚合/关联
- 结构化日志:输出机器可解析的键值对(JSON),将"阅读句子"转变为"查询字段";核心原则--日志面向机器查询,而非面向人类阅读
- 日志等级反映真实严重程度,生产环境应支持动态调整;避免所有日志使用 INFO 级别,避免用 ERROR 记录预期内的业务失败
- 字段规范:团队统一"列名"(timestamp/level/service/msg/trace_id + 业务字段),通过上下文注入保证一致性并减少重复编写
- Trace ID 是连接三支柱的纽带:每条日志携带 trace_id,才能从指标异常→追踪定位→日志细节完整贯通,这是基本要求
- 成本与采样:日志成本最高;通过调整等级,采样和分级存储进行控制;采样基本原则是保留异常,稀释正常,ERROR 级别日志绝不采样
结构化日志采用 JSON 格式,人类直接阅读不如纯文本方便,本地开发时如何处理?
这是一个真实的使用体验权衡,解决方案是按环境区分输出格式:本地开发使用人类友好的彩色文本格式(多数日志库支持通过配置切换为 console 输出),方便边编写边查看;生产/测试环境输出 JSON,提供给日志系统处理.同一套日志代码,通过配置切换输出格式即可,无需在两种需求之间做出妥协.
日志,指标,追踪在信息上似有重叠,能否仅使用日志并从日志中计算出指标?
技术上可行(从日志聚合出 QPS,错误率),部分系统也确实采用了这一方案.但需要权衡:基于日志计算指标成本高,延迟大(需处理海量文本),远不如专用指标系统(Prometheus)那样低成本,实时性强.反之,指标也无法替代日志提供的细节信息.务实的做法仍是各取所长:指标用于高频实时的健康度监控,日志用于低频深入的根因归因,两者通过 trace_id 关联.第 01 篇提及的"以结构化事件统一数据底座"是更前沿的方向,但就工程成熟度和成本而言,当前大多数团队仍维持三支柱的分工格局.
动手练习
- 改写传统日志为 JSON:从项目中选取一段传统文本日志,手动将其改写为结构化 JSON,标注哪些信息被提取为独立字段.
- 使用结构化日志库:使用所用语言的结构化日志库(Go 的 slog/zap,Python 的 structlog 等)输出若干条 JSON 格式日志,确认字段对齐.
- 实现上下文注入:在请求入口绑定一个
request_id,使该请求范围内的所有日志自动附带该字段,验证无需每行手动编写. - 验证 Trace ID 串联:为日志添加
trace_id字段(先用任意值模拟),然后使用grep或jq按 trace_id 过滤出"同一次请求"的所有日志,体会 Trace ID 串联的效果. - 审视日志等级使用:审视服务中日志等级的使用情况:是否存在"所有日志使用 INFO"或"用 ERROR 记录正常业务失败"的情况?列出应当调整等级的条目.