【架构实战】可观测性体系:从监控到全链路追踪
字数统计:约3700字
一、真实故事引入:从"系统正常"到"用户投诉"的谜题
2024年夏天,我们的电商系统上线了"下单自动发券"的新功能,上线当天就有用户反馈:"下单要转10多秒才能成功,有时候还失败。" 我们第一时间查看监控大盘:订单服务的CPU使用率不到30%,内存充足,QPS只有200多,完全在正常范围内,错误率也是0。所有监控指标都显示"系统正常",但用户就是觉得慢。
那时候我们的可观测性体系只有监控指标,没有全链路追踪,就像医生只给病人量体温,体温正常就认为没病,却不知道病人其实胃疼。后来我们紧急引入了OpenTelemetry+Jaeger的全链路追踪体系,终于找到了问题根源:用户下单时,订单服务会调用优惠券服务查询可用券,优惠券服务又要查询数据库,而数据库的商品ID字段没有加索引,每次查询要2秒多,一个下单流程要调用3次优惠券接口,光这部分就花了6秒,加上其他链路,总耗时超过10秒。
给数据库加上索引后,单次查询降到50毫秒以内,下单总耗时降到了1.5秒以内,用户投诉立刻消失了。这次经历让我深刻意识到:监控只是可观测性的冰山一角,只有指标、日志、追踪三者结合,才能真正理解系统的运行状态。
二、概念原理:可观测性到底是什么?
2.1 可观测性的定义
可观测性(Observability)是指通过观察系统的外部输出( telemetry 数据)来理解系统内部状态的能力。和监控不同:
- 监控:关注"系统是不是正常",是被动的,预先定义好要监控的指标,超标就告警
- 可观测性:关注"系统为什么不正常",是主动的,可以通过任意维度的数据查询,定位问题的根因
2.2 可观测性三大支柱
- 指标(Metrics):时序数值数据,比如CPU使用率、QPS、接口耗时、错误率等,适合监控系统的整体状态和趋势
- 日志(Logs):结构化的文本记录,比如请求日志、错误堆栈、业务日志等,适合排查具体问题
- 追踪(Traces):记录一个请求从入口到出口的完整调用链路,包括调用的所有服务、数据库、缓存、消息队列等,适合定位分布式系统中的性能瓶颈
三者是互补关系,不能互相替代:指标告诉你"有问题",日志告诉你"哪里报错了",追踪告诉你"请求经过了哪些环节,哪里慢了"。
2.3 全链路追踪核心原理
全链路追踪的核心是Trace(追踪)和Span(跨度):
- Trace :一个完整的请求链路,用一个全局唯一的
traceId标识 - Span :链路中的一个操作单元,比如一次服务调用、一次数据库查询、一次缓存读取,每个Span有唯一的
spanId,并且有父Span的parentSpanId,形成调用树 - 上下文传播 :每次服务调用时,把
traceId和spanId传递到下游服务,保证整个链路可以串联起来
主流的全链路追踪标准有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秒):
yamlotel: 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_id、order_id,导致时间序列爆炸 - 解决 :清理不必要的标签,只保留低基数的标签(如
service、method、status_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:
yamlotel: exporter: jaeger: endpoint: http://jaeger-collector:4317 # gRPC端口 protocol: grpc
六、总结与思考
6.1 核心总结
可观测性体系的价值不是"锦上添花",而是分布式系统的"眼睛":
- 指标:告诉我们系统"有没有病"
- 日志:告诉我们系统"哪里不舒服"
- 追踪:告诉我们系统"病因在哪里"
从监控到可观测性,是从"被动告警"到"主动诊断"的升级,对于微服务架构来说是必备能力。
6.2 思考题
- 如何平衡可观测性的成本和收益?比如小团队要不要上全链路追踪?
- 如果系统中的请求量非常大(比如10万QPS),如何优化可观测性体系的性能?
- 可观测性数据和业务数据如何结合?比如通过traceId关联订单号和用户ID,快速定位某个用户的问题。
6.3 个人观点
很多团队觉得可观测性体系"太复杂""成本太高",但我想说的是:没有可观测性的微服务,就像在黑盒子里跑,出了问题只能靠猜。
落地可观测性不需要一步到位,可以分阶段:
- 第一阶段:先上监控指标,保证知道系统"挂没挂"
- 第二阶段:上结构化日志,保证能查到错误堆栈
- 第三阶段:上全链路追踪,保证能定位性能瓶颈
另外,不要盲目追求"全量采集",根据实际业务需求调整采样率,避免不必要的成本浪费。对于核心业务(比如支付、下单),可以用全采样;对于非核心业务(比如推荐、评论),可以用低采样。
最后,可观测性不是某个人的工作,而是整个团队的工作:开发人员要打日志、传上下文,运维人员要维护可观测性组件,测试人员要验证可观测性是否生效。只有全员参与,才能构建真正有效的可观测性体系。