跳到主要内容

分布式系统可观测性

在分布式系统中,一个请求可能经过数十个服务、跨越多个数据中心。当系统出现问题时,如何快速定位故障根源?如何了解系统内部的运行状态?这就是可观测性要解决的问题。

什么是可观测性

可观测性(Observability)一词源自控制理论,指的是通过系统的外部输出来推断系统内部状态的能力。在软件系统中,可观测性让我们能够通过系统的外部信号(日志、指标、追踪)来理解系统的内部行为。

可观测性 vs 监控

监控和可观测性经常被混淆,但它们有着本质的区别:

监控回答的是"我知道什么出了问题",它关注的是已知的故障模式。例如:CPU使用率超过80%触发告警、磁盘空间不足等。监控是基于预设阈值的被动响应。

可观测性回答的是"为什么会出问题",它关注的是未知的未知。当系统出现前所未有的问题时,可观测性工具能帮助我们探索、分析、定位问题根源。可观测性是主动探索的能力。

打个比方,监控就像汽车的仪表盘,告诉你油量不足、发动机过热;而可观测性就像飞机的黑匣子,记录了飞行过程中的所有细节,帮助调查人员分析事故原因。

可观测性三大支柱

可观测性由三个核心组件构成,被称为"三大支柱":

日志(Logs)、指标(Metrics)、分布式追踪(Traces)。这三者各有特点,相互补充,共同构成完整的可观测性体系。

日志是离散的事件记录,包含时间戳和详细的上下文信息。每条日志记录了系统在某一时刻发生的具体事件,比如用户登录、订单创建、错误发生等。日志适合用于详细的问题诊断和审计。

指标是数值型的聚合数据,反映系统在某个时间段的统计特征。比如每秒请求数(QPS)、平均响应时间、错误率等。指标数据结构紧凑,适合长期存储和趋势分析,能够快速发现系统异常。

分布式追踪记录了一个请求在分布式系统中的完整调用路径。它展示了请求从入口到出口所经过的所有服务、每个服务的处理时间、服务之间的依赖关系。追踪是定位分布式系统性能瓶颈和故障根源的利器。

维度日志指标追踪
数据类型文本/结构化数值时间序列有向无环图
粒度单个事件聚合统计请求级别
存储成本
查询灵活性
主要用途问题诊断趋势监控、告警调用链分析

分布式追踪

分布式追踪是理解分布式系统行为的最有力工具。当一个请求经过多个微服务时,追踪能够将其串联起来,形成完整的调用链路图。

核心概念

分布式追踪的核心概念来自 Google Dapper 论文。理解这些概念是掌握分布式追踪的基础。

**Trace(追踪)**代表一个请求从发起到结束的完整旅程。每个 Trace 有一个全局唯一的 TraceID,用于标识整个请求链路。一个 Trace 包含多个 Span,它们共同描述了请求的完整生命周期。

**Span(跨度)**代表一个独立的工作单元。每个 Span 记录了一次具体的操作,比如一次 RPC 调用、一次数据库查询、一次缓存访问。Span 包含操作名称、开始时间、结束时间、标签(Tags)、日志(Logs)等信息。

Spans 之间通过父子关系组织成树状结构。最顶层的 Span 称为根 Span(Root Span),代表请求的入口点。子 Span 代表父 Span 内部的子操作。

Trace: 用户下单请求

├── Span: API网关接收请求 (0ms - 50ms)
│ ├── Span: 用户认证 (5ms - 15ms)
│ └── Span: 路由转发 (20ms - 45ms)

├── Span: 订单服务创建订单 (50ms - 200ms)
│ ├── Span: 库存服务检查库存 (60ms - 100ms)
│ │ └── Span: 数据库查询库存 (65ms - 90ms)
│ ├── Span: 支付服务预扣款 (110ms - 180ms)
│ └── Span: 数据库写入订单 (185ms - 195ms)

└── Span: 返回响应 (200ms - 210ms)

**上下文传播(Context Propagation)**是分布式追踪的关键机制。当一个请求从服务 A 调用服务 B 时,追踪上下文(包含 TraceID、ParentSpanID 等)需要从 A 传递到 B,这样 B 产生的 Span 才能正确关联到同一个 Trace。

上下文传播通常通过 HTTP 头部实现。W3C Trace Context 标准定义了标准的头部格式:

  • traceparent:包含 trace-id、parent-id、trace-flags
  • tracestate:可选,包含供应商特定的追踪信息

