Spring Boot 监控与链路追踪完全指南
一、监控与追踪的价值
微服务架构中,一次用户请求可能跨越多个服务。监控和追踪解决两个核心问题:
-
监控(Monitoring):系统现在健不健康?资源够不够用?
-
链路追踪(Tracing):一次请求经过了哪些服务?在哪里慢了?在哪里报错了?
用户请求 → 网关 → 订单服务 → 库存服务 → 数据库
↓
支付服务 → 第三方支付问题:这次请求为什么慢了 3 秒?到底卡在哪一步?
答案:链路追踪告诉你。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、Spring Boot Actuator --- 健康与指标
2.1 什么是 Actuator
Actuator 是 Spring Boot 内置的生产就绪特性模块,提供一系列 HTTP 端点(Endpoints),暴露应用的运行状态、指标、配置信息等。
2.2 依赖引入
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2.3 核心端点
| 端点 | 路径 | 作用 |
|---|---|---|
| health | /actuator/health |
应用健康状态(UP/DOWN) |
| info | /actuator/info |
应用信息(版本、Git 信息等) |
| metrics | /actuator/metrics |
应用指标(JVM、HTTP、自定义) |
| env | /actuator/env |
环境变量和配置属性 |
| loggers | /actuator/loggers |
运行时动态调整日志级别 |
| threaddump | /actuator/threaddump |
线程转储 |
| heapdump | /actuator/heapdump |
堆转储文件下载 |
| prometheus | /actuator/prometheus |
Prometheus 格式指标输出 |
| beans | /actuator/beans |
所有 Spring Bean 列表 |
| mappings | /actuator/mappings |
所有 URL 映射 |
2.4 配置
yaml
# application.yml
management:
# 端点暴露配置
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers,threaddump
base-path: /actuator # 默认路径前缀
# 健康检查详情
endpoint:
health:
show-details: when-authorized # always / never / when-authorized
show-components: always
loggers:
enabled: true
# 健康检查组件
health:
db:
enabled: true
redis:
enabled: true
elasticsearch:
enabled: true
diskspace:
enabled: true
threshold: 10MB
# 指标配置
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active:default}
export:
prometheus:
enabled: true
# Actuator 端口(可选,与业务端口分离)
server:
port: 8081
2.5 健康检查详解
默认健康指示器
Spring Boot 自动检测依赖并注册对应的健康检查:
| 指示器 | 检测对象 | 自动注册条件 |
|---|---|---|
DataSourceHealthIndicator |
MySQL/数据库连接 | 引入了 DataSource |
RedisHealthIndicator |
Redis 连接 | 引入了 spring-data-redis |
ElasticsearchRestHealthIndicator |
ES 集群状态 | 引入了 spring-data-elasticsearch |
KafkaHealthIndicator |
Kafka Broker | 引入了 spring-kafka |
RabbitHealthIndicator |
RabbitMQ 连接 | 引入了 spring-amqp |
DiskSpaceHealthIndicator |
磁盘空间 | 默认启用 |
NacosDiscoveryHealthIndicator |
Nacos 注册中心 | 引入了 nacos-discovery |
自定义健康指示器
java
/**
* 自定义健康检查 - 检测第三方支付服务可用性.
*/
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
private final String paymentHealthUrl;
public PaymentGatewayHealthIndicator(RestTemplate restTemplate,
@Value("${payment.gateway.health-url}") String url) {
this.restTemplate = restTemplate;
this.paymentHealthUrl = url;
}
@Override
public Health health() {
try {
long start = System.currentTimeMillis();
ResponseEntity<String> response = restTemplate.getForEntity(
paymentHealthUrl, String.class);
long latency = System.currentTimeMillis() - start;
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("url", paymentHealthUrl)
.withDetail("latency_ms", latency)
.build();
} else {
return Health.down()
.withDetail("statusCode", response.getStatusCode().value())
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
健康检查响应示例
json
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "MySQL",
"validationQuery": "isValid()"
}
},
"redis": {
"status": "UP",
"details": {
"version": "7.0.12"
}
},
"elasticsearch": {
"status": "UP",
"details": {
"cluster_name": "my-cluster",
"status": "green",
"number_of_nodes": 3
}
},
"paymentGateway": {
"status": "UP",
"details": {
"url": "https://pay.example.com/health",
"latency_ms": 45
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 107374182400,
"free": 53687091200,
"threshold": 10485760
}
}
}
}
2.6 Kubernetes 就绪/存活探针集成
yaml
# application.yml
management:
endpoint:
health:
probes:
enabled: true
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
# Kubernetes Deployment
spec:
containers:
- name: order-service
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 20
periodSeconds: 5
failureThreshold: 3
| 探针 | 含义 | 失败后果 |
|---|---|---|
| Liveness | 应用是否还活着 | 失败则 K8s 重启 Pod |
| Readiness | 应用是否准备好接收流量 | 失败则从 Service 摘除 |
2.7 指标(Metrics)
内置指标
bash
# 查看所有可用指标名
GET /actuator/metrics
# 查看具体指标
GET /actuator/metrics/jvm.memory.used
GET /actuator/metrics/http.server.requests
GET /actuator/metrics/system.cpu.usage
常用内置指标:
| 指标 | 说明 |
|---|---|
jvm.memory.used |
JVM 已用内存 |
jvm.memory.max |
JVM 最大内存 |
jvm.gc.pause |
GC 暂停时间 |
jvm.threads.live |
活跃线程数 |
http.server.requests |
HTTP 请求统计(含状态码、URI、耗时) |
system.cpu.usage |
系统 CPU 使用率 |
process.cpu.usage |
进程 CPU 使用率 |
hikaricp.connections.active |
连接池活跃连接数 |
kafka.consumer.records-lag |
Kafka 消费积压 |
自定义业务指标
java
/**
* 自定义业务指标.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderMetrics {
private final MeterRegistry meterRegistry;
private Counter orderCreateCounter;
private Counter orderCreateFailCounter;
private Timer orderProcessTimer;
private AtomicInteger pendingOrderGauge;
@PostConstruct
public void init() {
// 计数器:订单创建总数
orderCreateCounter = Counter.builder("order.create.total")
.description("订单创建总数")
.tag("service", "order-service")
.register(meterRegistry);
// 计数器:订单创建失败数
orderCreateFailCounter = Counter.builder("order.create.failure")
.description("订单创建失败数")
.tag("service", "order-service")
.register(meterRegistry);
// 计时器:订单处理耗时
orderProcessTimer = Timer.builder("order.process.duration")
.description("订单处理耗时")
.publishPercentiles(0.5, 0.95, 0.99) // P50, P95, P99
.register(meterRegistry);
// 仪表盘:待处理订单数
pendingOrderGauge = new AtomicInteger(0);
Gauge.builder("order.pending.count", pendingOrderGauge, AtomicInteger::get)
.description("待处理订单数")
.register(meterRegistry);
}
/**
* 记录订单创建.
*/
public void recordOrderCreated() {
orderCreateCounter.increment();
}
/**
* 记录订单创建失败.
*/
public void recordOrderFailed() {
orderCreateFailCounter.increment();
}
/**
* 记录订单处理耗时.
*/
public Timer.Sample startTimer() {
return Timer.start(meterRegistry);
}
public void stopTimer(Timer.Sample sample) {
sample.stop(orderProcessTimer);
}
/**
* 更新待处理订单数.
*/
public void updatePendingCount(int count) {
pendingOrderGauge.set(count);
}
}
使用方式:
java
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMetrics orderMetrics;
@Override
public OrderResultDto createOrder(OrderCreateDto dto) {
Timer.Sample timer = orderMetrics.startTimer();
try {
// 业务逻辑...
OrderResultDto result = doCreateOrder(dto);
orderMetrics.recordOrderCreated();
return result;
} catch (Exception e) {
orderMetrics.recordOrderFailed();
throw e;
} finally {
orderMetrics.stopTimer(timer);
}
}
}
2.8 Prometheus + Grafana 集成
引入 Prometheus 依赖
xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Prometheus 抓取配置
yaml
# prometheus.yml
scrape_configs:
- job_name: 'spring-boot-apps'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets: ['order-service:8081', 'inventory-service:8081']
# 或使用 Nacos 服务发现
# nacos_sd_configs:
# - server: 'nacos:8848'
/actuator/prometheus 输出示例
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Eden Space"} 1.048576E7
jvm_memory_used_bytes{area="heap",id="G1 Old Gen"} 5.24288E7
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{method="GET",uri="/api/orders",status="200"} 1523
http_server_requests_seconds_sum{method="GET",uri="/api/orders",status="200"} 45.67
# HELP order_create_total 订单创建总数
# TYPE order_create_total counter
order_create_total{service="order-service"} 8923.0
三、Sleuth + Zipkin --- 分布式链路追踪
3.1 核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| Trace | 一次完整的请求链路 | 一次用户操作的全程记录 |
| Span | 链路中的一个操作单元 | 每个微服务处理的一个步骤 |
| TraceId | 全局唯一标识,贯穿整条链路 | 快递单号 |
| SpanId | 每个 Span 的唯一标识 | 每个中转站编号 |
| ParentSpanId | 父 Span 的 ID | 上一个中转站 |
| Annotation | 时间戳标记(请求到达、响应发送等) | 到站/离站时间 |
TraceId: abc123(贯穿全程)
[Gateway] SpanId: 001, ParentSpan: null
↓
[Order Service] SpanId: 002, ParentSpan: 001
↓ ↘
[Inventory] [Payment]
SpanId: 003 SpanId: 004
Parent: 002 Parent: 002
3.2 Spring Boot 2.x 方案:Sleuth + Zipkin
依赖引入
xml
<!-- 链路追踪核心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- Zipkin 报告(HTTP 方式) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
<!-- Zipkin 通过 Kafka 发送(推荐生产使用) -->
<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>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
配置
yaml
# application.yml
spring:
application:
name: order-service
sleuth:
enabled: true
sampler:
probability: 1.0 # 采样率:1.0=100%(开发环境),生产建议 0.1=10%
rate: 100 # 每秒最多采样数
propagation:
type: B3 # 传播格式:B3 / W3C
async:
enabled: true # 异步线程传播 TraceId
zipkin:
enabled: true
sender:
type: kafka # 发送方式:kafka / web / rabbit
kafka:
topic: zipkin # Kafka topic 名称
# 如果用 HTTP 方式:
# base-url: http://zipkin-server:9411
# sender:
# type: web
kafka:
bootstrap-servers: localhost:9092
3.3 Spring Boot 3.x 方案:Micrometer Tracing + Zipkin
Spring Boot 3.x 中 Sleuth 被 Micrometer Tracing 替代:
依赖引入
xml
<!-- Micrometer Tracing(替代 Sleuth) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!-- Zipkin Reporter -->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
<!-- Kafka 发送器 -->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-sender-kafka</artifactId>
</dependency>
配置
yaml
# application.yml (Spring Boot 3.x)
management:
tracing:
enabled: true
sampling:
probability: 1.0 # 采样率
propagation:
type: B3 # 传播格式
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
# 或使用 Kafka:
# transport: kafka
3.4 Sleuth 自动增强的组件
Sleuth 无需手动编码即可自动追踪以下组件:
| 组件 | 说明 |
|---|---|
| Spring MVC | 自动为每个 HTTP 请求创建 Span |
| RestTemplate | HTTP 调用自动传播 TraceId |
| WebClient | Reactive HTTP 调用自动传播 |
| OpenFeign | Feign 调用自动传播 TraceId |
| Spring Kafka | 消息发送/消费自动传播 TraceId |
| RabbitMQ | 消息发送/消费自动传播 |
| Spring Data Redis | Redis 操作自动创建 Span |
| JDBC | 数据库操作自动创建 Span |
| @Async | 异步方法自动传播 TraceId |
| @Scheduled | 定时任务自动创建新 Trace |
3.5 日志中的 TraceId
Sleuth 自动将 TraceId 和 SpanId 注入到 MDC(Mapped Diagnostic Context),日志中自动输出:
xml
<!-- logback-spring.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-},%X{spanId:-}] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
日志输出效果:
2024-01-15 10:23:45.123 [http-nio-8080-exec-1] [abc123def456,001] INFO OrderController - 收到创建订单请求, userId: U001
2024-01-15 10:23:45.234 [http-nio-8080-exec-1] [abc123def456,002] INFO InventoryService - 调用库存服务扣减库存
2024-01-15 10:23:45.345 [http-nio-8080-exec-1] [abc123def456,003] INFO PaymentService - 调用支付服务创建支付单
同一个 TraceId abc123def456 贯穿所有服务,可以在日志系统(ELK)中一次检索出完整链路。
3.6 手动创建 Span
对于 Sleuth 无法自动覆盖的场景,手动创建 Span:
java
/**
* 手动 Span 示例 - 追踪外部 HTTP 调用或复杂业务逻辑.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ExternalApiService {
private final Tracer tracer;
/**
* 调用第三方物流接口 - 手动创建 Span.
*/
public ShippingResult queryShippingStatus(String trackingNo) {
// 创建新 Span
Span span = tracer.nextSpan().name("query-shipping-status").start();
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
// 添加标签(方便后续在 Zipkin 中搜索和过滤)
span.tag("tracking.no", trackingNo);
span.tag("provider", "sf-express");
// 记录事件
span.event("开始调用物流API");
// 实际调用逻辑
ShippingResult result = doCallShippingApi(trackingNo);
span.event("物流API返回成功");
span.tag("shipping.status", result.getStatus());
return result;
} catch (Exception e) {
span.error(e); // 记录异常
throw e;
} finally {
span.end(); // 结束 Span
}
}
}
注解方式(更简洁)
java
@Service
public class InventoryService {
/**
* 使用 @NewSpan 创建新的子 Span.
*/
@NewSpan("check-inventory")
public boolean checkInventory(@SpanTag("sku") String sku,
@SpanTag("quantity") int quantity) {
// 业务逻辑
return doCheck(sku, quantity);
}
/**
* 使用 @ContinueSpan 在当前 Span 上添加标签.
*/
@ContinueSpan(log = "reduce-stock")
public void reduceStock(@SpanTag("sku") String sku,
@SpanTag("quantity") int quantity) {
// 业务逻辑
}
}
3.7 Kafka 发送器原理与配置
为什么用 Kafka 发送(而非 HTTP)
| 对比 | HTTP 发送 | Kafka 发送 |
|---|---|---|
| 耦合度 | 服务直连 Zipkin,Zipkin 宕机影响发送 | 解耦,Zipkin 宕机不影响业务 |
| 性能 | 同步 HTTP 调用有延迟 | 异步发送,几乎零延迟 |
| 可靠性 | Zipkin 不可用时数据丢失 | Kafka 持久化,不丢数据 |
| 吞吐量 | 受限于 Zipkin 处理能力 | Kafka 缓冲,削峰填谷 |
| 适用场景 | 开发/测试环境 | 生产环境推荐 |
架构
Service A ─┐
Service B ─┼── 发送 Span 到 Kafka ──→ Kafka Topic: zipkin
Service C ─┘ │
▼
Zipkin Server(消费 Kafka)
│
▼
存储(Elasticsearch / MySQL / Cassandra)
│
▼
Zipkin UI(查询和展示链路)
Zipkin Server 消费 Kafka 配置
bash
# 启动 Zipkin Server(Docker)
docker run -d --name zipkin \
-p 9411:9411 \
-e KAFKA_BOOTSTRAP_SERVERS=kafka:9092 \
-e STORAGE_TYPE=elasticsearch \
-e ES_HOSTS=http://elasticsearch:9200 \
openzipkin/zipkin
环境变量说明:
| 变量 | 说明 |
|---|---|
KAFKA_BOOTSTRAP_SERVERS |
Kafka 集群地址 |
KAFKA_TOPIC |
消费的 topic(默认 zipkin) |
STORAGE_TYPE |
存储类型:elasticsearch / mysql / cassandra / mem |
ES_HOSTS |
ES 地址(当 STORAGE_TYPE=elasticsearch) |
四、Zipkin 部署与使用
4.1 Docker Compose 完整部署
yaml
# docker-compose-tracing.yml
version: '3.8'
services:
zipkin:
image: openzipkin/zipkin:latest
ports:
- "9411:9411"
environment:
- KAFKA_BOOTSTRAP_SERVERS=kafka:9092
- STORAGE_TYPE=elasticsearch
- ES_HOSTS=http://elasticsearch:9200
- ES_INDEX=zipkin
- ES_INDEX_SHARDS=3
- ES_INDEX_REPLICAS=1
depends_on:
- kafka
- elasticsearch
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- es-data:/usr/share/elasticsearch/data
kafka:
image: confluentinc/cp-kafka:7.5.0
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
depends_on:
- zookeeper
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
volumes:
es-data:
4.2 Zipkin UI 功能
访问 http://localhost:9411:
| 功能 | 说明 |
|---|---|
| 搜索 Trace | 按服务名、时间范围、时长、标签搜索 |
| 链路拓扑 | 服务间调用关系的可视化图 |
| Span 详情 | 每个 Span 的耗时、标签、异常信息 |
| 依赖图 | 自动生成服务依赖拓扑 |
4.3 数据保留策略
bash
# Zipkin on ES - 设置索引保留天数
docker run -d --name zipkin \
-e STORAGE_TYPE=elasticsearch \
-e ES_HOSTS=http://es:9200 \
-e ES_INDEX_SHARDS=3 \
-e ES_INDEX_REPLICAS=1 \
openzipkin/zipkin
# 使用 ES ILM 策略自动清理旧数据
# 或定时删除旧索引:
# curl -X DELETE "es:9200/zipkin:span-2024-01-*"
五、采样策略
为什么需要采样
生产环境 100% 采集会带来:
- 大量存储消耗
- 网络和 Kafka 额外负载
- Zipkin 查询变慢
采样配置
yaml
spring:
sleuth:
sampler:
# 方式1:概率采样(推荐)
probability: 0.1 # 10% 的请求会被追踪
# 方式2:限速采样
rate: 100 # 每秒最多采样 100 个请求
自定义采样器
java
/**
* 自定义采样策略.
* 错误请求和慢请求100%采集,正常请求10%采集.
*/
@Configuration
public class CustomSamplerConfig {
@Bean
public Sampler customSampler() {
return new Sampler() {
// 基础采样率 10%
private final Sampler defaultSampler = Sampler.create(0.1f);
@Override
public boolean isSampled(long traceId) {
// 基础概率采样
return defaultSampler.isSampled(traceId);
}
};
}
}
基于请求路径的采样
java
/**
* 按路径差异化采样.
* 健康检查等高频端点不采样,核心业务接口高采样率.
*/
@Configuration
public class PathBasedSamplerConfig {
@Bean
public HttpTracingCustomizer httpTracingCustomizer() {
return builder -> builder.serverSampler(new HttpSampler() {
@Override
public Boolean trySample(HttpRequest request) {
String path = request.path();
// 健康检查和 actuator 不采样
if (path.startsWith("/actuator") || path.equals("/health")) {
return false;
}
// 支付相关接口 100% 采样
if (path.startsWith("/api/payment")) {
return true;
}
// 其他接口使用默认采样率
return null; // null 表示交给默认采样器决定
}
});
}
}
采样率建议
| 环境 | 采样率 | 说明 |
|---|---|---|
| 开发/测试 | 1.0(100%) | 全量采集便于调试 |
| 预发布 | 0.5(50%) | 足够排查问题 |
| 生产(低流量) | 0.1-0.3(10%-30%) | 平衡可观测性和资源 |
| 生产(高流量) | 0.01-0.05(1%-5%) | 避免存储爆炸 |
| 关键业务路径 | 强制 1.0 | 支付/退款等必须全量 |
六、TraceId 透传机制
6.1 HTTP 调用传播
Sleuth 通过 HTTP Header 传播追踪上下文:
# B3 格式 Header(默认)
X-B3-TraceId: 463ac35c9f6413ad48485a3953bb6124
X-B3-SpanId: a2fb4a1d1a96d312
X-B3-ParentSpanId: 0020000000000001
X-B3-Sampled: 1
# W3C Trace Context 格式
traceparent: 00-463ac35c9f6413ad48485a3953bb6124-a2fb4a1d1a96d312-01
6.2 Kafka 消息传播
Sleuth 自动将 TraceId 写入 Kafka 消息的 Header:
java
// 生产者端(自动注入,无需手动编码)
// Sleuth 会自动在 ProducerRecord 的 headers 中添加追踪信息
// 消费者端(自动提取)
@KafkaListener(topics = "order-topic")
public void onMessage(ConsumerRecord<String, String> record) {
// TraceId 已自动从 Header 中提取并设置到当前上下文
// 日志中会自动打印继承的 TraceId
log.info("处理订单消息, orderId: {}", record.value());
}
6.3 线程池传播
默认情况下 TraceId 不会跨线程传播。Sleuth 提供了包装类:
java
/**
* 线程池配置 - 支持 TraceId 跨线程传播.
*/
@Configuration
public class AsyncConfig {
private final BeanFactory beanFactory;
public AsyncConfig(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-");
executor.initialize();
// 使用 Sleuth 包装器,自动传播 TraceId
return new LazyTraceExecutor(beanFactory, executor);
}
}
或者使用 @Async 注解(Sleuth 自动增强):
java
@Async
public CompletableFuture<Void> asyncProcess(String data) {
// TraceId 自动传播到这里
log.info("异步处理, data: {}", data);
return CompletableFuture.completedFuture(null);
}
6.4 手动传递 TraceId(特殊场景)
java
/**
* 需要手动传递 TraceId 的场景:
* 比如使用自定义 HTTP Client 或第三方 SDK.
*/
@Service
@RequiredArgsConstructor
public class ThirdPartyCallService {
private final Tracer tracer;
public void callThirdPartyApi() {
Span currentSpan = tracer.currentSpan();
if (currentSpan != null) {
TraceContext context = currentSpan.context();
String traceId = context.traceId();
String spanId = context.spanId();
// 手动设置到第三方请求头
HttpHeaders headers = new HttpHeaders();
headers.set("X-B3-TraceId", traceId);
headers.set("X-B3-SpanId", spanId);
headers.set("X-B3-Sampled", "1");
// 发起请求...
}
}
}
七、TraceId 返回给前端
将 TraceId 返回给前端,方便用户报错时提供追踪线索:
java
/**
* 统一响应包装 - 包含 TraceId.
*/
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private String traceId;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> fail(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
/**
* 响应增强 - 自动注入 TraceId.
*/
@RestControllerAdvice
@RequiredArgsConstructor
public class TraceIdResponseAdvice implements ResponseBodyAdvice<Object> {
private final Tracer tracer;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// 在 Response Header 中添加 TraceId
Span currentSpan = tracer.currentSpan();
if (currentSpan != null) {
String traceId = currentSpan.context().traceId();
response.getHeaders().add("X-Trace-Id", traceId);
// 如果返回值是统一包装类,注入 traceId 字段
if (body instanceof Result) {
((Result<?>) body).setTraceId(traceId);
}
}
return body;
}
}
前端拿到 TraceId 后,用户反馈问题时附带此 ID,运维可直接在 Zipkin 或 ELK 中搜索定位全链路。
八、与 ELK 日志系统联动
架构
Service (日志含 TraceId)
↓ (Filebeat / Logstash)
Elasticsearch (日志存储)
↓
Kibana (日志查询)
↔ Zipkin (链路查询)
通过 TraceId 在两个系统之间跳转
Logback 配置输出 JSON 格式日志
xml
<!-- logback-spring.xml -->
<configuration>
<springProperty scope="context" name="appName" source="spring.application.name"/>
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${appName}.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${appName}.%d{yyyy-MM-dd}.json</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"service":"${appName}"}</customFields>
<!-- TraceId/SpanId 由 Sleuth 自动注入到 MDC -->
</encoder>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{HH:mm:ss.SSS} [%thread] [%X{traceId:-},%X{spanId:-}] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="JSON_FILE"/>
</root>
</configuration>
JSON 日志输出效果:
json
{
"@timestamp": "2024-01-15T10:23:45.123+08:00",
"level": "INFO",
"thread": "http-nio-8080-exec-1",
"logger": "com.example.order.service.OrderServiceImpl",
"message": "创建订单成功, orderId: ORD20240115001",
"service": "order-service",
"traceId": "abc123def456",
"spanId": "002",
"userId": "U001"
}
在 Kibana 中,搜索 traceId: abc123def456 即可获取该请求在所有服务中的日志。
九、生产环境完整配置模板
yaml
# application-prod.yml - 生产环境监控与追踪配置
spring:
application:
name: order-service
# === 链路追踪 ===
sleuth:
enabled: true
sampler:
probability: 0.1 # 生产环境 10% 采样
propagation:
type: B3
async:
enabled: true
scheduled:
enabled: true # 定时任务也追踪
log:
slf4j:
enabled: true # 自动注入 MDC
zipkin:
enabled: true
sender:
type: kafka # 通过 Kafka 发送
kafka:
topic: zipkin
service:
name: ${spring.application.name}
kafka:
bootstrap-servers: kafka-cluster:9092
# === 监控端点 ===
management:
server:
port: 8081 # 管理端口与业务端口分离
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
base-path: /actuator
endpoint:
health:
show-details: when-authorized
probes:
enabled: true # K8s 探针
shutdown:
enabled: false # 禁用远程关闭
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
db:
enabled: true
redis:
enabled: true
kafka:
enabled: true
metrics:
tags:
application: ${spring.application.name}
env: prod
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 100ms, 500ms, 1s, 5s
export:
prometheus:
enabled: true
step: 15s # 指标推送间隔
十、安全防护
10.1 Actuator 端点安全
java
/**
* Actuator 安全配置.
* 生产环境禁止公网直接访问 Actuator.
*/
@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
http
.requestMatcher(EndpointRequest.toAnyEndpoint())
.authorizeRequests()
// health 和 info 允许匿名访问(K8s 探针需要)
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
// 其他端点需要认证
.anyRequest().hasRole("ACTUATOR_ADMIN")
.and()
.httpBasic();
return http.build();
}
}
10.2 敏感信息过滤
yaml
management:
endpoint:
env:
# 隐藏配置中的敏感值
keys-to-sanitize: password,secret,key,token,credentials
十一、告警规则(Prometheus + AlertManager)
yaml
# prometheus-alerts.yml
groups:
- name: service-alerts
rules:
# 服务不健康
- alert: ServiceDown
expr: up{job="spring-boot-apps"} == 0
for: 30s
labels:
severity: critical
annotations:
summary: "服务 {{ $labels.instance }} 已宕机"
# 接口响应时间超过 2 秒
- alert: HighLatency
expr: http_server_requests_seconds_max{uri!~"/actuator.*"} > 2
for: 1m
labels:
severity: warning
annotations:
summary: "{{ $labels.application }} 接口 {{ $labels.uri }} 延迟超过2秒"
# 5xx 错误率超过 5%
- alert: HighErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (application)
/
sum(rate(http_server_requests_seconds_count[5m])) by (application)
> 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "{{ $labels.application }} 5xx错误率超过5%"
# JVM 内存使用超过 85%
- alert: HighMemoryUsage
expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.application }} 堆内存使用超过85%"
# Kafka 消费积压
- alert: KafkaConsumerLag
expr: kafka_consumer_records_lag_max > 10000
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.application }} Kafka消费积压超过10000"
十二、完整技术栈架构图
┌─────────────────────────────────────────────────────────────────┐
│ 监控与追踪架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Service A │ │ Service B │ │ Service C │ ← 微服务集群 │
│ │ Actuator │ │ Actuator │ │ Actuator │ │
│ │ Sleuth │ │ Sleuth │ │ Sleuth │ │
│ └────┬──┬──┘ └────┬──┬──┘ └────┬──┬──┘ │
│ │ │ │ │ │ │ │
│ 指标│ │Span 指标│ │Span 指标│ │Span │
│ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────────────────┐ │
│ │Prometheus│ │ Kafka │ │
│ │ (拉取) │ │ (zipkin topic) │ │
│ └────┬────┘ └──────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌──────────────────┐ │
│ │ Grafana │ │ Zipkin Server │ │
│ │ (可视化) │ │ (消费Kafka) │ │
│ └─────────┘ └────────┬─────────┘ │
│ │ │
│ ┌──────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌──────────────┐ ┌──────────┐ │
│ │AlertMgr │ │Elasticsearch │ │ Zipkin UI │ │
│ │ (告警) │ │ (链路存储) │ │ (查询) │ │
│ └────┬────┘ └──────┬───────┘ └──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌──────────────┐ │
│ │钉钉/邮件 │ │ Kibana │ │
│ │ (通知) │ │ (日志+链路) │ │
│ └─────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
十三、总结
| 组件 | 职责 | 核心能力 |
|---|---|---|
| Actuator | 健康检查 + 指标暴露 | 服务是否存活、资源使用情况 |
| Prometheus | 指标采集与存储 | 时序数据存储、告警规则 |
| Grafana | 指标可视化 | Dashboard、趋势分析 |
| Sleuth | TraceId 生成与传播 | 自动注入追踪上下文 |
| Zipkin | 链路数据收集与展示 | 全链路可视化、性能瓶颈定位 |
| Kafka(发送器) | 追踪数据传输通道 | 解耦、削峰、保证可靠性 |
| ELK | 日志收集与检索 | 结合 TraceId 搜索全链路日志 |
| AlertManager | 告警通知 | 异常实时通知到人 |
一句话总结:Actuator 让你知道"系统现在怎么样",Sleuth + Zipkin 让你知道"请求经过了哪里、在哪里出了问题",两者配合 Prometheus + Grafana 构成完整的可观测性体系。