【架构实战】可观测性体系:从监控到全链路追踪

【架构实战】可观测性体系:从监控到全链路追踪

字数统计:约3700字

一、真实故事引入:从"系统正常"到"用户投诉"的谜题

2024年夏天,我们的电商系统上线了"下单自动发券"的新功能,上线当天就有用户反馈:"下单要转10多秒才能成功,有时候还失败。" 我们第一时间查看监控大盘:订单服务的CPU使用率不到30%,内存充足,QPS只有200多,完全在正常范围内,错误率也是0。所有监控指标都显示"系统正常",但用户就是觉得慢。

那时候我们的可观测性体系只有监控指标,没有全链路追踪,就像医生只给病人量体温,体温正常就认为没病,却不知道病人其实胃疼。后来我们紧急引入了OpenTelemetry+Jaeger的全链路追踪体系,终于找到了问题根源:用户下单时,订单服务会调用优惠券服务查询可用券,优惠券服务又要查询数据库,而数据库的商品ID字段没有加索引,每次查询要2秒多,一个下单流程要调用3次优惠券接口,光这部分就花了6秒,加上其他链路,总耗时超过10秒。

给数据库加上索引后,单次查询降到50毫秒以内,下单总耗时降到了1.5秒以内,用户投诉立刻消失了。这次经历让我深刻意识到:监控只是可观测性的冰山一角,只有指标、日志、追踪三者结合,才能真正理解系统的运行状态。

二、概念原理:可观测性到底是什么?

2.1 可观测性的定义

可观测性(Observability)是指通过观察系统的外部输出( telemetry 数据)来理解系统内部状态的能力。和监控不同:

  • 监控:关注"系统是不是正常",是被动的,预先定义好要监控的指标,超标就告警
  • 可观测性:关注"系统为什么不正常",是主动的,可以通过任意维度的数据查询,定位问题的根因

2.2 可观测性三大支柱

  1. 指标(Metrics):时序数值数据,比如CPU使用率、QPS、接口耗时、错误率等,适合监控系统的整体状态和趋势
  2. 日志(Logs):结构化的文本记录,比如请求日志、错误堆栈、业务日志等,适合排查具体问题
  3. 追踪(Traces):记录一个请求从入口到出口的完整调用链路,包括调用的所有服务、数据库、缓存、消息队列等,适合定位分布式系统中的性能瓶颈

三者是互补关系,不能互相替代:指标告诉你"有问题",日志告诉你"哪里报错了",追踪告诉你"请求经过了哪些环节,哪里慢了"。

2.3 全链路追踪核心原理

全链路追踪的核心是Trace(追踪)和Span(跨度):

  • Trace :一个完整的请求链路,用一个全局唯一的traceId标识
  • Span :链路中的一个操作单元,比如一次服务调用、一次数据库查询、一次缓存读取,每个Span有唯一的spanId,并且有父Span的parentSpanId,形成调用树
  • 上下文传播 :每次服务调用时,把traceIdspanId传递到下游服务,保证整个链路可以串联起来

主流的全链路追踪标准有OpenTracing和OpenTelemetry,现在OpenTelemetry已经成为CNCF的孵化项目,是未来的主流标准。

三、配置代码:从零搭建可观测性体系

我们的技术栈是Spring Boot + Spring Cloud微服务,K8s部署,可观测性体系组件选型:

  • 指标:Micrometer + Prometheus + Grafana
  • 日志:Logback + Filebeat + Elasticsearch + Kibana
  • 追踪:OpenTelemetry + Jaeger

3.1 指标采集配置(Spring Boot + Micrometer + Prometheus)

1. 添加依赖(pom.xml)

xml 复制代码
<dependencies>
    <!-- Micrometer核心 -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-core</artifactId>
    </dependency>
    <!-- Prometheus exporter -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
</dependencies>

2. 配置暴露Prometheus端点(application.yml)

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: prometheus, health, info, metrics
  endpoint:
    prometheus:
      enabled: true
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: ${spring.application.name}  # 给所有指标加应用名标签

3. Prometheus配置(prometheus.yml)

yaml 复制代码
global:
  scrape_interval: 15s
scrape_configs:
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['coupon-service:8080', 'order-service:8080']  # 微服务地址
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance

3.2 日志配置(Logback + traceId注入)

1. 添加依赖(pom.xml)

xml 复制代码
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.4</version>
</dependency>

2. Logback配置(logback-spring.xml),输出JSON格式日志,包含traceId和spanId:

