线上出问题,最痛苦的不是「bug 本身」,而是**「看不到全貌」**:日志是碎片的、没有上下文、跨服务断掉。可观测性三支柱——日志、追踪、指标——协同才能快速定位。本文从 Scala 服务视角讲透三支柱:结构化日志怎么做(JSON + 字段规范)、链路追踪怎么贯穿异步(OpenTelemetry + trace 上下文)、指标怎么采(Micrometer + Prometheus)、以及三支柱如何与 Effect/Fiber 的异步世界无缝集成。
前置:/scala-microservices-practice/(服务部署与可观测性)、/scala-functional-effects/(IO/Fiber 异步)、/scala-performance-jvm/(JFR 与诊断)、/scala-build-tooling/(运行环境)。
目录
- 1. 可观测性三支柱:日志、追踪、指标的分工
- 2. 结构化日志:JSON 输出与字段规范
- 3. SLF4J/Logback 工程实践
- 4. 链路追踪:trace、span 与上下文传播
- 5. OpenTelemetry 集成
- 6. 追踪与 Fiber 的异步贯穿
- 7. 指标采集:Micrometer 与 Prometheus
- 8. 采样、脱敏与告警联动
- 9. 三支柱协同:一个故障定位案例
- 10. 速查表与一句话记忆
- 延伸阅读
1. 可观测性三支柱:日志、追踪、指标的分工
三支柱各管一段,配合才是完整图景:
日志(Log):发生了什么(事件)
散落的、带上下文的、可搜索的事件流
→ 回答「发生了什么、何时」
追踪(Trace):一次请求的完整路径
trace/span 串联跨服务调用
→ 回答「哪个环节慢、哪次调用失败」
指标(Metric):系统的数字脉搏
延迟/吞吐/错误率/资源的聚合数字
→ 回答「系统健康吗、趋势如何」
协同:
指标告警 → 发现异常 → 追踪定位到服务/环节 → 日志看细节
工程要点:三支柱是**「分层定位」**——指标告诉你「哪里不对劲」,追踪告诉你「哪个环节」,日志告诉你「具体发生了什么」。Scala 工程实践上,三支柱都基于「上下文贯穿」(requestId/traceId)才能串起来。
2. 结构化日志:JSON 输出与字段规范
「人能看懂的日志」其实机器读不懂,要结构化:
结构化日志 = JSON 一行一条:
{ "time": "...", "level": "WARN", "logger": "...",
"message": "...", "requestId": "...", "userId": "...",
"durationMs": 123, "status": 500 }
字段规范(统一才有聚合价值):
□ 必带:time/level/message/logger
□ 贯穿:requestId/traceId/userId(跨日志关联)
□ 业务:自定义业务字段(订单号、操作类型)
□ 性能:不要打大量高基数字段(如每次输出时间戳的秒值)
反模式:
✗ 散落字符串拼接:log.info("user " + id + " failed")
✓ 结构化字段:log.info("user failed", "userId" -> id)
工程要点:日志是给机器读的(搜索/聚合),JSON + 统一字段是前提。贯穿字段(requestId/traceId/userId)是跨日志、跨服务关联的粘合剂——没有它们,日志只是一堆孤立的行。
3. SLF4J/Logback 工程实践
Scala 服务的主流日志栈是 SLF4J + Logback:
SLF4J:日志门面(统一 API)
Logback:实现(配置灵活)
Logstash/logstash-logback-encoder:JSON 编码器
Logback 配置要点:
□ appender:Console(本地)+ File/JSON(生产)
□ 级别:dev=DEBUG,prod=INFO(WARN/ERROR 进告警)
□ 滚动:按大小/时间滚动,保留 N 天
□ 异步:AsyncAppender 提升吞吐(注意丢日志风险)
MDC(Mapped Diagnostic Context):
□ 线程本地键值 → 每条日志自动带上下文(requestId/userId)
□ 关键:异步线程要手动传递 MDC(见第 6 节)
import org.slf4j.MDC
MDC.put("requestId", requestId)
log.warn("balance check failed", Map("userId" -> uid))
MDC.remove("requestId")
工程要点:SLF4J + Logback + JSON 编码器是标配。MDC 让「上下文自动进日志」——但异步(Fiber/线程池)必须显式传递 MDC,这是 Scala 异步服务最容易漏的一环。
4. 链路追踪:trace、span 与上下文传播
链路追踪把「一次请求跨服务」串成一条链:
核心概念:
□ trace:一次用户请求的完整调用树
□ span:调用树里的一步(服务内部/跨服务调用)
□ traceId:整条链共享
□ parentId:父子关系(嵌套/跨服务)
□ span 属性:url/状态/耗时/错误
传播机制:
□ 服务 A 调 B:A 生成 span → 注入 traceId/parentId 到请求头
□ B 收到:从请求头提取 → 续接 parent → 记录 span
□ 标准头:traceparent(W3C)/ b3
跨进程传播工具:
□ 客户端注入:OkHttp/Http4s 的 OTel instrumentation
□ 消息队列:消息头带 trace 上下文(Kafka/AMQP)
span 示例:
GET /timeline (trace=ab12)
├─ DB query (span, 45ms)
├─ Redis get (span, 2ms)
└─ 调用推荐服务 (span, 120ms, HTTP)
工程要点:追踪的价值在**「跨服务串链」**——HTTP 头带 traceId/parentId,服务边界续接 span。没有传播机制,追踪只能在单服务内断掉。HTTP 客户端、DB、MQ 的 instrumentation 是落地的关键。
5. OpenTelemetry 集成
OpenTelemetry(OTel)是链路追踪与指标的事实标准:
OTel 组件:
□ SDK:Tracer/Meter/Logger 统一 API
□ Instrumentation:自动埋点(HTTP/DB/Redis/消费者)
□ Exporter:发到后端(Jaeger/Tempo/Grafana/OTLP)
□ Context:trace 上下文在线程内传播
Scala 侧集成:
□ OpenTelemetry Java SDK(Java/Scala 通用)
□ http4s / cats-effect 有 OTel 集成(Http4s OTel、Cats effect otel)
□ ZIO:zio-opentelemetry(原生 fiber 上下文贯穿)
关键点:
□ 自动埋点覆盖大部分「框架层调用」
□ 业务关键路径可手动加 span(定义业务语义)
□ 采样策略在 Exporter 层控制(见第 8 节)
// 手动 span(业务语义)
val span = tracer.spanBuilder("create-post").startSpan()
try { doWork() } finally { span.end() }
工程要点:OTel 是**「统一埋点 + 统一导出」**的标准层——自动埋点覆盖框架,手动 span 补充业务语义。选型时优先「与 Effect 生态集成良好」的实现(http4s/ZIO 都有官方或社区方案)。
6. 追踪与 Fiber 的异步贯穿
Scala 服务的异步(Fiber/线程池)是追踪最容易断的地方:
问题:Fiber 切换线程 → trace 上下文(线程本地)丢失
→ 异步代码的 span 断了、日志的 traceId 没了
解决:
□ Cats Effect IO:io.cede / parTraverse 会跨线程
→ 需要「上下文贯穿」:把 trace 上下文放进 fiber 环境
□ OTel Java agent:支持增强(自动传递)
□ 纯函数式方案:把上下文放进 Effect 环境(ZIO Environment / IOLocal)
IOLocal(Cats Effect):
IO 的「本地上下文」→ 随 fiber 传递,天然跨线程
→ 用来携带 requestId/traceId 比 MDC 更安全
MDC 在异步的替代:
□ 不用线程本地存上下文,用 Effect 环境/IOLocal
□ 日志输出时从上下文读取,写入 MDC 或 JSON 字段
import cats.effect.IOLocal
val requestId: IOLocal[String] = IOLocal.unsafe("unknown")
requestId.set("req-123").flatMap { _ =>
ioParTraverse(jobs) // 跨线程,但 requestId 随 fiber 传递
}
工程要点:异步世界的上下文要「随 fiber 走」,不能靠线程本地——用 IOLocal(CE)/ Environment(ZIO)携带 traceId/requestId,跨线程不丢。这是 Scala 异步服务可观测性最容易踩、也最值得做对的点。
7. 指标采集:Micrometer 与 Prometheus
指标(Metrics)用 Micrometer + Prometheus 采集:
Micrometer:指标门面(统一 Meter API)
Prometheus:时序存储 + 查询
Grafana:可视化 + 告警
常用指标:
□ 业务:请求 QPS、错误率、处理时长(histogram)
□ 资源:JVM 内存/GC、连接池、线程池
□ 依赖:下游调用延迟/错误
Counter/Histogram/Gauge:
□ Counter:累加(请求数、错误数)
□ Histogram:分布(延迟 P50/P99)
□ Gauge:瞬时值(连接数、队列深度)
import io.micrometer.core.instrument.Metrics
val reqTimer = Metrics.timer("http.requests", "path" -> "/timeline")
val t = reqTimer.start()
try { handle() } finally { t.stop() }
// Prometheus 抓取:/metrics 端点暴露
指标命名与标签:
□ 命名:点分(http.requests, db.query.time)
□ 标签:path/status/type(注意标签基数别爆炸)
□ 基数控制:不要用 userId 当标签(高基数 → 存储爆炸)
工程要点:指标的准则是**「少数、有标签、基数受控」**——业务指标用 Timer/Counter/Histogram,标签控制基数(不用 userId 当标签)。Micrometer 统一门面 + Prometheus 存储 + Grafana 告警是主流标配。
8. 采样、脱敏与告警联动
可观测性的「成本与安全」控制:
采样(Trace):
□ 全量采样成本高 → 按比例采样(10% 默认)
□ 重要服务/错误 span 必采(错误永远采,不采样本)
□ 尾采样:聚合后按「整条 trace 是否异常」决定保留
脱敏:
□ 日志:password/token/身份证 → 脱敏(正则替换)
□ trace 属性:请求体/响应体默认不打(隐私)
□ 指标标签:不携带敏感维度
告警联动:
□ 指标告警(p99 超阈、错误率超阈)→ 触发
□ 追踪入口查样本 → 定位环节
□ 日志查 requestId → 看细节
□ 告警要带「跳转链接」(到 trace/日志)
采样策略示例:
默认 10% 采样;ERROR span 100% 保留
→ 成本 10% × 正常流量,但所有故障 100% 可追踪
工程要点:可观测性是**「有成本的,要控制采样;有风险的,要脱敏」**——错误必采、正常按比例采样,敏感字段脱敏。告警要「联动直达」——指标告警 → trace → 日志,一条链路点过去,不让人在多个系统间手动找。
9. 三支柱协同:一个故障定位案例
一次真实的故障定位演示三支柱如何协同:
场景:接口 /timeline P99 从 200ms 飙到 2s,用户投诉
第 1 步(指标):
告警:http.requests.p99 /timeline > 1s
→ 明确「接口慢」,时间窗口定位到 14:02 起
第 2 步(追踪):
打开该接口的 trace 样本
→ 发现「推荐服务调用 span 平均 1.4s」→ 定位到下游
第 3 步(日志):
用 traceId 过滤推荐服务的日志
→ 发现「Redis 连接池超时」重复 WARN
→ 根因:Redis 连接池被某批慢查询占满
第 4 步(修复 + 验证):
扩大连接池 + 加慢查询告警
→ 指标回落 p99 250ms,日志不再 WARN
工程要点:三支柱协同的路径是**「指标发现 → 追踪定位 → 日志确认」**——每一步都基于贯穿的 traceId/requestId 无缝衔接。如果没有追踪与日志的上下文贯穿,第 2 步就会卡住,只能靠人肉猜。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| 三支柱分工 | 日志看事件、追踪看路径、指标看健康 |
| 日志怎么打 | JSON 结构化 + 统一字段 + 贯穿 requestId |
| 日志栈用什么 | SLF4J + Logback + JSON 编码器 |
| 追踪怎么串 | trace/span + 请求头传播 |
| 标准是什么 | OpenTelemetry(统一埋点/导出) |
| 异步怎么贯穿 | IOLocal/Environment 随 fiber 传递 |
| 指标怎么采 | Micrometer + Prometheus + Grafana |
| 采样脱敏 | 错误必采、敏感脱敏 |
| 怎么定位 | 指标 → 追踪 → 日志 联动 |
一句话记忆:Scala 可观测性 = 三支柱协同(指标发现/追踪定位/日志确认)+ 结构化日志(JSON + 贯穿字段)+ OTel 追踪(trace/span 跨服务传播)+ IOLocal 异步贯穿(上下文随 fiber 走)+ Micrometer 指标(基数受控)+ 采样脱敏告警联动——把「看日志猜问题」升级为「一条链路点过去的确定性定位」。
延伸阅读
- /scala-microservices-practice/ — 服务部署与可观测性集成
- /scala-functional-effects/ — IOLocal 与 Fiber 上下文
- /scala-performance-jvm/ — JFR 与 JVM 诊断
- /scala-configuration-feature-flags/ — 配置与告警联动
- /scala-testing-practice/ — 可观测性的测试
- 可观测性专题 — OTel 与监控体系
- DevOps 专题 — 告警与日志平台
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。