【架构实战】可观测性三支柱实战:Metrics、Logging、Tracing 如何统一落地
上午我们聊了全链路追踪,用 OpenTelemetry 把每一次调用串成一条 Trace。但单靠 Trace 是不够的------Trace 回答"这一次请求为什么慢",Metrics 回答"系统整体水位怎么样",Logging 回答"具体报了什么错"。这三者合起来,才叫可观测性(Observability)。
很多团队踩过的坑是:Prometheus 一套、ELK 一套、SkyWalking 一套,三套系统各玩各的,指标对不上、日志串不起、链路查不到根因。排查一个问题要在三个控制台之间来回跳,光"对时间"就能对半小时。
今天聊清楚:可观测性三支柱到底是什么、怎么选型、以及如何用 OpenTelemetry 把它们统一到一条技术栈里。
一、三支柱:谁解决什么问题
1.1 Metrics(指标):系统健康度
Metrics 是聚合后的数值,回答"系统现在怎么样":
- 每秒请求数(QPS)、错误率、延迟分位数(P50/P95/P99)
- CPU、内存、磁盘、网络
- 队列堆积量、连接池使用率、GC 暂停时间
特点:数据量小、可聚合、可告警。适合做 dashboard 和告警规则,但看不到"具体哪一次请求"。
1.2 Logging(日志):事件详情
Logging 是离散的事件记录,回答"到底发生了什么":
- 业务日志:下单成功、支付回调、库存扣减
- 异常日志:Exception 堆栈、SQL 执行失败
- 访问日志:谁在什么时间调了什么接口
特点:信息最丰富、最细粒度,但数据量巨大,且非结构化(或半结构化),检索需要索引。
1.3 Tracing(追踪):调用链路
Tracing 是跨服务的调用关系记录,回答"一次请求经过了哪些服务、每步花了多久":
- Trace ID 贯穿所有服务
- 每个 Span 记录一个操作的耗时、状态、属性
- 还原完整的调用树,定位最慢环节
特点:结构清晰、跨服务串联,但采样成本高,无法记录所有请求的所有细节。
1.4 三者的关系:一个经典例子
用户反馈"下单很慢"。三种工具各自怎么用:
- Metrics 先定位:查 Prometheus,发现下单接口 P99 从 200ms 涨到 2s,错误率 3%。确认问题存在,范围在订单服务。
- Tracing 找环节 :查 Trace,发现
order-service调用inventory-service的 Span 耗时 1.8s,其他环节都正常。定位到库存服务。 - Logging 查根因 :查库存服务的日志,发现 Redis 连接池获取超时,堆栈指向
JedisConnectionException: timeout。根因是连接池配置过小 + Redis 抖动。
三者是递进关系,缺一不可。只靠日志,你得猜是哪个服务慢;只靠指标,你不知道具体哪次请求;只靠 Trace,你不知道底层报了什么错。
二、选型:主流方案对比
2.1 Metrics 领域
| 方案 | 特点 | 适合场景 |
|---|---|---|
| Prometheus + Grafana | 拉模式、PromQL 强大、生态最全 | 事实标准,K8s 环境首选 |
| VictoriaMetrics | Prometheus 兼容、单机性能强、存储省 | 大规模指标、长期存储 |
| Thanos / Mimir | Prometheus 高可用 + 长期存储 | 多集群、多租户场景 |
| InfluxDB | 写模型不同、持续查询 | 时序分析类需求(较少用于 K8s) |
2.2 Logging 领域
| 方案 | 特点 | 适合场景 |
|---|---|---|
| ELK(Elasticsearch + Logstash + Kibana) | 全文检索强、生态成熟 | 经典方案,中小规模首选 |
| EFK(Fluentd/Fluent Bit + ES) | 采集轻量、配置灵活 | K8s 环境常用 |
| Loki + Grafana | 只索引标签不索引内容、成本低 | 与 Prometheus 共用 Grafana 的团队 |
| ClickHouse 自建 | 写入极快、压缩比高 | 日志量特别大的场景 |
2.3 Tracing 领域
| 方案 | 特点 | 适合场景 |
|---|---|---|
| Jaeger | OTel 原生支持、轻量 | 中小团队快速落地 |
| Zipkin | 老牌、简单 | 历史项目迁移成本低 |
| SkyWalking | 无侵入 Agent、中文文档全 | Java 系、不想改代码的团队 |
| Grafana Tempo | 与 Grafana 生态深度集成 | 已用 Grafana 全家桶的团队 |
2.4 关键判断:到底选几套?
很多团队的误区是"Metrics 选 Prometheus、Logging 选 ELK、Tracing 选 SkyWalking,都是各自领域最强的"------然后发现三套系统的数据互相割裂:
- 告警说 QPS 暴涨,但 Trace 里查不到对应时段的数据(采样率不同)
- 日志里有关联的 requestId,但 Tracing 用另一套 traceId,对不上
- 三个 UI 三套查询语法,值班同学要记三本手册
正确思路是:优先选能"统一"的栈。要么全上 Grafana 系(Prometheus + Loki + Tempo),要么全上 OTel 生态(统一 exporter + collector,后端按需接)。
三、统一的关键:OpenTelemetry
OpenTelemetry(OTel)已经成了可观测性的事实标准------它用一套 SDK 统一了 Metrics、Logging、Tracing 三大信号的采集,通过 OTLP 协议发给后端。
3.1 OTel 的架构
应用 (Java/Go/Python/Node...)
│ OTel SDK 采集三大信号
▼
OTel Collector (agent 模式 / 独立部署)
│ OTLP / Prometheus / Jaeger / 其他协议
▼
后端存储 + 可视化 (Prometheus+Grafana / Tempo / Loki / ES...)
核心组件:
- OTel SDK:打进应用里的库,自动或手动埋点,生成 Metrics、Logs、Traces 三种数据
- OTel Collector:独立的数据管道服务,接收、处理、转发遥测数据,支持路由、采样、脱敏、批处理
- OTLP 协议:三种信号统一走这个协议传输,一套网络、一套鉴权
3.2 三大信号如何关联
OTel 的杀手锏是通过 Context 关联三种信号:
- 每个请求生成
trace_id和span_id - 日志里自动注入当前 Trace 的
trace_id - 指标采集时带上服务名、环境等标签
这样就能实现"从指标下钻到日志,从日志跳到 Trace":
yaml
# OTel SDK 配置示例(环境变量)
OTEL_SERVICE_NAME: order-service
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_TRACES_EXPORTER: otlp
OTEL_TRACES_SAMPLER: parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG: "0.1"
只要配置了这些,应用启动后:
- 自动采集 HTTP/RPC 调用指标(QPS、延迟、错误率)
- 自动生成调用链 Span
- 日志自动带上 trace_id
3.3 日志关联 Trace 的关键操作
日志要能和 Trace 关联,必须把 trace_id 注入到日志字段里。以 Java + Logback 为例:
xml
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}
[traceId=%X{trace_id:-}, spanId=%X{span_id:-}] - %msg%n
</pattern>
OTel Java Agent 会自动把 trace_id/span_id 放入 MDC(Mapped Diagnostic Context),Logback 里用 %X{trace_id} 就能取到。排查问题时:
- 从告警或 Dashboard 看到异常
- 日志检索
trace_id=xxx找到那一次请求的所有日志 - 点开同 ID 的 Trace,看完整调用链
从"三套系统来回切"变成"一个 ID 贯穿到底"。
四、落地架构实战:一套完整的可观测性平台
下面给出一套经过验证的落地方案,中小团队(5~50个服务)可以直接照抄。
4.1 整体架构
┌─ K8s 集群 ──────────────────────────────────────┐
│ 应用 Pod (OTel SDK / Agent 埋点) │
│ │ OTLP (gRPC) │
│ OTel Collector (DaemonSet, 每个节点一个) │
│ ├──→ Prometheus (指标) ──→ Grafana (看板) │
│ ├──→ Tempo (Trace) ────→ Grafana (链路视图) │
│ └──→ Loki (日志) ──────→ Grafana (日志检索) │
│ │ │
│ └──→ Alertmanager → 钉钉/企微 │
└─────────────────────────────────────────────────┘
选型理由:
- Prometheus + Grafana:指标事实标准,Grafana 一个入口看所有
- Tempo + Loki :同为 Grafana 系,天然联动,
trace_id一键跳转 - OTel Collector:统一接入,未来换后端不用改应用
- Alertmanager:统一告警,路由到钉钉/企业微信/邮件
4.2 OTel Collector 配置示例
yaml
# collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
prometheus:
endpoint: 0.0.0.0:9091
namespace: otel
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
otlp/loki:
endpoint: loki:4317
tls:
insecure: true
service:
pipelines:
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/tempo]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/loki]
4.3 告警规则实战
指标采进来了,不配告警等于白采。经典告警规则:
yaml
groups:
- name: service-alerts
rules:
# 错误率超过 5% 持续 5 分钟
- alert: HighErrorRate
expr: |
sum(rate(http_server_duration_milliseconds_count{
status_code=~"5.."
}[5m])) by (service_name)
/ sum(rate(http_server_duration_milliseconds_count[5m]))
by (service_name) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.service_name }} 错误率超过 5%"
description: "当前错误率 {{ $value | humanizePercentage }}"
# P99 延迟超过 1s 持续 10 分钟
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(http_server_duration_milliseconds_bucket[5m]))
by (service_name, le)) > 1000
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.service_name }} P99 延迟超过 1s"
告警分层建议:
- P0(立即处理):错误率 > 5%、服务完全不可用、磁盘将满
- P1(30分钟内):P99 超阈值、队列堆积、连接池打满
- P2(当天处理):容量水位(CPU/内存 > 80%)、证书即将过期
4.4 日志采集最佳实践
日志规范是日志平台能用的前提,强烈建议强制这几条:
- JSON 结构化输出:别用自由文本,统一 JSON,字段固定
- 必带 trace_id / span_id:与 Trace 关联的前提
- 分级打日志:ERROR 只打可恢复/不可恢复的异常,别把异常当正常流程
- 别打敏感信息:手机号、身份证、密码一律脱敏(或干脆不打)
- 日志级别可动态调整:线上排查时能把某个服务的日志临时调到 DEBUG
五、踩坑清单(真实经验)
坑1:Metrics 和 Tracing 的采样率不一致
现象 :告警说 QPS 3000,但 Trace 里同一时段只有 300 条。
原因 :Trace 开了 10% 采样,但没意识到 Metrics 是全额统计的。
解决:明确"Metrics 全额、Trace 采样"是正常设计。需要精确排查时临时把采样率调到 100%,查完再调回。
坑2:日志和 Trace 对不上
现象 :日志里有 trace_id,但在 Tempo 里查不到。
原因:
- 异步线程没传递 Context(线程池里的任务丢了 trace_id)
- 用了消息队列,Consumer 侧没继续传递 traceparent 头
- 采样导致:这条日志的 Trace 被采样掉了
解决:
- 线程池用 OTel 的
Context.taskWrapping包装任务 - MQ 消息头里透传
traceparent - 日志检索时对"有 trace_id 但查不到 Trace"的情况有预期
坑3:Collector 成为瓶颈
现象 :接入 OTel 后,应用 RT 没变,但 Collector 的 CPU 飙到 90%。
原因 :Collector 单点部署,所有信号都压在一个 Pod 上;batch 配置不当,每条数据都立刻转发。
解决:
- Collector 用 DaemonSet 部署(每个节点一个,各收各的)
- 开 batch processor,5s 攒一批再发
- memory_limiter 防止 OOM
坑4:日志量爆炸,存储扛不住
现象 :Loki 磁盘 3 个月满了,查询越来越慢。
原因 :DEBUG 日志全量采集;日志没分级;保留策略没配。
解决:
- 生产只采 INFO 以上,DEBUG 按需开
- 按环境分流:dev/test 日志保留 7 天,生产保留 30~90 天
- Loki 配 retention,ES 配 ILM 冷热分层
坑5:告警风暴
现象 :凌晨 3 点,钉钉被刷了 200 条告警,值班同学直接静音。
原因 :告警规则没配 for(持续时间),抖动一下也告警;没做聚合。
解决:
- 所有告警加
for: 5m,持续 5 分钟才触发 - 用
sum by (service_name)聚合,别每条实例都发 - 配 Alertmanager 的
group_wait/group_interval抑制重复告警
六、渐进式落地路线
别想着一步到位,建议按这个节奏推进:
第一阶段(1~2周):指标先行
- 部署 Prometheus + Grafana
- 服务接入 OTel SDK,采集 HTTP 指标
- 配置 5 条核心告警(错误率、延迟、CPU、内存、磁盘)
- 产出:值班同学有"系统健康总览"了
第二阶段(2~4周):Trace 补上
- 部署 Tempo(或 Jaeger)
- 所有服务开启 Trace 导出,先开 10% 采样
- 打通"从 Grafana 面板点进 Trace"的路径
- 产出:慢请求能定位到具体服务了
第三阶段(1~2月):日志统一 + 三信号联动
- 部署 Loki(或迁移 ELK)
- 日志接入 OTel,注入 trace_id
- 实现"指标 → 日志 → Trace"一键下钻
- 产出:完整可观测性闭环
第四阶段(按需):深化
- SLO 管理(错误预算)
- 链路拓扑自动生成(服务依赖图)
- 日志异常检测(AI 辅助,可选)
七、总结
一句话:Metrics 告诉你"哪里病了",Logging 告诉你"病根是什么",Tracing 告诉你"病是怎么传染的"。三者结合才是完整的可观测性。
落地要点回顾:
- 选型看生态:优先选能统一的栈(Grafana 系或 OTel 系),别三套系统割裂
- 标准看 OTel:一套 SDK、一套协议(OTLP)采三种信号,未来换后端不改应用
- 关联靠 trace_id:日志注入 trace_id,才能实现三信号联动下钻
- 告警要克制:for 时长 + 聚合 + 分级,别让告警风暴毁掉告警体系
- 落地要渐进:先指标、再链路、后日志,每一步都能立刻产生价值
可观测性建设是典型的"越早做越省":等系统出过两次"查了 3 小时没定位到根因"的事故,团队的决心就有了。与其等事故,不如现在就把三支柱搭起来------你不需要最好的工具,你需要的是"一套能用、能联动、查得到底"的体系。