一、凌晨三点的那次宕机,让我重新审视监控体系
2020年双11当天凌晨3点,支付服务出现间歇性失败。
我们当时的监控只有两类:CPU/内存/磁盘 + 业务错误日志。
问题来了------监控显示一切正常,CPU没打满、内存没泄漏、磁盘没满、错误日志也看不到。但用户疯狂投诉"支付失败"。最后排查发现,是某个服务的线程池队列积压,导致请求超时。
这种"没有指标异常但业务已经失败"的情况,传统监控根本发现不了。
这就是可观测性(Observability)问题的经典场景。Google SRE团队提出的可观测性三支柱------日志(Logging)、指标(Metrics)、链路追踪(Tracing)------就是为了解决这类问题。
但我必须说:三支柱不是银弹,融合才是关键。 单一支柱都有局限,只有把三者关联起来,才能形成完整的可观测性体系。
二、可观测性三支柱的本质区别
2.1 三个支柱的核心价值
指标(Metrics):可聚合的数值型数据,用于监控整体趋势和异常检测。
日志(Logging):离散的事件记录,用于定位具体问题。
链路追踪(Tracing):请求在分布式系统中的完整路径,用于分析调用关系和性能瓶颈。
【指标】告诉你"系统在某个时刻整体表现如何"
【日志】告诉你"那一刻具体发生了什么"
【链路】告诉你"问题在哪个环节、消耗了多少时间"
2.2 三者能力对比
| 维度 | 指标 | 日志 | 链路 |
|---|---|---|---|
| 数据形态 | 数值型时间序列 | 结构化/非结构化文本 | 调用链+Span |
| 存储成本 | 低(可压缩) | 高(全文索引) | 中(按需采样) |
| 查询方式 | 维度过滤、聚合 | 全文检索、字段过滤 | TraceId串联 |
| 典型工具 | Prometheus、InfluxDB | ELK、Loki | Jaeger、Zipkin、SkyWalking |
| 适用场景 | 趋势监控、告警 | 问题定位、审计 | 性能分析、调用链 |
| 数据量 | 每秒数千 | 每秒数万至百万 | 每秒数千至数万 |
2.3 单一支柱的局限性
仅靠指标的盲点:指标只能反映"聚合后的数字",无法回答"为什么这个数字异常"。
仅靠日志的痛点:日志能告诉你"哪里出错了",但无法回答"整体健康度如何"和"调用关系是什么"。
仅靠链路的不足:链路数据量太大,全量采集成本高,且无法覆盖"业务逻辑内部的细节"。
结论:必须三支柱融合,缺一不可。
三、统一上下文:TraceId贯穿三支柱
3.1 为什么需要统一TraceId
三支柱融合的核心是关联------能够从一个指标异常,跳转到对应的日志和链路。
没有TraceId的痛点:
- 看到"支付服务P99延迟5秒"的告警
- 去查日志,几百万条记录,无法定位是哪一笔支付
- 即使找到日志,也不知道调用链路是什么
有TraceId的效果:
- 看到告警,点击跳转到该时刻的链路数据
- 在链路中定位到慢的Span
- 根据Span的TraceId查询对应日志
- 秒级定位问题
3.2 TraceId生成与传播
java
/**
* Trace上下文管理
*/
public class TraceContext {
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
private static final ThreadLocal<String> SPAN_ID = new ThreadLocal<>();
public static String getTraceId() {
return TRACE_ID.get();
}
public static void setTraceId(String traceId) {
TRACE_ID.set(traceId);
}
public static String newTraceId() {
// 32位UUID,保证全局唯一
return UUID.randomUUID().toString().replace("-", "");
}
}
/**
* HTTP过滤器,提取或生成TraceId
*/
@Component
public class TraceFilter implements Filter {
public static final String TRACE_HEADER = "X-Trace-Id";
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) {
HttpServletRequest req = (HttpServletRequest) request;
// 优先从Header获取(上游调用方传入)
String traceId = req.getHeader(TRACE_HEADER);
if (StringUtils.isEmpty(traceId)) {
traceId = TraceContext.newTraceId();
}
TraceContext.setTraceId(traceId);
// 响应Header也带上,方便客户端追踪
((HttpServletResponse) response).setHeader(TRACE_HEADER, traceId);
try {
chain.doFilter(request, response);
} finally {
TraceContext.clear();
}
}
}
/**
* 日志MDC集成TraceId
*/
public class LogConfig {
@Bean
public LayoutWrappingEncoder<ILoggingEvent> logEncoder() {
// 在日志中自动输出TraceId
return new LayoutWrappingEncoder<>() {
@Override
public byte[] encode(ILoggingEvent event) {
// 注入TraceId到MDC
String traceId = TraceContext.getTraceId();
if (traceId != null) {
event.getMDCPropertyMap().put("traceId", traceId);
}
return super.encode(event);
}
};
}
}
3.3 跨服务TraceId传播
java
/**
* Feign拦截器,传递TraceId
*/
@Component
public class FeignTraceInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String traceId = TraceContext.getTraceId();
if (traceId != null) {
template.header(TraceFilter.TRACE_HEADER, traceId);
}
}
}
/**
* RocketMQ生产者,消息携带TraceId
*/
@Component
public class MqTraceInterceptor implements MessagePostProcessor {
@Override
public Message postProcessMessage(Message message) {
String traceId = TraceContext.getTraceId();
if (traceId != null) {
// 放入RocketMQ的Trace属性
message.putUserProperty("X-Trace-Id", traceId);
}
return message;
}
}
// 消费者端提取TraceId
@RocketMQMessageListener(topic = "order-events", consumerGroup = "inventory-group")
public class InventoryConsumer implements RocketMQListener<String> {
@Override
public void onMessage(Message message) {
String traceId = message.getUserProperty("X-Trace-Id");
TraceContext.setTraceId(traceId);
try {
// 业务处理,日志自动带上TraceId
inventoryService.deduct(message);
} finally {
TraceContext.clear();
}
}
}
四、指标体系:业务与系统的统一表达
4.1 RED方法论
RED方法是服务监控的事实标准:
- Rate(请求速率):每秒请求数
- Errors(错误数):失败请求数/错误率
- Duration(耗时):请求处理时长(P50/P95/P99)
yaml
# Prometheus配置应用指标采集
metrics:
enabled: true
tags:
application: order-service
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5, 0.95, 0.99
sla:
http.server.requests: 100ms, 500ms, 1s
# 业务自定义指标
@Timed(value = "order.create", description = "创建订单耗时")
public Order createOrder(OrderRequest request) {
// 业务逻辑
}
4.2 USE方法论(资源视角)
USE方法关注系统资源:
-
Utilization(使用率):资源被使用的时间比例
-
Saturation(饱和度):资源排队程度
-
Errors(错误事件数):错误计数
CPU使用率 = CPUUtilization(node_cpu_seconds_total)
内存饱和度 = node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
磁盘IO使用率 = rate(node_disk_io_time_seconds_total[5m])
4.3 业务指标与系统指标的关联
关键原则:业务指标的异常往往能在系统指标上找到根因。
java
/**
* 业务指标埋点
*/
@Component
public class BusinessMetrics {
private final MeterRegistry registry;
/**
* 订单创建成功指标
*/
public void recordOrderCreated(String channel, BigDecimal amount) {
// 业务指标带维度(渠道、金额区间等)
registry.counter("order.created.total",
"channel", channel,
"amount_range", getAmountRange(amount))
.increment();
}
/**
* 支付转化漏斗
*/
public void recordPaymentFunnel(String stage, String userType) {
registry.counter("payment.funnel",
"stage", stage, // CREATE_ORDER, PAY, SUCCESS
"user_type", userType)
.increment();
}
}
五、链路追踪:分布式调用的可视化
5.1 OpenTelemetry标准化
OpenTelemetry(OTel)已经成为可观测性数据采集的事实标准,统一了指标、日志、链路的采集规范。
java
/**
* OpenTelemetry SDK配置
*/
@Bean
public OpenTelemetry openTelemetry() {
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(
ResourceAttributes.SERVICE_NAME, "order-service",
ResourceAttributes.SERVICE_VERSION, "1.0.0"
)));
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(
OtlpGrpcSpanExporter.builder()
.setEndpoint("http://jaeger:4317")
.build())
.build())
.setResource(resource)
.build();
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.build();
}
/**
* 业务代码中创建Span
*/
@Service
public class OrderService {
private final Tracer tracer;
public Order createOrder(OrderRequest request) {
Span span = tracer.spanBuilder("createOrder")
.setAttribute("order.amount", request.getAmount())
.setAttribute("user.id", request.getUserId())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 业务逻辑
Inventory inventory = checkInventory(request);
BigDecimal payment = processPayment(request);
return persistOrder(request, inventory, payment);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
}
}
5.2 采样策略
全量采集成本太高,需要智能采样:
yaml
# 链路采样配置
sampling:
# 基础采样率
default_rate: 0.1 # 10%
# 错误请求全量采集
error:
sample_rate: 1.0 # 100%
# 慢请求全量采集
slow:
threshold: 1s
sample_rate: 1.0
# 关键业务全量采集
critical_paths:
- path: "/api/payment/**"
sample_rate: 1.0
Tail-based Sampling(基于尾部的采样):
- 全部请求先暂存
- 等请求结束后,根据实际结果(成功/失败/慢请求)决定是否保留
- 优点:保证关键请求不丢失
- 缺点:需要额外存储
5.3 Baggage与Context Propagation
java
/**
* 业务上下文跨服务传递
*/
public void processOrder(OrderRequest request) {
// 业务上下文(Baggage)
Baggage.current().toBuilder()
.put("userId", request.getUserId())
.put("userTier", request.getUserTier()) // VIP/普通用户
.put("orderChannel", request.getChannel())
.build()
.makeCurrent();
// 后续所有Span都自动携带这些属性
// 可以在链路系统中按"userTier=VIP"过滤
}
日志体系:结构化与智能化
6.1 结构化日志
传统字符串日志:
2026-07-20 10:23:45 ERROR 订单创建失败: userId=12345, amount=99.00, reason=库存不足
结构化日志(推荐):
json
{
"timestamp": "2026-07-20T10:23:45.123Z",
"level": "ERROR",
"service": "order-service",
"traceId": "abc123def456",
"spanId": "789xyz",
"userId": "12345",
"amount": 99.00,
"errorType": "INVENTORY_INSUFFICIENT",
"message": "订单创建失败",
"stack": "..."
}
java
/**
* 结构化日志输出
*/
@Slf4j
public class OrderService {
public void createOrder(OrderRequest request) {
// MDC自动注入上下文
MDC.put("traceId", TraceContext.getTraceId());
MDC.put("userId", request.getUserId());
try {
log.info("创建订单开始 amount={} itemCount={}",
request.getAmount(), request.getItems().size());
// 业务逻辑
} catch (Exception e) {
// 结构化异常日志
log.error("创建订单失败 errorType={} amount={}",
"INVENTORY_INSUFFICIENT", request.getAmount(), e);
} finally {
MDC.clear();
}
}
}
6.2 日志分级与采样
问题:全量INFO日志成本高,关键ERROR日志可能淹没在海量日志中。
解决方案:
- DEBUG/TRACE:仅在排障时开启,采样1%
- INFO:关键业务节点,采样10%
- WARN:异常但不影响业务,100%保留
- ERROR:业务错误,100%保留+告警
java
/**
* 动态日志级别(远程配置)
*/
@RefreshScope
@Component
public class DynamicLogLevel {
@Value("${logging.level.root:INFO}")
private String logLevel;
@PostConstruct
public void init() {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.getLogger("root").setLevel(Level.toLevel(logLevel));
}
// Nacos/Apollo配置变更时自动刷新
@NacosValue(value = "${logging.level.root:INFO}", autoRefreshed = true)
private String dynamicLevel;
}
6.3 日志与链路的关联
java
/**
* 关键日志自动关联TraceId
*/
public class TraceLogger {
public static void error(String message, Throwable t) {
// 错误日志自动带上TraceId和SpanId
Map<String, Object> context = new HashMap<>();
context.put("traceId", TraceContext.getTraceId());
context.put("spanId", TraceContext.getSpanId());
// 日志平台(ES/Loki)通过traceId建立索引
// 在Grafana/Jaeger中通过traceId跳转
log.error(message, context, t);
}
}
七、三支柱融合的工程实践
7.1 统一数据采集层
┌─────────────────────────────────────────────────────────────┐
│ 统一采集层 │
│ │
│ 应用代码(埋点SDK) │
│ ├── Micrometer(指标) │
│ ├── OpenTelemetry(链路) │
│ └── Logback MDC(日志) │
│ │
│ Agent自动采集 │
│ ├── JVM指标(内存、GC、线程) │
│ ├── HTTP/RPC调用(自动埋点) │
│ └── 数据库访问(自动埋点) │
│ │
└─────────────────────────────────────────────────────────────┘
↓ ↓ ↓
Prometheus Jaeger/Tempo ELK/Loki
(指标存储) (链路存储) (日志存储)
↓ ↓ ↓
┌────────────────────────────────────┐
│ Grafana统一展示 │
│ • 指标异常 → 跳转链路 │
│ • 链路Span → 跳转日志 │
│ • 错误日志 → 跳转链路 │
└────────────────────────────────────┘
7.2 Grafana关联展示
json
{
"title": "订单服务可观测性仪表盘",
"panels": [
{
"type": "graph",
"title": "订单创建QPS",
"targets": [
{
"expr": "rate(order_created_total[5m])",
"datasource": "Prometheus"
}
]
},
{
"type": "logs",
"title": "订单错误日志(自动联动)",
"targets": [
{
"expr": "{service=\"order-service\", level=\"ERROR\"}",
"datasource": "Loki"
}
],
"links": [
{
"title": "查看链路",
"url": "https://jaeger.example.com/search?traceId=${traceId}"
}
]
},
{
"type": "tracing",
"title": "慢链路Top10",
"targets": [
{
"query": "service:order-service duration:>1s",
"datasource": "Jaeger"
}
]
}
]
}
7.3 告警联动
yaml
# Prometheus告警规则(指标异常)
groups:
- name: order_service_alerts
rules:
- alert: OrderCreateSlow
expr: |
histogram_quantile(0.99,
rate(order_create_duration_seconds_bucket[5m])
) > 1
for: 2m
annotations:
summary: "订单创建P99 > 1s"
# 关键:告警中带上TraceId链接
dashboard: "https://grafana.example.com/d/order?traceId={{ $labels.traceId }}"
# 告警处理流程
# 1. Prometheus触发告警
# 2. AlertManager发送到钉钉/飞书
# 3. 告警消息携带Grafana/Jaeger链接
# 4. 运维同学点击链接直接定位问题
八、实战案例:从"看不到"到"秒级定位"
8.1 故障现象
某天生产环境报警:订单创建P99延迟从500ms飙升到3秒。
8.2 三支柱融合定位过程
Step 1:指标定位范围
promql
# 查看订单服务P99延迟变化
histogram_quantile(0.99, rate(order_create_duration_seconds_bucket[5m]))
# 发现异常时间段:14:00-14:15
# 查看各环节耗时分布
sum by (le, step) (rate(order_create_duration_seconds_bucket[5m]))
Step 2:链路定位瓶颈
bash
# 在Jaeger中搜索慢链路
service:order-service duration:>2s
# 发现:库存查询环节耗时占比70%
# 进一步定位到Redis连接获取慢
Step 3:日志确认根因
bash
# 在Loki中通过traceId查询
{service="inventory-service", traceId="abc123def456"}
# 日志显示:
# 14:05:23 WARN Redis connection pool exhausted, waiting...
# 14:05:25 ERROR Redis timeout after 3000ms
根因:Redis连接池配置过小(最大10个),高并发时等待超时。
修复:
yaml
# Lettuce连接池配置
spring.redis.lettuce.pool:
max-active: 50 # 调整为50
max-idle: 20
min-idle: 5
max-wait: 1s # 等待超时1秒
整个定位过程:3分钟 。如果只有指标,看不到根因;如果只有日志,找不到全局;如果只有链路,看不到错误信息。三支柱融合才能快速定位。
九、踩坑总结
9.1 过度埋点
问题:业务代码中埋了上百个指标点,维护成本高。
解决:
- 优先依靠自动埋点(Java Agent字节码增强)
- 业务埋点聚焦核心业务(订单、支付、转化漏斗)
- 定期清理无用指标
9.2 TraceId串联断裂
问题:异步调用、消息队列、线程池切换时TraceId丢失。
解决:
- 线程池使用
TaskDecorator传递TraceId - 消息队列生产者/消费者Header中传递
- 异步任务包装
Runnable/Callable
java
/**
* 线程池TraceId传递
*/
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setTaskDecorator(runnable -> {
String traceId = TraceContext.getTraceId();
return () -> {
try {
TraceContext.setTraceId(traceId);
runnable.run();
} finally {
TraceContext.clear();
}
};
});
return executor;
}
9.3 日志存储成本爆炸
问题:全量INFO日志一个月存储成本几十万。
解决:
- 关键业务INFO,非关键DEBUG降级
- 冷热分离:热数据7天ES,冷数据归档OSS
- 采样存储:DEBUG日志采样1%
9.4 指标基数爆炸
问题:用userId、orderId作为指标label,Prometheus存储爆炸。
解决:
- label值必须是低基数(有限枚举值)
- 高基数字段(用户ID)放入日志/链路,不放入指标
9.5 链路采样丢关键数据
问题:采样率10%,偶发的慢请求被丢了。
解决:
- 使用Tail-based sampling
- 错误/慢请求100%采集
- 关键业务全量采集
十、总结
可观测性三支柱的核心是融合,不是孤立。
关键要点:
- 三支柱定位不同问题:指标看趋势、日志看细节、链路看路径
- TraceId是融合的纽带:贯穿指标、日志、链路
- 统一采集、统一展示、统一告警:避免数据孤岛
- 采样与成本平衡:全量采集不现实,智能采样是关键
- 业务指标与系统指标结合:技术问题反映业务,业务异常根因在技术
未来演进:
- eBPF:内核级可观测性,无需修改应用代码
- AIOps:AI自动异常检测、根因分析
- 统一数据模型:OpenTelemetry统一三大支柱的数据模型
- 可观测性即代码:通过IaC管理监控配置
最后的话:
可观测性是"事后诸葛亮",但好的可观测性体系能让"事后"变得快速且准确。监控告诉你"系统出问题了",可观测性告诉你"系统为什么出问题、问题在哪里、影响范围多大"。
没有可观测性的系统,是蒙着眼睛开车。
今日思考 :
你们系统的可观测性三支柱是怎么融合的?有没有遇到过TraceId串联断裂的情况?欢迎分享实战经验!
作者:架构实战团队
日期:2026-07-20
标签:#可观测性 #监控 #日志 #指标 #链路追踪 #OpenTelemetry