分布式系统可观测性
在分布式系统中,一个请求可能经过数十个服务、跨越多个数据中心。当系统出现问题时,如何快速定位故障根源?如何了解系统内部的运行状态?这就是可观测性要解决的问题。
什么是可观测性
可观测性(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-flagstracestate:可选,包含供应商特定的追踪信息
追踪数据的采集
追踪数据的采集主要有两种方式:代码埋点和自动插桩。
代码埋点需要在业务代码中手动添加追踪逻辑。这种方式精确可控,但侵入性强,维护成本高。
// 使用 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
日志收集的典型流程:
- 应用将日志写入本地文件或标准输出
- Filebeat 监控日志文件,收集新增的日志行
- Filebeat 将日志发送到 Elasticsearch
- 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
小结
本章我们学习了分布式系统可观测性的核心知识:
-
可观测性三大支柱:日志、指标、分布式追踪,三者相互补充,共同构成完整的可观测性体系
-
分布式追踪:Trace 和 Span 的概念,上下文传播机制,Jaeger 追踪系统,采样策略
-
指标监控:Counter、Gauge、Histogram 三种指标类型,Prometheus 的拉模型架构,PromQL 查询语言,告警规则设计
-
日志管理:结构化日志的优势,ELK Stack 日志收集架构,日志与追踪的关联
-
OpenTelemetry:统一可观测性标准的价值,API 和 SDK 分层设计,零侵入的 Java Agent 集成
-
最佳实践:RED 和 USE 方法论,SLO 驱动的可靠性管理,统一的标签体系
可观测性不是可有可无的附加功能,而是分布式系统的必要组成部分。在系统设计阶段就应该考虑可观测性,而不是在出现问题后才亡羊补牢。