xml 复制代码
<configuration>
    <appender name="json" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <!-- 注入OpenTelemetry的traceId和spanId到日志 -->
            <includeMdcKeyName>traceId</includeMdcKeyName>
            <includeMdcKeyName>spanId</includeMdcKeyName>
            <includeMdcKeyName>spanId</includeMdcKeyName>
            <customFields>{"application":"coupon-service","env":"prod"}</customFields>
        </encoder>
    </appender>
    <root level="info">
        <appender-ref ref="json" />
    </root>
</configuration>

3.3 全链路追踪配置(OpenTelemetry + Jaeger)

1. 添加OpenTelemetry依赖(pom.xml)

xml 复制代码
<dependencies>
    <!-- OpenTelemetry Spring Boot Starter -->
    <dependency>
        <groupId>io.opentelemetry.instrumentation</groupId>
        <artifactId>opentelemetry-spring-boot-starter</artifactId>
        <version>1.28.0</version>
    </dependency>
    <!-- Jaeger Exporter -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-jaeger</artifactId>
        <version>1.28.0</version>
    </dependency>
</dependencies>

2. OpenTelemetry配置(application.yml)

yaml 复制代码
otel:
  traces:
    exporter: jaeger
  exporter:
    jaeger:
      endpoint: http://jaeger-collector:14250  # Jaeger Collector地址
  resource:
    attributes:
      service.name: coupon-service
      deployment.environment: prod
  javaagent:
    enabled: true  # 启用javaagent自动埋点

3. 部署Jaeger(Docker方式)

bash 复制代码
docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -e COLLECTOR_OTLP_ENABLED=true \
  -p 16686:16686 \  # UI端口
  -p 4317:4317 \    # OTLP gRPC端口
  -p 4318:4318 \    # OTLP HTTP端口
  -p 9411:9411 \    # Zipkin端口
  jaegertracing/all-in-one:1.47

3.4 Filebeat配置(日志采集到Elasticsearch)

