【架构实战】可观测性三支柱:日志、指标、链路的融合

一、凌晨三点的那次宕机,让我重新审视监控体系

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%采集
  • 关键业务全量采集

十、总结

可观测性三支柱的核心是融合,不是孤立。

关键要点

  1. 三支柱定位不同问题:指标看趋势、日志看细节、链路看路径
  2. TraceId是融合的纽带:贯穿指标、日志、链路
  3. 统一采集、统一展示、统一告警:避免数据孤岛
  4. 采样与成本平衡:全量采集不现实,智能采样是关键
  5. 业务指标与系统指标结合:技术问题反映业务,业务异常根因在技术

未来演进

  • eBPF:内核级可观测性,无需修改应用代码
  • AIOps:AI自动异常检测、根因分析
  • 统一数据模型:OpenTelemetry统一三大支柱的数据模型
  • 可观测性即代码:通过IaC管理监控配置

最后的话

可观测性是"事后诸葛亮",但好的可观测性体系能让"事后"变得快速且准确。监控告诉你"系统出问题了",可观测性告诉你"系统为什么出问题、问题在哪里、影响范围多大"。

没有可观测性的系统,是蒙着眼睛开车。


今日思考

你们系统的可观测性三支柱是怎么融合的?有没有遇到过TraceId串联断裂的情况?欢迎分享实战经验!


作者:架构实战团队

日期:2026-07-20

标签:#可观测性 #监控 #日志 #指标 #链路追踪 #OpenTelemetry

相关推荐
CCPC不拿奖不改名4 小时前
大模型推理架构与开源生态知识整理
数据库·windows·python·架构·langchain·开源·github
阿里云大数据AI技术5 小时前
阿里云 Elasticsearch 日志采集与加工服务:让日志链路少一串组件,多一份稳定
人工智能·elasticsearch
万点科技码农8 小时前
MCP+A2A协议与AI原生架构:软件定制开发新标准下的西安服务商实力解析
架构·ai-native
Elasticsearch9 小时前
Elastic Observability 中的 Prometheus 指标:你的 PromQL 可以保持不变地运行
elasticsearch
dogstarhuang9 小时前
从 0 到 1 搭建可收费的 API 开放平台(实战)
java·架构·api
GIoT801010 小时前
自动化请求的智能重试策略:指数退避 + 熔断 + IP 轮换
架构
葬送的代码人生10 小时前
从 Vue 到 React:Tailwind CSS 布局 + BFF 代理实战
前端·react.js·架构
Elasticsearch10 小时前
为什么 Elasticsearch 平台是你的 AI 技术栈中缺失的一块拼图
elasticsearch
Elasticsearch10 小时前
数据平台赌注:为什么金融 AI 计划停滞,以及成功者如何扩展规模
elasticsearch