Skip to content

元数据卡

  • 前置知识:第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 默认集成):

java
// 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.0

Grafana 把数据变成面板——你可以配置"过去 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
  • 开始/结束时间戳
  • 标签和事件
json
// 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 的采集标准。

java
// 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 上部署整套方案:

bash
# 添加 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(一行命令搞定,无需改代码):

bash
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 数据源,配置仪表盘:

  1. 总请求量面板(Counter: http_server_duration_count
  2. P99 延迟面板(Histogram: http_server_duration_bucket
  3. 错误率面板(Counter: http_server_duration_count + status filter)
  4. 服务依赖图(根据 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)。你发现 fetchDatarenderMap 可以并发执行。重构为并行调用后,总耗时从 1.8s 降到 900ms,P99 数据验证了这一变化。


常见陷阱

  1. 只在生产环境部署可观测性。 可观测性需要在开发/测试环境就部署。否则你在开发环境看不到的问题,到生产环境会措手不及。OTel 可以配置抽样率——开发环境全部采样,生产环境按比例采样。

  2. 日志结构化不够或者过度。 纯文本日志("user login failed")无法被机器解析。过度结构化的日志(一堆嵌套 JSON 每个字段都很长)消耗存储和索引资源。找到平衡点:关键字段(trace_id、span_id、service、level、message)结构化,额外信息用 JSON 字符串内嵌。

  3. 过度采样 Tracing。 高吞吐系统每秒处理 10 万请求——如果每个请求都生成 Trace,Jaeger 后端和存储扛不住。合理的做法:头采样(根据请求特性确定是否采样:错误=100%、高延迟=100%、正常=1%)、尾采样(先收集所有 span 到缓存,再由后端决定是否留存)。

  4. 把 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。

bash
# 最终的可观测性栈查询:跨维度关联

# 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 秒)。然后执行以下排查流程:
    1. Grafana 上发现错误率上升
    2. Jaeger 定位到故障服务
    3. 故障服务的日志发现 root cause
    4. 修复后确认指标恢复

旅人笔记

可观测性是分布式系统的"眼睛"。Metrics 告诉你"出了事",Tracing 告诉你"在哪出事",Logging 告诉你"为什么出事"。三个维度缺一个,排查问题时就像闭着眼抓瞎。OpenTelemetry 的价值不只是采集数据——它建立了一个贯穿所有服务的上下文体系,让每一次请求都带着自己的 "ID 手环",穿越 10 个服务也不会丢掉追踪线索。这不仅是工具,更是分布式系统的诊断方法论。


本卷回顾

你站在了第七卷的终点,回望走过的八站:

  1. 分布式系统概览——了解了 Fallacies、CAP、FLP 和时钟问题。你看清了分布式世界的残酷真相:网络不可靠,时钟不可信,部分失败是常态。
  2. RPC 与序列化——学会了 gRPC 和 Protobuf,理解了 IDL 契约如何让服务之间精准对话。
  3. 一致性与共识——深入了 Raft 的三个子问题(选举、复制、安全性),建立了 Paxos 的直觉,理解了 ZAB 和 ZooKeeper 的选主机制。
  4. 分布式存储——掌握了一致性哈希、HDFS 的块存储、Cassandra 的 Ring 架构和 Spanner 的全球级设计。
  5. 分布式计算——从 MapReduce 到 Spark,理解了内存 DAG 执行和 RDD/DataFrame 的层次抽象。
  6. 微服务架构——部署了服务发现、配置中心、API 网关、熔断器和 Saga 协调器,把一个大系统拆成了独立的战斗单元。
  7. 容器化与编排——用 Docker 标准化了交付,用 K8s 的 Pod/Service/Deployment 三层模型管理了大规模实例。
  8. 可观测性——装上了 Metrics/Tracing/Logging 三合一的感知系统,让分布在集群中的服务变得可观测、可调试。

林将军站在指挥台上,看着你在地图上标注的架构图,说:"你现在有了一个人打天下的全部工具——但是,别一个人打。分布式系统的本质不是技术多高超,是你知道怎么组织一群人合作。"

下一站预告

从工匠之都到远征军前线,你学会了怎样让几百台机器、几十个服务协同工作。但在边境,新的威胁正在逼近——你的系统暴露在公网上,随时可能遭到攻击。如果你不防御,你的集群会成为别人的计算资源。下一卷——Vol 8 安全——你该穿上盔甲了。

Built with VitePress | Software Systems Atlas