Spring Cloud 微服务可观测性全栈实践

日常工作中很多时候都会讨论微服务可观测性的话题,但在实际项目中落地实施仍然非常困难,经常受限与时间、服务器资源等问题无法落地,正好最近一段时间抽身出来完成了一个微服务可观测性全栈实践,希望能给大家带来一些参考。

一、方案选型背景

为什么不选 SkyWalking?

  1. 错误识别问题:项目内部将异常统一封装为业务 code 错误码返回,HTTP 状态码始终为 200。SkyWalking 基于 HTTP 状态码判断请求是否出错,无法识别业务层面的错误,导致错误率指标完全失真。
  2. 资源开销大:SkyWalking Agent 的 Java Agent 机制会拦截大量字节码,在高并发 Dubbo 调用场景下 CPU 和内存开销显著,对业务性能有可感知的影响。

相比之下,Sleuth + Tempo 方案足够轻量,且错误判定完全由应用侧通过 Span Tag 自定义,不受 HTTP 状态码限制。

为什么不选 Ctld?

我也尝试接入 Ctld,但 Ctld 的版本依赖与当前技术栈(Spring Boot 2.7.18 / Spring Cloud 2021.0.8)存在兼容性冲突,强行适配成本过高,故放弃。


二、整体架构

整体架构图

三条数据线:

数据线 采集端 传输通道 存储 可视化
Traces Sleuth + Brave-Dubbo → Tempo(Zipkin 协议直连) Tempo Grafana
Metrics Tempo Metrics Generator → Prometheus Prometheus Grafana
Logs Promtail → Loki Loki Grafana

三、版本信息与 Maven 依赖

1
2
3
4
5
<properties>
<spring-boot.version>2.7.18</spring-boot.version>
<spring-cloud.version>2021.0.8</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
</properties>

3.1 链路追踪依赖(各微服务引入)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-dubbo</artifactId>
<version>5.13.7</version>
</dependency>

3.2 应用配置

1
2
3
4
5
6
7
8
9
10
11
# application.yml
spring:
application:
name: your-service-name # 服务名,会作为 Tempo 的 service.name
sleuth:
sampler:
probability: 1.0 # 全量采样(生产可按需调低)
zipkin:
base-url: http://tempo:9411 # Tempo 的 Zipkin Receiver 端口
sender:
type: web # 通过 HTTP 上报

3.3 Dubbo 配置

1
2
3
4
5
6
7
# application.yml
# ... 其他 dubbo 配置省略 ...
dubbo:
consumer:
filter: braveConsumerFilter=true # 启用 Brave Dubbo 消费者过滤器
provider:
filter: braveProviderFilter=true # 启用 Brave Dubbo 提供者过滤器

braveConsumerFilter / braveProviderFilterbrave-instrumentation-dubbo 提供,自动在 Dubbo 调用时传递 traceId/spanId。

