元数据卡
- 前置知识:第6章(微服务)、第7章(容器化与 K8s)
- 预计时间:45 分钟
- 核心难度:进阶
- 阅读模式:高度专注
- 完成标志:理解 Metrics / Tracing / Logging 三者的区别和联系,能在 K8s 中配置 Prometheus 采集指标并用 Grafana 展示,理解 OpenTelemetry 的架构和 span 传播机制
你的进度
在 K8s 上成功运行了 20 个微服务,部署流程已经标准化。但一个新问题浮出水面:用户报了一个报告生成慢的问题。你开始排查:
kubectl logs -l service=report-service—— 查看报告服务的日志,没有异常kubectl top pods—— CPU 和内存使用正常- 然后你愣住了——你不知道请求经过了哪些服务,每个服务花了多少时间
林将军指着指挥地图说:"你看得见每个堡垒在哪,但你不知道信号经了什么路由、在哪段路耽搁了。你需要一个全局的战场感知系统。"
在分布式系统中,这种"感知系统"叫做可观测性。
你的任务
掌握可观测性的三个支柱:Metrics(指标)、Tracing(链路追踪)、Logging(日志)。理解 OpenTelemetry 如何统一这三个维度。最后通过一个实战案例——在 K8s 集群上部署一个完整的可观测性栈——把这卷所有知识串起来。
破局 · 溯源
为什么需要三种数据?
一个维度不够。看问题视角不同,需要的工具不同:
| 场景 | 你问的问题 | 需要什么 |
|---|---|---|
| 报警 | 服务是否在正常工作? | Metrics |
| 定位 | 这个错误请求经过了哪些服务? | Tracing |
| 复盘 | 服务 X 在崩溃前打印了什么? | Logging |
三个维度各有盲区,合在一起才完整:
- Metrics 告诉你"系统退化的那一分钟发生在 14:32"(时间范围缩小到分钟级)
- Tracing 告诉你"是 report-service 到 database-service 的调用超时了"(把问题定位到具体服务调用)
- Logging 告诉你"数据库连接池耗尽,连接等待了 45 秒"(给出 root cause 的文本细节)
Metrics(指标)
Metrics 是聚合的、数字化的系统状态快照。常见的 Metrics 类型:
Counter(计数器):
只能增加,不能减少
例如: 请求总数、错误总数、队列入队总数
用途: 逐秒记录,用 rate() 计算 QPS
Gauge(仪表盘):
可增可减
例如: 当前活跃连接数、内存使用量、队列长度
用途: 观察当前资源状态
Histogram(直方图):
统计数值分布
例如: 请求延迟分布、响应体大小分布
用途: 计算 P50/P95/P99 延迟Prometheus + Grafana
Prometheus 是 CNCF 毕业项目,标准的时间序列数据库,广泛用于 Metrics 采集。
Prometheus 拉取模型:
+------------+
| Prometheus | ← 定时拉取(scrape)
+----+-------+
|
+----------+----------+
| | |
v v v
+--------+ +--------+ +--------+
|ServiceA| |ServiceB| |ServiceC|
|/metrics| |/metrics| |/metrics|
+--------+ +--------+ +--------+每个服务暴露一个 /metrics 端点,Prometheus 定期抓取。Grafana 从 Prometheus 读取数据做可视化展示。
暴露 Metrics 的 Java 示例(使用 Micrometer,Spring Boot 默认集成):
// MetricsExporter.java
// Spring Boot 自动集成 Micrometer + Prometheus
// 依赖: spring-boot-starter-actuator + micrometer-registry-prometheus
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/reports")
public class ReportController {
private final Counter reportGeneratedCounter;
public ReportController(MeterRegistry registry) {
reportGeneratedCounter = Counter.builder("atlas.report.generated")
.description("Total reports generated")
.tag("type", "scout_analysis")
.register(registry);
}
@PostMapping
public Report generateReport(@RequestBody ReportRequest request) {
long start = System.currentTimeMillis();
// 生成报告...
Report result = reportService.generate(request);
long duration = System.currentTimeMillis() - start;
reportGeneratedCounter.increment();
// 记录延迟(用于 Histogram)
Metrics.counter("atlas.report.generation.time",
Tags.of("type", request.getType()))
.increment(duration);
return result;
}
}访问 GET /actuator/prometheus 看到:
# HELP atlas_report_generated Total reports generated
# TYPE atlas_report_generated counter
atlas_report_generated{type="scout_analysis"} 1283.0Grafana 把数据变成面板——你可以配置"过去 24 小时 QPS""P99 延迟趋势""错误率变化"等面板,叠加多个数据源。
Tracing(链路追踪)
一个请求穿越 5 个服务时,每个服务的日志各有时间戳,但你要靠"相邻 1 秒内"来猜测调用关系——这不可靠。Tracing 通过 Trace ID 和 Span 把一次请求的完整路径串起来。
Trace 结构:
Client Gateway UserSvc IntelSvc
| | | |
|----------- HTTP GET /reports ------->| | |
| |--- span ---> | |
| | (usvc:300ms) | |
| | |---span->|
| | |(5s:8ms) |
| |<---response------| |
|<-------- HTTP 200 -------------------| | |
v v v v每个 Span 包含:
- Trace ID(全局唯一,贯穿所有服务)
- Span ID(当前操作的唯一 ID)
- Parent Span ID(调用方的 Span ID,构成父子关系)
- 操作名称(如
HTTP GET /reports) - 开始/结束时间戳
- 标签和事件
// Span 示例(OpenTelemetry 格式)
{
"traceId": "7b8e9f1a2c3d4e5f",
"spanId": "a1b2c3d4",
"parentSpanId": "00000000",
"name": "report-service.generateReport",
"startTime": "2026-06-24T14:30:00.123Z",
"endTime": "2026-06-24T14:30:00.432Z",
"attributes": {
"report.type": "scout_analysis",
"report.id": "R-20260624-001"
},
"events": [
{"name": "query.intel_db", "timestamp": "14:30:00.200"},
{"name": "query.complete", "timestamp": "14:30:00.400"}
],
"status": "OK"
}OpenTelemetry
OpenTelemetry(OTel)是 CNCF 的第二个毕业项目(继 Prometheus 之后),目标是统一 Metrics、Tracing、Logging 的采集标准。
// OpenTelemetry Tracing 示例
// 在 Spring Boot 中集成 OTel
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
@Service
public class ReportService {
private static final Tracer tracer =
GlobalOpenTelemetry.getTracer("atlas-report-service");
public Report generate(ReportRequest request) {
// 创建一个 Span
Span span = tracer.spanBuilder("generateReport")
.setAttribute("report.type", request.getType())
.startSpan();
try (Scope ignored = span.makeCurrent()) {
// 在这个 Span 上下文中的操作
String data = fetchIntelligenceData(request);
// 子调用:创建子 Span
Span childSpan = tracer.spanBuilder("formatReport")
.setAttribute("data.size", data.length())
.startSpan();
try (Scope childScope = childSpan.makeCurrent()) {
Report report = format(data);
return report;
} finally {
childSpan.end();
}
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
}
}上下文传播(Context Propagation)
当服务 A 调用服务 B 时,Trace ID 必须在 HTTP/gRPC 请求头中传递:
服务 A 发出请求时注入:
traceparent: 00-7b8e9f1a2c3d4e5f6a7b8c9d0e1f2a3b-a1b2c3d4-01
服务 B 收到请求时提取:
traceparent → 解析出 traceId 和 parentSpanId
→ 创建一个新的 Span,parent = 提取到的 spanId这样,即使跨越 10 个服务、3 种语言(Java → Python → Go),整个调用链都能贯穿起来。
Jaeger 可视化
Jaeger 接收 OTel span 数据,在 UI 中展示:
Trace: 7b8e9f1a (报告生成请求,总耗时 2.3s)
├── user-service.validateToken (42ms)
├── gateway.route (5ms)
├── report-service.generateReport (1.8s) ← 这里慢!
│ ├── intel-service.fetchData (800ms) ← 主要耗时
│ ├── map-service.renderMap (200ms)
│ └── report-service.format (780ms)
└── gateway.response (2ms)一眼就能看出瓶颈是 intel-service.fetchData 花了 800ms——然后你可以去查对应的 Metrics 和 Logs 找原因。
三支柱一体:OpenTelemetry 的宏大愿景
OpenTelemetry 想把三个维度统一到一套 API 中:
从应用侧:
OTel SDK →
输出 Metrics → Prometheus
输出 Traces → Jaeger / Zipkin
输出 Logs → ELK / Loki
从运维侧:
一个 Kubernetes Operator 就能部署完整的 OTel Collector
采集 → 处理 → 导出本质上,OTel 做的事情是:你在代码里只需要了解 OTel 的 API,采集到的数据由 Collector 负责处理并路由到不同的后端系统。
系统实战:在 K8s 上搭建可观测性栈
用 Helm 在本地 K8s 上部署整套方案:
# 添加 Helm 仓库
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo add jaegertracing https://jaegertracing.io/helm-charts
helm repo update
# 安装 Prometheus + Grafana
helm upgrade --install monitoring prometheus-community/kube-prometheus-stack
# 安装 Jaeger
helm upgrade --install jaeger jaegertracing/jaeger-operator
kubectl apply -f - <<EOF
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: simplest
EOF然后在 Java 微服务项目中添加 OpenTelemetry agent(一行命令搞定,无需改代码):
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=atlas-report-service \
-Dotel.traces.exporter=jaeger \
-Dotel.metrics.exporter=prometheus \
-Dotel.logs.exporter=otlp \
-jar atlas-report-service.jar启动后,在 Grafana 中接入 Prometheus 数据源,配置仪表盘:
- 总请求量面板(Counter:
http_server_duration_count) - P99 延迟面板(Histogram:
http_server_duration_bucket) - 错误率面板(Counter:
http_server_duration_count+ status filter) - 服务依赖图(根据 Tracing 数据自动生成)
可观测性的三种应用场景
场景 1:日常监控
Grafana 仪表盘上看到 P99 延迟从 50ms 突然升到 2s。你切换到 Jaeger 看调用链,发现某个服务的数据库查询突然变慢。再看 Prometheus,这个服务所在的 Node 的磁盘 IO 饱和了。
场景 2:故障定位
用户报告"报告生成失败"。你搜索日志关键字 "ERROR",看到一条错误日志:"outpost_007: connection refused"。查看 Tracing,定位到情报分析服务的一个 API 调用超时。检查 Prometheus,发现情报分析服务的实例数从 4 降到了 1(可能是 HPA 缩过头或容器挂了)。
场景 3:性能优化
Jaeger 上显示报告生成的耗时分布:intel-service.fetchData (800ms) + map-service.renderMap (200ms) + format (780ms)。你发现 fetchData 和 renderMap 可以并发执行。重构为并行调用后,总耗时从 1.8s 降到 900ms,P99 数据验证了这一变化。
常见陷阱
只在生产环境部署可观测性。 可观测性需要在开发/测试环境就部署。否则你在开发环境看不到的问题,到生产环境会措手不及。OTel 可以配置抽样率——开发环境全部采样,生产环境按比例采样。
日志结构化不够或者过度。 纯文本日志("user login failed")无法被机器解析。过度结构化的日志(一堆嵌套 JSON 每个字段都很长)消耗存储和索引资源。找到平衡点:关键字段(trace_id、span_id、service、level、message)结构化,额外信息用 JSON 字符串内嵌。
过度采样 Tracing。 高吞吐系统每秒处理 10 万请求——如果每个请求都生成 Trace,Jaeger 后端和存储扛不住。合理的做法:头采样(根据请求特性确定是否采样:错误=100%、高延迟=100%、正常=1%)、尾采样(先收集所有 span 到缓存,再由后端决定是否留存)。
把 Prometheus 当 SQL 数据库用。 Prometheus 不擅长高基数的标签组合(例如
user_id作为标签——100 万用户就有 100 万个不同的标签值,Prometheus 的索引暴增)。把高基数维度放在日志或 Trace 中,Metrics 只保留聚合值。
通关挑战
热身:在你的 K8s 集群上用 Helm 部署 kube-prometheus-stack 和 Jaeger。访问 Grafana(默认端口 3000,默认密码 admin/prom-operator),配置一个 Panel 展示集群 POD 的 CPU 使用率。
挑战:写一个简单的 Spring Boot Java 服务,集成 Micrometer 和 OpenTelemetry(通过 javaagent 方式),暴露
/metrics端点和自定义 Counter。在 Jaeger 中查看由该服务产生的 Trace。
# 最终的可观测性栈查询:跨维度关联
# PromQL:过去10分钟的错误率
rate(http_server_duration_count{status_code=~"5.."}[5m])
# 看特定 Trace 的日志上下文
kubectl logs -l service=report-service | grep "trace_id=7b8e9f1a"
# 从 Grafana 仪表盘钻取到 Jaeger 的具体 Trace- 观察:在你的微服务集群中人为制造一个故障(例如让一个服务 50% 请求返回 500 或延迟 3 秒)。然后执行以下排查流程:
- Grafana 上发现错误率上升
- Jaeger 定位到故障服务
- 故障服务的日志发现 root cause
- 修复后确认指标恢复
旅人笔记
可观测性是分布式系统的"眼睛"。Metrics 告诉你"出了事",Tracing 告诉你"在哪出事",Logging 告诉你"为什么出事"。三个维度缺一个,排查问题时就像闭着眼抓瞎。OpenTelemetry 的价值不只是采集数据——它建立了一个贯穿所有服务的上下文体系,让每一次请求都带着自己的 "ID 手环",穿越 10 个服务也不会丢掉追踪线索。这不仅是工具,更是分布式系统的诊断方法论。
本卷回顾
你站在了第七卷的终点,回望走过的八站:
- 分布式系统概览——了解了 Fallacies、CAP、FLP 和时钟问题。你看清了分布式世界的残酷真相:网络不可靠,时钟不可信,部分失败是常态。
- RPC 与序列化——学会了 gRPC 和 Protobuf,理解了 IDL 契约如何让服务之间精准对话。
- 一致性与共识——深入了 Raft 的三个子问题(选举、复制、安全性),建立了 Paxos 的直觉,理解了 ZAB 和 ZooKeeper 的选主机制。
- 分布式存储——掌握了一致性哈希、HDFS 的块存储、Cassandra 的 Ring 架构和 Spanner 的全球级设计。
- 分布式计算——从 MapReduce 到 Spark,理解了内存 DAG 执行和 RDD/DataFrame 的层次抽象。
- 微服务架构——部署了服务发现、配置中心、API 网关、熔断器和 Saga 协调器,把一个大系统拆成了独立的战斗单元。
- 容器化与编排——用 Docker 标准化了交付,用 K8s 的 Pod/Service/Deployment 三层模型管理了大规模实例。
- 可观测性——装上了 Metrics/Tracing/Logging 三合一的感知系统,让分布在集群中的服务变得可观测、可调试。
林将军站在指挥台上,看着你在地图上标注的架构图,说:"你现在有了一个人打天下的全部工具——但是,别一个人打。分布式系统的本质不是技术多高超,是你知道怎么组织一群人合作。"
下一站预告
从工匠之都到远征军前线,你学会了怎样让几百台机器、几十个服务协同工作。但在边境,新的威胁正在逼近——你的系统暴露在公网上,随时可能遭到攻击。如果你不防御,你的集群会成为别人的计算资源。下一卷——Vol 8 安全——你该穿上盔甲了。