yaml 复制代码
filebeat.inputs:
  - type: container
    paths:
      - /var/log/containers/*.log
    processors:
      - add_kubernetes_metadata:
          host: ${NODE_NAME}
          matchers:
            - logs_path:
                logs_path: "/var/log/containers/"
output.elasticsearch:
  hosts: ["elasticsearch:9200"]
  username: "elastic"
  password: "changeme"

四、实战案例:定位下单慢的完整过程

当用户反馈下单慢时,我们的排查流程如下:

4.1 第一步:看指标大盘(Grafana)

打开订单服务的Grafana仪表盘,发现:

  • QPS:200,正常
  • 平均响应时间:8秒,远高于正常的500毫秒
  • 错误率:0%,没有报错

指标只能告诉我们"订单服务响应慢",但不知道慢在哪里。

4.2 第二步:查全链路追踪(Jaeger)

在Jaeger UI中搜索service=order-service,找到耗时最长的trace,发现:

  • 订单服务的/api/order/create接口总耗时8.2秒
  • 其中调用coupon-service/api/coupon/query接口花了6秒,占总耗时的73%
  • 优惠券服务的这个接口,又调用了数据库的select * from coupons where product_id=?,单次查询花了2秒

4.3 第三步:查日志(Kibana)

在Kibana中搜索这个trace的traceId,找到优惠券服务的日志,发现查询语句确实是select * from coupons where product_id=?,且product_id字段没有索引。

4.4 第四步:解决问题

给优惠券表的product_id字段加索引:

sql 复制代码
ALTER TABLE coupons ADD INDEX idx_product_id (product_id);

重新部署后,数据库查询降到50毫秒,下单总耗时降到1.2秒,问题解决。

整个排查过程从原来的平均2小时,缩短到15分钟,这就是可观测性体系的威力。

五、踩坑实录:可观测性落地过程中的那些坑

5.1 追踪数据量太大,存储成本爆炸

  • 现象:上线全链路追踪一周后,Jaeger的存储(用的是Elasticsearch)占了500GB,成本太高

  • 原因:默认是100%采样,所有的请求都生成trace,数据量太大

  • 解决 :调整采样率,只采样10%的普通请求,100%采样错误请求和高延迟请求(>1秒):

    yaml 复制代码
    otel:
      traces:
        sampler: parentbased_traceidratio
        sampler.arg: 0.1  # 10%采样

    同时配置Jaeger的过期策略,只保留7天的追踪数据。

5.2 跨线程调用丢失traceId

  • 现象 :用@Async异步执行的逻辑,在日志和追踪中找不到traceId

  • 原因:Spring的异步线程没有传递MDC上下文,OpenTelemetry的上下文也丢失了

  • 解决 :配置TaskDecorator传递上下文:

    java 复制代码
    @Configuration
    public class AsyncConfig {
        @Bean
        public TaskExecutor taskExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setTaskDecorator(new ContextCopyingTaskDecorator());
            executor.initialize();
            return executor;
        }
    }

    或者用OpenTelemetry的Context.current()手动传递上下文。

5.3 日志和追踪上下文不关联

  • 现象:日志中有traceId,但Jaeger中搜不到对应的trace
  • 原因:日志中的traceId是十六进制,而Jaeger中的traceId是十进制,格式不一致
  • 解决:统一traceId的格式,配置OpenTelemetry输出十六进制的traceId,日志中也用同样的格式,或者在日志中同时输出十进制和十六进制的traceId。

5.4 指标维度太多,Prometheus查询慢

  • 现象 :Prometheus的查询经常超时,比如查http_server_requests_seconds_count要10多秒
  • 原因 :指标中加入了太多高基数标签,比如user_idorder_id,导致时间序列爆炸
  • 解决 :清理不必要的标签,只保留低基数的标签(如servicemethodstatus_code),高基数的标签放到日志中。

5.5 OpenTelemetry和Jaeger版本不兼容

  • 现象 :追踪数据上报时报Unsupported version错误

  • 原因:OpenTelemetry 1.28.0用的是Jaeger的Thrift协议,而Jaeger 1.35.0默认用的是gRPC协议

  • 解决 :升级Jaeger到1.47.0,或者配置OpenTelemetry用Jaeger的gRPC exporter:

    yaml 复制代码
    otel:
      exporter:
        jaeger:
          endpoint: http://jaeger-collector:4317  # gRPC端口
          protocol: grpc

六、总结与思考

6.1 核心总结

可观测性体系的价值不是"锦上添花",而是分布式系统的"眼睛":

  • 指标:告诉我们系统"有没有病"
  • 日志:告诉我们系统"哪里不舒服"
  • 追踪:告诉我们系统"病因在哪里"

从监控到可观测性,是从"被动告警"到"主动诊断"的升级,对于微服务架构来说是必备能力。

6.2 思考题

  1. 如何平衡可观测性的成本和收益?比如小团队要不要上全链路追踪?
  2. 如果系统中的请求量非常大(比如10万QPS),如何优化可观测性体系的性能?
  3. 可观测性数据和业务数据如何结合?比如通过traceId关联订单号和用户ID,快速定位某个用户的问题。

6.3 个人观点

很多团队觉得可观测性体系"太复杂""成本太高",但我想说的是:没有可观测性的微服务,就像在黑盒子里跑,出了问题只能靠猜

落地可观测性不需要一步到位,可以分阶段:

  1. 第一阶段:先上监控指标,保证知道系统"挂没挂"
  2. 第二阶段:上结构化日志,保证能查到错误堆栈
  3. 第三阶段:上全链路追踪,保证能定位性能瓶颈

另外,不要盲目追求"全量采集",根据实际业务需求调整采样率,避免不必要的成本浪费。对于核心业务(比如支付、下单),可以用全采样;对于非核心业务(比如推荐、评论),可以用低采样。

最后,可观测性不是某个人的工作,而是整个团队的工作:开发人员要打日志、传上下文,运维人员要维护可观测性组件,测试人员要验证可观测性是否生效。只有全员参与,才能构建真正有效的可观测性体系。

相关推荐
菩提树下的凡夫1 小时前
FACE 与 AUTOSAR 开放架构标准的比较分析
架构
网管NO.11 小时前
SQL 日期函数全套精讲!时间格式化、日期加减、年月日提取,做日报周报直接套用
数据库·sql
杨云龙UP1 小时前
Linux 根分区被日志吃满?一次 58G Broker 日志清理实战_2026-05-20
linux·运维·服务器·数据库·hdfs·apache
sdk大全1 小时前
Studio 3T for MongoDB 2025.13.0
数据库·mongodb
码农阿豪1 小时前
平替MongoDB:金仓多模数据库助力电子证照国产化实践
数据库·mongodb
罗超驿1 小时前
22.深入剖析JDBC架构:从原生API到企业级数据交互核心
java·数据库·mysql·面试
易辰君1 小时前
【数据库】MongoDB深度解析与Python操作指南:从安装到实战操作全覆盖
数据库·mongodb
一直有一个ac的梦想1 小时前
cmu15445 2025fall lec 18 transactions with two-phase lock
java·开发语言·数据库
身如柳絮随风扬2 小时前
Redis 集群脑裂深度剖析:成因、危害与防丢失策略
数据库