3.4 Logback 日志配置(关键部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

<configuration>



<appender name="CONSOLE" class="ch.qcloud.cdp.log.LogTraceAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [${springAppName},%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n
</pattern>
</encoder>
</appender>


<appender name="FILE" class="ch.qcloud.cdp.log.LogTraceAppender">
<file>/var/log/spring-boot/${springAppName}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/spring-boot/${springAppName}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [${springAppName},%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n
</pattern>
</encoder>
</appender>


</configuration>

核心要点:%X{traceId} / %X{spanId} 是 Sleuth 自动注入 MDC 的变量,Logback 通过 %X{} 取出并打印到每条日志中,这是后续 Promtail 解析 traceId 并与 Tempo 关联的基础。


四、链路追踪:Sleuth → Tempo(Zipkin 协议直连)

4.1 数据流

1
2
3
4
5
6
7
8
9
10
微服务(Sleuth 生成 traceId/spanId)
│ HTTP POST /api/v2/spans(Zipkin 格式)

Tempo Zipkin Receiver(:9411 原生支持 Zipkin 协议)
│ 存储 + 索引

Tempo 查询
│ Grafana Tempo Data Source

Grafana Explore / Dashboard → 查看完整调用链

Tempo 原生内置 Zipkin Receiver,无需额外部署 Zipkin Server,Sleuth 直接上报即可。

4.2 Tempo 关键配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# tempo.yaml
server:
http_listen_port: 3200

distributor:
receivers:
zipkin: # 接收 Zipkin 格式的 Span
endpoint: 0.0.0.0:9411

ingester:
trace_idle_period: 10s
max_block_duration: 30m

compactor:
compaction:
block_retention: 48h # Trace 保留时间

storage:
trace:
backend: local
local:
path: /tmp/tempo/traces

metrics_generator: # ← 关键:从 Trace 生成指标
storage:
path: /tmp/tempo/generator
remote_write:
- url: http://prometheus:9090/api/v1/write # 指标推送到 Prometheus
send_exemplars: true
traces_storage:
path: /tmp/tempo/generator/traces

4.3 验证 Trace 链路

  1. 启动微服务后,发起一次包含 Dubbo 调用的请求
  2. 查看日志中的 [service-name,traceId,spanId] 是否有值
  3. Grafana 添加 Tempo Data Source(http://tempo:3200),用 traceId 搜索调用链

五、指标体系:Tempo → Prometheus → Grafana

5.1 QPS / 错误率 / 慢请求 的来源

Tempo 的 Metrics Generator 会根据 Span 数据自动聚合生成 RED 指标(Rate / Error / Duration):

指标名 含义
traces_spanmetrics_calls_total 总请求数(QPS = rate)
traces_spanmetrics_calls_total{status_code="error"} 错误请求数
traces_spanmetrics_latency_bucket 请求延迟分布(Histogram)

5.2 Prometheus 记录规则(可选,简化查询)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# prometheus-rules.yml
groups:
- name: service_metrics
rules:
- record: job:qps:rate1m
expr: rate(traces_spanmetrics_calls_total[1m])
- record: job:error_rate:rate1m
expr: |
rate(traces_spanmetrics_calls_total{status_code="error"}[1m])
/
rate(traces_spanmetrics_calls_total[1m])
- record: job:slow_rate:rate1m
expr: |
sum(rate(traces_spanmetrics_latency_bucket{le="+Inf"}[1m]))
-
sum(rate(traces_spanmetrics_latency_bucket{le="1.0"}[1m]))
/
sum(rate(traces_spanmetrics_latency_bucket{le="+Inf"}[1m]))

5.3 Grafana 面板 PromQL

QPS:

1
sum(rate(traces_spanmetrics_calls_total{service_name=~"$service"}[1m]))

错误率:

1
2
3
sum(rate(traces_spanmetrics_calls_total{service_name=~"$service",status_code="error"}[1m]))
/
sum(rate(traces_spanmetrics_calls_total{service_name=~"$service"}[1m]))

慢请求率(耗时 > 1s 为慢请求):

1
2
3
4
5
6
7
(
sum(rate(traces_spanmetrics_latency_bucket{service_name=~"$service",le="+Inf"}[1m]))
-
sum(rate(traces_spanmetrics_latency_bucket{service_name=~"$service",le="1.0"}[1m]))
)
/
sum(rate(traces_spanmetrics_latency_bucket{service_name=~"$service",le="+Inf"}[1m]))

P99 延迟:

1
2
3
histogram_quantile(0.99,
sum(rate(traces_spanmetrics_latency_bucket{service_name=~"$service"}[1m])) by (le)
)

六、日志聚合:Promtail → Loki

6.1 数据流

1
2
3
4
5
6
7
微服务日志文件(/var/log/*.log,含 traceId)
│ Promtail 监听采集

Loki(存储 + 索引)
│ Grafana Loki Data Source
���
Grafana Explore → 按 traceId / 时间 / 关键词搜索日志

6.2 Promtail 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# promtail-config.yaml
server:
http_listen_port: 9080

clients:
- url: http://loki:3100/loki/api/v1/push

scrape_configs:
- job_name: spring-boot-apps
static_configs:
- targets:
- localhost
labels:
job: spring-boot
env: prod
__path__: /var/log/spring-boot/*.log # 日志文件路径
pipeline_stages:
# 从日志中提取 traceId,作为 Loki 的结构化标签
- regex:
expression: '.*\[(?P<app>[^,]*),(?P<traceId>[^,]*),(?P<spanId>[^\]]*)\].*'
- labels:
app:
traceId:

6.3 Tempo ↔ Loki 关联

原理: Sleuth 在日志中注入了 traceId,Loki 通过 traceId 标签索引日志,Grafana 根据相同的 traceId 在 Tempo 和 Loki 之间跳转。

Loki Data Source 配置关键字段(Grafana):

1
2
3
4
5
6
7
Derived fields:
Name: traceId
Regex: traceId=(\w+)
URL: ${__value.raw}
URL Label: Trace
Internal link: [x] 启用
Data source: Tempo(关联到 Tempo Data Source)

这样在 Grafana 中查看 Loki 日志时,每条日志旁边的 traceId 会变成一个可点击的链接,点击后直接跳转到 Tempo 的对应调用链视图。反之在 Tempo 的 Span 详情中也能看到 Related logs


七、告警链路:Prometheus → Alertmanager → PrometheusAlert → 企业微信

7.1 整体链路

1
2
3
4
5
6
7
8
9
10
Prometheus(告警规则触发)
│ alerting rule → firing

Alertmanager(告警分组 / 抑制 / 静默)
│ webhook 转发

PrometheusAlert(模板渲染 + 多通道适配)
│ 企业微信机器人 / 应用消息

企业微信群 / 个人

为什么引入 PrometheusAlert? Alertmanager 原生不支持企业微信。PrometheusAlert 作为中间层,接收 Alertmanager 的 webhook,按照自定义模板渲染告警内容后,通过企业微信机器人、应用消息或微信客服通道完成推送。

7.2 Prometheus 告警规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# prometheus-alert-rules.yml
groups:
- name: service_alerts
rules:
# --- 错误率告警 ---
- alert: HighErrorRate
expr: |
sum(rate(traces_spanmetrics_calls_total{status_code="error"}[5m]))
/
sum(rate(traces_spanmetrics_calls_total[5m]))
> 0.01
for: 2m
labels:
severity: critical
annotations:
summary: "服务 {{ $labels.service_name }} 错误率过高"
description: "错误率 {{ $value | humanizePercentage }},已持续 2 分钟"

# --- 慢请求率告警 ---
- alert: HighSlowRequestRate
expr: |
(
sum(rate(traces_spanmetrics_latency_bucket{le="+Inf"}[5m]))
-
sum(rate(traces_spanmetrics_latency_bucket{le="1.0"}[5m]))
)
/
sum(rate(traces_spanmetrics_latency_bucket{le="+Inf"}[5m]))
> 0.05
for: 3m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.service_name }} 慢请求率过高"
description: "慢请求(>1s) 占比 {{ $value | humanizePercentage }},已持续 3 分钟"

# --- QPS 突增 / 突降告警(可选) ---
- alert: QpsAnomaly
expr: |
abs(
sum(rate(traces_spanmetrics_calls_total[5m]))
-
sum(rate(traces_spanmetrics_calls_total[5m] offset 1h))
)
/
sum(rate(traces_spanmetrics_calls_total[5m] offset 1h))
> 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.service_name }} QPS 波动超过 50%"

7.3 Alertmanager 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# alertmanager.yml
global:
resolve_timeout: 5m

route:
receiver: 'prometheus-alert' # 默认接收器
group_by: ['alertname', 'service']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h

receivers:
- name: 'prometheus-alert'
webhook_configs:
- url: 'http://prometheus-alert:8080/prometheusalert?type=wechat&tpl=wechat-template'
send_resolved: true # 恢复通知也发送

7.4 PrometheusAlert 配置要点

1
2
3
4
5
6
7
# PrometheusAlert conf/app.conf 关键配置

# 企业微信机器人 Webhook
wechat_webhook_url=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY

# 自定义告警模板路径
custom_tpl_dir=/app/custom-templates

企业微信告警模板示例(wechat-template.tpl):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{{ $var := .externalURL}}
{{ range $k, $v := .alerts }}
{{ if eq $v.status "resolved" }}
## ✅ 告警恢复
**告警名称:** {{ $v.labels.alertname }}
**服务名称:** {{ $v.labels.service_name }}
**恢复时间:** {{ $v.endsAt }}
{{ else }}
## 🚨 告警触发
**级别:** {{ $v.labels.severity }}
**告警名称:** {{ $v.labels.alertname }}
**服务名称:** {{ $v.labels.service_name }}
**告警详情:** {{ $v.annotations.description }}
**触发时间:** {{ $v.startsAt }}
{{ end }}
---
{{ end }}

八、Docker Compose 一键部署(参考)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
version: '3.8'
services:
# ---- 链路追踪(Sleuth 直连 Tempo 的 Zipkin Receiver) ----
tempo:
image: grafana/tempo:2.4
volumes:
- ./tempo.yaml:/etc/tempo.yaml
- ./tempo-data:/tmp/tempo
command: ["-config.file=/etc/tempo.yaml"]
ports:
- "3200:3200"
- "9411:9411" # Zipkin Receiver,Sleuth 直连此端口

# ---- 指标 ----
prometheus:
image: prom/prometheus:v2.47
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus-rules.yml:/etc/prometheus/rules.yml
- ./prometheus-alert-rules.yml:/etc/prometheus/alert-rules.yml
- prometheus-data:/prometheus
ports:
- "9090:9090"

# ---- 日志 ----
loki:
image: grafana/loki:2.9
ports:
- "3100:3100"

promtail:
image: grafana/promtail:2.9
volumes:
- ./promtail-config.yaml:/etc/promtail/config.yml
- /var/log:/var/log:ro
command: ["-config.file=/etc/promtail/config.yml"]

# ---- 告警 ----
alertmanager:
image: prom/alertmanager:v0.26
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
ports:
- "9093:9093"

prometheus-alert:
image: feiyu563/prometheus-alert:latest
volumes:
- ./prometheus-alert-conf:/app/conf
ports:
- "8080:8080"

# ---- 可视化 ----
grafana:
image: grafana/grafana:10.1
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
volumes:
- grafana-data:/var/lib/grafana

volumes:
prometheus-data:
grafana-data:
tempo-data:

九、关键验证清单

序号 验证项 预期结果
1 微服务日志中可见 [appName,traceId,spanId] traceId 非空
2 Grafana Tempo Data Source 通过 traceId 检索调用链 调用链展示完整,含 Dubbo Span
3 Prometheus 中存在 traces_spanmetrics_* 指标 指标有数据点
4 Grafana Dashboard 中 QPS / 错误率 / 慢请求 面板有数据 曲线正常
5 Loki 可按 traceId 检索日志 返回对应 Trace 的日志行
6 Tempo Span 详情页可看到 Related logs 点击跳转到 Loki 日志
7 触发错误率 > 1% → 企业微信收到告警 告警内容准确
8 错误率恢复 → 企业微信收到恢复通知 [Resolved] 消息正常

十、注意事项

  1. 采样率:生产环境 Sleuth 采样率建议 0.1(10%),全量采样对 Tempo 存储压力大。
  2. Tempo Metrics Generator:是 Tempo 的 optional 组件,需显式开启才能生成 traces_spanmetrics_* 指标推送到 Prometheus。
  3. Tempo 直连:Tempo 内置 Zipkin Receiver(端口 9411),Sleuth 配置 zipkin.base-url 指向 Tempo 即可,无需部署 Zipkin Server。
  4. PrometheusAlert 版本:推荐 v4.0+,对企微支持更完善。
  5. 日志格式:必须确保 traceId 以结构化方式出现在日志中(推荐 JSON 格式,方便 Promtail pipeline 解析)。
  6. 关联生效前提:Grafana 中 Loki Data Source 必须正确配置 Derived fields 关联到 Tempo Data Source。
  7. Dubbo 过滤器braveConsumerFilter / braveProviderFilter 必须配置,否则 Dubbo 调用不会传递 traceId,链路会断。

(注:内容由 AI 辅助编写)

[越努力,越幸运!]