追踪数据的采集

追踪数据的采集主要有两种方式:代码埋点和自动插桩。

代码埋点需要在业务代码中手动添加追踪逻辑。这种方式精确可控,但侵入性强,维护成本高。

// 使用 OpenTelemetry SDK 手动埋点
public class OrderService {

private final Tracer tracer; // OpenTelemetry Tracer

public Order createOrder(OrderRequest request) {
// 创建一个 Span,spanName 为操作名称
Span span = tracer.spanBuilder("createOrder")
.setAttribute("order.userId", request.getUserId())
.setAttribute("order.productId", request.getProductId())
.startSpan();

try (Scope scope = span.makeCurrent()) {
// 业务逻辑
validateOrder(request);
Order order = saveOrder(request);

span.setStatus(StatusCode.OK);
return order;
} catch (Exception e) {
// 记录异常信息
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
// 必须结束 Span
span.end();
}
}
}

自动插桩通过字节码增强或代理方式自动采集追踪数据,无需修改业务代码。OpenTelemetry 提供了 Java Agent,可以自动追踪常见的框架和库。

# 使用 OpenTelemetry Java Agent 启动应用
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://collector:4317 \
-jar myapp.jar

自动插桩大大降低了接入成本,但对于自定义的业务逻辑,仍然需要适当的手动埋点。

Jaeger:分布式追踪系统

Jaeger 是 Uber 开源的分布式追踪系统,是 CNCF 的毕业项目。它实现了 OpenTracing 标准,并支持 OpenTelemetry 数据格式。

Jaeger 的架构包含以下组件:

Agent:部署在应用主机上,接收应用发送的追踪数据,批量转发给 Collector。Agent 减少了应用与 Collector 之间的网络通信开销。

Collector:接收 Agent 发送的追踪数据,进行验证、转换、索引等处理,然后存储到后端。

Storage:追踪数据的持久化存储。Jaeger 支持 Elasticsearch、Cassandra、Kafka 等多种后端。

Query:提供 Web UI 和 API,用于查询和可视化追踪数据。

# docker-compose.yml - Jaeger 快速部署
version: '3'
services:
jaeger:
image: jaegertracing/all-in-one:1.52
ports:
- "16686:16686" # Web UI
- "14268:14268" # HTTP 收集
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
environment:
- COLLECTOR_OTLP_ENABLED=true

在 Jaeger UI 中,可以查看 Trace 的瀑布图(Waterfall Diagram),直观地看到每个 Span 的耗时和调用关系。瀑布图是分析性能瓶颈的关键工具:耗时最长的 Span 往往是优化重点。

追踪采样策略

在流量较大的系统中,记录每一个请求的完整追踪数据会产生巨大的存储和性能开销。采样策略帮助我们平衡观测成本与数据完整性。

全量采样记录所有请求的追踪数据,适用于低流量系统或关键服务。优点是数据完整,缺点是成本高。

概率采样按固定比例随机采样,如采样率设为 1%,则只记录 1% 的请求。实现简单,但可能漏掉重要请求。

限速采样限制每秒采样的 Trace 数量,如每秒最多采样 10 个 Trace。可以控制数据量上限。

自适应采样根据服务的请求量动态调整采样率。请求量大时降低采样率,请求量小时提高采样率,保证数据代表性的同时控制成本。

// OpenTelemetry 采样配置
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.setSampler(
// 使用 TraceIdRatioBased 采样器,采样率 10%
Sampler.traceIdRatioBased(0.1)
)
.addSpanProcessor(
BatchSpanProcessor.builder(OtlpGrpcSpanExporter.builder()
.setEndpoint("http://collector:4317")
.build())
.build())
.build();

生产环境通常采用分层采样策略:入口网关使用较低采样率,错误请求和慢请求强制采样,关键链路全量采样。

指标监控

指标是系统状态的数字化表示,通过时间序列数据反映系统的运行状况。

指标类型

指标主要分为三种类型,每种类型适用于不同的监控场景。

**计数器(Counter)**是只增不减的累积值。它适合记录只会增加的事件,如请求总数、错误总数、处理的消息数。计数器的值可以重置(如服务重启),但绝不会减少。

计数器的主要用途是计算速率。通过 rate() 函数计算计数器在单位时间内的增量,得到 QPS(每秒请求数)、错误率等关键指标。

**测量值(Gauge)**是可以增加也可以减少的瞬时值。它反映系统在某一时刻的状态,如当前内存使用量、活跃连接数、队列长度。测量值的波动反映了系统状态的变化。

**直方图(Histogram)**将观测值分布到预定义的桶中。它不仅记录观测值的总和和数量,还记录各桶的计数。直方图特别适合分析响应时间、请求大小等分布型数据。

通过直方图可以计算百分位数(Percentile),如 P50、P95、P99。P99 响应时间表示 99% 的请求响应时间小于该值,是衡量用户体验的重要指标。

请求响应时间分布示例:

桶边界 计数 累计
10ms 150 150 (15%)
50ms 400 550 (55%)
100ms 300 850 (85%)
200ms 100 950 (95%)
500ms 40 990 (99%)
1000ms 10 1000 (100%)

P50 ≈ 50ms
P95 ≈ 200ms
P99 ≈ 500ms

Prometheus:指标采集与存储

Prometheus 是云原生领域最流行的监控系统,采用拉模型(Pull Model)采集指标。

Prometheus 的核心设计理念是每个服务暴露一个 /metrics 端点,Prometheus Server 定期从这个端点拉取指标数据。这种拉模型使得服务注册发现更加灵活,也便于问题排查。

// Spring Boot 集成 Prometheus
// 1. 添加依赖
// implementation 'io.micrometer:micrometer-registry-prometheus'

// 2. 自定义指标
@Component
public class OrderMetrics {

private final Counter orderCounter;
private final Timer orderTimer;
private final Gauge pendingOrdersGauge;

public OrderMetrics(MeterRegistry registry) {
// 计数器:订单总数
this.orderCounter = Counter.builder("orders_total")
.description("Total number of orders")
.tag("type", "normal")
.register(registry);

// 计时器:订单处理时间
this.orderTimer = Timer.builder("order_processing_time")
.description("Order processing time")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
}

public void recordOrder(String type, long processingTimeMs) {
orderCounter.increment();
orderTimer.record(processingTimeMs, TimeUnit.MILLISECONDS);
}
}

// 3. application.yml 配置
// management:
// endpoints:
// web:
// exposure:
// include: prometheus,health,info
// metrics:
// tags:
// application: order-service

PromQL 是 Prometheus 的查询语言,功能强大且表达力丰富。

# 每秒请求率(QPS)
rate(http_requests_total[5m])

# 按服务分组的错误率
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/ sum(rate(http_requests_total[5m])) by (service)

# P99 响应时间
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m]))

# 过去 1 小时内存使用量的最大值
max_over_time(process_resident_memory_bytes[1h])

告警规则

监控的价值在于及时发现问题。告警规则定义了触发告警的条件,当指标违反预设阈值时,系统会发送通知。

# Prometheus 告警规则示例
groups:
- name: service-alerts
rules:
# 服务可用性告警
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "服务 {{ $labels.job }} 不可用"
description: "{{ $labels.instance }} 已经超过 1 分钟无响应"

# 高错误率告警
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/ sum(rate(http_requests_total[5m])) by (service) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.service }} 错误率过高"
description: "错误率 {{ $value | humanizePercentage }},超过 5% 阈值"

# P99 响应时间告警
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
) > 2
for: 10m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.service }} P99 延迟过高"
description: "P99 延迟 {{ $value }}s,超过 2s 阈值"

告警设计需要遵循"少即是多"原则。过多的告警会导致告警疲劳,使运维人员忽视真正重要的问题。好的告警应该:

  • 可操作:每条告警都应该有明确的处理方式
  • 有上下文:包含足够的诊断信息
  • 有优先级:区分严重程度,避免次要问题淹没重要问题

日志管理

日志是系统行为的详细记录,是问题诊断的主要依据。在分布式环境中,日志管理面临着新的挑战。

结构化日志

传统的文本日志难以解析和检索。结构化日志使用 JSON 或其他结构化格式,便于日志系统解析和查询。

// 使用 SLF4J + Logback 输出结构化日志
// logback-spring.xml 配置
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<includeTags>true</includeTags>
</encoder>
</appender>

// 代码中使用
import org.slf4j.MDC;

public class OrderService {

private static final Logger log = LoggerFactory.getLogger(OrderService.class);

public Order createOrder(OrderRequest request) {
// 将追踪信息放入 MDC,自动附加到每条日志
MDC.put("traceId", request.getTraceId());
MDC.put("userId", request.getUserId());

log.info("Creating order",
kv("productId", request.getProductId()),
kv("quantity", request.getQuantity()));

try {
Order order = processOrder(request);
log.info("Order created successfully", kv("orderId", order.getId()));
return order;
} catch (Exception e) {
log.error("Failed to create order",
kv("error", e.getMessage()),
e);
throw e;
} finally {
MDC.clear();
}
}
}

// 输出的 JSON 日志示例
{
"@timestamp": "2024-01-15T10:30:00.123Z",
"@version": "1",
"level": "INFO",
"logger_name": "com.example.OrderService",
"message": "Creating order",
"traceId": "abc123def456",
"spanId": "span789",
"userId": "user001",
"productId": "prod123",
"quantity": 2,
"service": "order-service",
"host": "order-service-pod-1"
}

结构化日志的关键优势:

  • 可以按任意字段查询和过滤
  • 便于聚合统计分析
  • 支持自动化的日志处理和告警

ELK Stack:日志收集与分析

ELK Stack(Elasticsearch、Logstash、Kibana)是最流行的日志管理解决方案。现代架构中,Logstash 常被更轻量的 Filebeat 或 Fluentd 替代。

# docker-compose.yml - ELK Stack 部署
version: '3'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- "9200:9200"

kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200

filebeat:
image: docker.elastic.co/beats/filebeat:8.11.0
volumes:
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro

日志收集的典型流程:

  1. 应用将日志写入本地文件或标准输出
  2. Filebeat 监控日志文件,收集新增的日志行
  3. Filebeat 将日志发送到 Elasticsearch
  4. Kibana 提供可视化界面,支持搜索、过滤、聚合分析

日志关联追踪

日志和追踪的关联能够极大提升问题定位效率。当在 Jaeger 中发现一个慢请求时,可以直接跳转到 Kibana 查看该请求的所有日志。

实现日志关联的关键是在每条日志中包含 TraceID。这通常通过日志框架的 MDC(Mapped Diagnostic Context)机制实现:

// Spring Boot 过滤器自动注入追踪信息
@Component
public class TracingFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;

// 从请求头提取或生成 TraceID
String traceId = httpRequest.getHeader("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
}

// 放入 MDC,日志框架会自动附加到每条日志
MDC.put("traceId", traceId);

try {
chain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}

OpenTelemetry:统一的可观测性框架

OpenTelemetry 是 CNCF 的项目,旨在提供统一的可观测性标准。它整合了追踪、指标、日志三种信号,并提供厂商无关的 API 和 SDK。

为什么需要 OpenTelemetry

在 OpenTelemetry 出现之前,不同的可观测性厂商有自己的 SDK 和数据格式:

  • Jaeger 使用 Jaeger 客户端和 Jaeger 格式
  • Zipkin 使用 Zipkin 客户端和 Zipkin 格式
  • Prometheus 有自己的客户端库
  • 各云厂商也有自己的方案

这导致了严重的厂商锁定问题。如果要切换可观测性后端,需要修改大量代码。OpenTelemetry 通过提供统一的 API 和数据格式解决了这个问题。

使用 OpenTelemetry 后,切换后端只需修改配置,无需修改代码:

# 切换到 Jaeger
otel.exporter.otlp.endpoint=http://jaeger-collector:4317

# 切换到 Zipkin
otel.exporter.zipkin.endpoint=http://zipkin:9411/api/v2/spans

# 切换到云厂商(如 AWS X-Ray)
otel.exporter.otlp.endpoint=http://otel-collector:4317

OpenTelemetry 架构

OpenTelemetry 的架构设计实现了关注点分离:

API 层定义了遥测数据的抽象接口,包括 Tracer、Meter、Logger 等。应用代码只依赖 API 层,不关心具体实现。

SDK 层实现了 API 层的接口,提供了采样、批处理、导出等具体功能。SDK 可以配置不同的导出器(Exporter)将数据发送到不同的后端。

Collector 是一个独立的服务,可以接收、处理、导出遥测数据。它支持多种数据格式,可以进行数据转换、过滤、聚合等操作。

# OpenTelemetry Collector 配置
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318

processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512

exporters:
jaeger:
endpoint: jaeger-collector:14250
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
elasticsearch:
endpoints: ["http://elasticsearch:9200"]

service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [elasticsearch]

Java 应用集成 OpenTelemetry

OpenTelemetry Java Agent 提供了零代码修改的集成方式:

# 下载 OpenTelemetry Java Agent
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

# 启动应用
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=my-service \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
-jar myapp.jar

对于需要自定义追踪的场景,可以使用 OpenTelemetry API:

// 手动埋点示例
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;

@Service
public class PaymentService {

private final Tracer tracer;

public PaymentService(Tracer tracer) {
this.tracer = tracer;
}

public PaymentResult processPayment(PaymentRequest request) {
// 创建 Span
Span span = tracer.spanBuilder("processPayment")
.setAttribute("payment.amount", request.getAmount())
.setAttribute("payment.currency", request.getCurrency())
.startSpan();

try (Scope scope = span.makeCurrent()) {
// 调用第三方支付
PaymentResult result = callPaymentGateway(request);

span.setAttribute("payment.status", result.getStatus());
span.setStatus(StatusCode.OK);
return result;

} catch (PaymentException e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}

可观测性最佳实践

构建有效的可观测性系统需要遵循一些关键原则。

RED 方法

RED 方法是微服务监控的核心方法论,关注三个关键指标:

  • Rate(速率):每秒处理的请求数,反映系统的负载水平
  • Errors(错误):每秒失败的请求数,反映系统的健康状况
  • Duration(耗时):请求处理时间的分布,反映系统的性能水平

这三个指标是服务健康的核心信号,应该作为告警的首要依据。当 Rate 突然下降可能表示流量异常,Errors 上升表示功能故障,Duration 增加表示性能问题。

USE 方法

USE 方法适用于资源监控,如 CPU、内存、磁盘、网络:

  • Utilization(利用率):资源被使用的百分比
  • Saturation(饱和度):资源排队等待的程度
  • Errors(错误):资源操作的错误率

USE 方法帮助发现资源瓶颈。例如,CPU 利用率高且饱和度高,说明 CPU 是性能瓶颈;磁盘错误率高,说明硬件可能存在问题。

服务等级目标(SLO)

可观测性数据应该服务于业务目标。SLO 定义了服务的可靠性目标,将技术指标与用户体验关联起来。

例如,"99.9% 的请求在 200ms 内响应"是一个 SLO。通过追踪和指标数据,我们可以计算 SLO 的达标率,当达标率下降时发出告警。

# 计算 SLO 达标率
# SLO: 99% 的请求在 500ms 内完成
sum(rate(http_request_duration_seconds_bucket{le="0.5"}[7d]))
/ sum(rate(http_request_duration_seconds_count[7d]))

# 计算"错误预算"消耗速率
# error_budget_remaining = 1 - (实际错误率 / 允许错误率)
1 - (
sum(rate(http_requests_total{status=~"5.."}[7d]))
/ sum(rate(http_requests_total[7d]))
) / 0.01 # 允许 1% 错误率

统一的标签体系

指标和追踪数据应该使用一致的标签体系,便于跨系统关联分析。推荐使用以下标准标签:

  • service:服务名称
  • version:服务版本
  • env:环境(prod、staging、dev)
  • region:部署区域
  • host:主机名或 Pod 名
# OpenTelemetry 资源配置
otel.resource.attributes:
service.name: order-service
service.version: 1.2.3
deployment.environment: production
cloud.region: us-east-1
k8s.cluster.name: prod-cluster

小结

本章我们学习了分布式系统可观测性的核心知识:

  1. 可观测性三大支柱:日志、指标、分布式追踪,三者相互补充,共同构成完整的可观测性体系

  2. 分布式追踪:Trace 和 Span 的概念,上下文传播机制,Jaeger 追踪系统,采样策略

  3. 指标监控:Counter、Gauge、Histogram 三种指标类型,Prometheus 的拉模型架构,PromQL 查询语言,告警规则设计

  4. 日志管理:结构化日志的优势,ELK Stack 日志收集架构,日志与追踪的关联

  5. OpenTelemetry:统一可观测性标准的价值,API 和 SDK 分层设计,零侵入的 Java Agent 集成

  6. 最佳实践:RED 和 USE 方法论,SLO 驱动的可靠性管理,统一的标签体系

可观测性不是可有可无的附加功能,而是分布式系统的必要组成部分。在系统设计阶段就应该考虑可观测性,而不是在出现问题后才亡羊补牢。