问题引入

2024 年 3 月 15 日凌晨 2:17,某金融科技公司的 On-Call 工程师被 PagerDuty 惊醒:

告警:order-service 健康检查失败,Pod 状态 NotReady,但进程仍在运行。

工程师登录 Grafana,发现过去 30 分钟内:

  • /actuator/health 返回 {"status":"DOWN"},但 HTTP 状态码仍然是 200
  • JVM 内存使用率 87%,GC 频率从每分钟 2 次飙升到每分钟 40 次
  • HTTP 接口 P99 延迟从 120ms 暴涨到 8.3 秒
  • /actuator/metrics 端点本身响应正常,各项指标数据还在上报

团队的排查陷入了迷雾:

  • Kubernetes 的 livenessProbe 配置为 httpGet: path: /actuator/health,返回 DOWN 但进程没死,所以 K8s 没有重启 Pod
  • readinessProbe 也配置为同一个端点,返回 DOWN 后 Pod 被从 Service endpoints 中摘除,但其他健康实例已经扛不住流量洪峰
  • 最终发现,是 DataSourceHealthIndicator 在执行 SELECT 1 时超时——数据库连接池已满,新的健康检查请求拿不到连接,导致健康检查本身失败
  • 更讽刺的是,健康检查的超时时间默认是 1 秒,而 SELECT 1 在数据库高负载下执行了 3 秒,超时抛异常后 health 端点返回 DOWN,但异常信息被吞掉了

SpringBoot 应用上线后,怎么知道它健不健康?JVM 内存用了多少?接口响应时间 P99 是多少?异常率有没有飙升?这些问题的答案都在监控与可观测性体系中。面试追问下去:

  • Actuator 的 /health 是怎么判断服务健康的?自定义的健康检查怎么写?
  • Micrometer 的 Counter、Timer、Gauge 有什么区别?Prometheus 的 histogram 和 summary 怎么选?
  • SpringBoot 3.x 的 Observability 有什么变化?Sleuth 为什么被废弃了?
  • 健康检查返回 DOWN 但进程还在,K8s 为什么不重启 Pod?
  • 监控指标采集多了有什么副作用?

本文从生产运维的视角出发,构建完整的可观测性认知。

核心概念

1. 可观测性三支柱

可观测性(Observability)不是监控(Monitoring)的同义词。监控是"我知道要看什么,所以我设了告警";可观测性是"我不知道会出什么问题,但系统内部状态可以通过外部输出推断出来"。

Google SRE 将可观测性归纳为三大支柱:

复制代码
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│     Metrics     │    │      Logs       │    │     Traces      │
│    (指标)      │    │    (日志)      │    │    (追踪)      │
├─────────────────┤    ├─────────────────┤    ├─────────────────┤
│ • 可聚合        │    │ • 离散事件      │    │ • 请求链路      │
│ • 计数/耗时/状态│    │ • 详细上下文    │    │ • 跨服务传播    │
│ • 适合告警      │    │ • 适合排查      │    │ • 适合定位      │
│ • 低开销        │    │ • 高存储成本    │    │ • 中等开销      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                      │                      │
         └──────────────────────┼──────────────────────┘
                                ▼
                    ┌─────────────────────┐
                    │   SpringBoot 3.x    │
                    │  Observability API  │
                    │  (Micrometer        │
                    │   Observation)      │
                    └─────────────────────┘

读图导引:三大支柱各有分工——Metrics 回答"有多少、有多快、是否健康",Logs 回答"发生了什么",Traces 回答"请求经过了哪些地方、每一步花了多久"。SpringBoot 3.x 的 Observation API 试图将三者统一到一个 API 中。

2. Actuator 端点体系

Actuator 是 SpringBoot 的内置监控模块,将应用内部状态暴露为 HTTP/JMX 端点。

端点分类

端点 功能 默认暴露 敏感性
/actuator/health 健康状态聚合 Web/JMX
/actuator/info 应用信息(版本、构建信息) Web/JMX
/actuator/metrics 指标列表 JMX
/actuator/metrics/{name} 单个指标详情 JMX
/actuator/loggers 日志级别查看和修改 JMX
/actuator/env 环境属性 JMX
/actuator/beans 所有 Bean JMX
/actuator/threaddump 线程 dump JMX
/actuator/heapdump 堆 dump JMX
/actuator/conditions 自动配置报告 JMX
/actuator/mappings HTTP 端点映射 JMX
/actuator/prometheus Prometheus 格式指标 JMX

端点暴露配置

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus  # 白名单
        # exclude: env,beans  # 黑名单
      base-path: /actuator  # 基础路径
  endpoint:
    health:
      show-details: when_authorized  # never / when_authorized / always
      show-components: always        # 显示各组件健康状态
      probes:
        enabled: true  # 启用 K8s 探针端点(/actuator/health/liveness, /actuator/health/readiness)

3. Micrometer 指标模型

Micrometer 是监控系统的抽象层,不是监控系统的实现。它定义了指标 API,通过 MeterRegistry 适配到不同的后端(Prometheus、InfluxDB、CloudWatch、Datadog 等)。

四种核心 Meter 类型

java 复制代码
// Counter:单调递增的计数器
Counter requests = Counter.builder("http.requests")
    .tag("method", "GET")
    .tag("status", "200")
    .register(registry);
requests.increment();  // 只能增加,不能减少

// Timer:记录耗时和分位数
Timer timer = Timer.builder("http.request.duration")
    .tag("api", "order.create")
    .publishPercentiles(0.5, 0.95, 0.99)
    .register(registry);
timer.record(() -> { /* 业务逻辑 */ });

// Gauge:瞬时值,可增可减
Gauge.builder("jvm.memory.used", 
        () -> ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed())
    .tag("area", "heap")
    .register(registry);

// DistributionSummary:记录数值分布(不限于时间)
DistributionSummary summary = DistributionSummary.builder("order.amount")
    .publishPercentiles(0.5, 0.99)
    .register(registry);
summary.record(199.99);

Meter 类型选择决策树

复制代码
你要记录什么?
├── 累计次数(请求数、错误数)
│   └── Counter(只增不减)
├── 耗时(接口响应时间)
│   └── Timer(自动记录 count、sum、max、分位数)
├── 瞬时值(内存、连接数、队列长度)
│   └── Gauge(回调式读取当前值)
└── 数值分布(订单金额、文件大小)
    └── DistributionSummary(记录数值的分位分布)

读图导引:选择 Meter 类型的关键是"数据是否可逆"——Counter 是不可逆的(计数器不能倒转),Gauge 是可逆的(内存可以释放),Timer 和 DistributionSummary 关注的是分布而非瞬时值。

4. Prometheus 指标格式

Prometheus 是云原生监控的事实标准,Micrometer 通过 PrometheusMeterRegistry 将指标转换为 Prometheus 格式:

复制代码
# HELP http_requests_total HTTP 请求总数
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200",application="order-service"} 1024

# HELP http_request_duration_seconds HTTP 请求耗时
# TYPE http_request_duration_seconds summary
http_request_duration_seconds_count{api="order.create"} 500
http_request_duration_seconds_sum{api="order.create"} 45.2
http_request_duration_seconds_max{api="order.create"} 0.8
http_request_duration_seconds{api="order.create",quantile="0.5"} 0.05
http_request_duration_seconds{api="order.create",quantile="0.95"} 0.15
http_request_duration_seconds{api="order.create",quantile="0.99"} 0.35

histogram vs summary 的选择

维度 Client-side Summary(Micrometer summary) Client-side Histogram(Micrometer histogram)
分位计算 客户端计算(T-Digest 算法) 服务端计算(Prometheus histogram_quantile)
可聚合性 不可跨实例聚合 可跨实例聚合
精度 高(T-Digest 误差小) 低(受桶边界限制)
开销 内存中维护 T-Digest 结构 维护 bucket 计数器

生产环境推荐:

  • 如果需要在 Grafana 做跨实例聚合(如"所有订单服务的 P99"),用 histogram
  • 如果需要高精度分位且只关注单实例,用 summary
  • SpringBoot 3.x 默认对 Timer 使用 histogram(因为 management.metrics.distribution.percentiles-histogram.enabled 默认开启)

5. SpringBoot 3.x Observability

SpringBoot 2.x 使用 Spring Cloud Sleuth 做分布式追踪,3.x 全面迁移到 Micrometer Observation API:

java 复制代码
// 统一的可观测性 API
Observation observation = Observation.start("order.create", registry);
try (Observation.Scope scope = observation.openScope()) {
    // 业务逻辑
    // 这里自动产生:
    // 1. Metric(Counter + Timer)
    // 2. Trace Span
    // 3. Log MDC(traceId, spanId)
    processOrder(order);
    observation.event(Observation.Event.of("order.processed"));
} catch (Exception e) {
    observation.error(e);
    throw e;
} finally {
    observation.stop();
}

读图导引:Observation 是 SpringBoot 3.x 可观测性的"统一入口"。一次 Observation.start() 同时产生三类数据:指标(Metric)、追踪(Trace)、日志上下文(Log MDC)。开发者不再需要手动分别调用 Counter.increment()Tracer.nextSpan()MDC.put()

原理分析

1. Actuator 端点的注册与暴露机制

Actuator 端点不是普通的 Spring MVC Controller,而是通过 Endpoint 注解 + EndpointWebExtension 机制注册的:

java 复制代码
// 端点定义(与技术无关,可被 JMX、Web、SSH 等多种方式暴露)
@Endpoint(id = "health")
public class HealthEndpoint {

    @ReadOperation
    public Health health() {
        // 聚合所有 HealthIndicator 的结果
    }
}

// Web 扩展(将端点适配为 HTTP 接口)
@EndpointWebExtension(endpoint = HealthEndpoint.class)
public class HealthEndpointWebExtension {

    @ReadOperation
    public WebEndpointResponse<Health> health(ApiVersion apiVersion) {
        Health health = delegate.health();
        // 根据健康状态返回对应的 HTTP 状态码
        int status = (health.getStatus() == Status.UP) ? 200 : 503;
        return new WebEndpointResponse<>(health, status);
    }
}

端点暴露的安全模型

复制代码
Endpoint(端点定义)
    │
    ├── JMX Extension → MBean 注册 → JMX 访问
    │
    └── Web Extension
            │
            ├── Servlet 环境 → 注册到 DispatcherServlet
            │
            └── Reactive 环境 → 注册到 RouterFunction

读图导引:端点暴露有两层"网关"——第一层是 management.endpoints.web.exposure.include 决定是否暴露,第二层是端点自身的 EndpointDiscoverer 决定是否可用。即使端点被暴露,如果安全拦截器配置了访问控制,未经授权的请求也会返回 401。

2. HealthIndicator 的聚合机制

/actuator/health 的核心是聚合所有 HealthIndicator 的结果:

java 复制代码
// HealthEndpoint 的聚合逻辑
public class HealthEndpoint {

    private final HealthContributorRegistry registry;

    public Health health() {
        // 遍历所有 HealthContributor,逐个检查
        Map<String, Health> components = new LinkedHashMap<>();
        for (Map.Entry<String, HealthContributor> entry : registry) {
            Health health = ((HealthIndicator) entry.getValue()).health();
            components.put(entry.getKey(), health);
        }
        // 整体状态 = 所有组件中最严重的状态
        Status status = StatusAggregator.getDefault()
            .getAggregateStatus(components.values());
        return Health.status(status)
            .withDetails(components)
            .build();
    }
}

状态优先级(从严重到轻微):DOWN > OUT_OF_SERVICE > UP > UNKNOWN

这意味着:只要有一个依赖(如数据库、Redis、MQ)不健康,整体状态就是 DOWN。

内置 HealthIndicator

Indicator 检查内容 默认超时
DataSourceHealthIndicator SELECT 1 1s
RedisHealthIndicator PING 1s
RabbitHealthIndicator 获取 Channel 1s
DiskSpaceHealthIndicator 磁盘剩余空间
PingHealthIndicator 空检查(永远 UP)

暗面DataSourceHealthIndicatorSELECT 1 在数据库高负载时本身就是压力源。生产环境中,如果数据库连接池已满,健康检查请求拿不到连接,就会超时返回 DOWN——此时健康检查本身成了压垮系统的最后一根稻草。

解决方案:

yaml 复制代码
management:
  endpoint:
    health:
      show-details: when_authorized
  health:
    db:
      enabled: true
      # SpringBoot 3.x 支持自定义超时
    defaults:
      enabled: true
java 复制代码
@Component
public class SafeDataSourceHealthIndicator implements HealthIndicator {

    @Autowired
    private DataSource dataSource;

    @Override
    public Health health() {
        // 使用独立的轻量级连接,不占用连接池
        try (Connection conn = dataSource.getConnection()) {
            // 设置查询超时,避免阻塞
            Statement stmt = conn.createStatement();
            stmt.setQueryTimeout(1); // 1 秒超时
            stmt.execute("SELECT 1");
            return Health.up().build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}

3. Micrometer 的 MeterRegistry 架构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                     MeterRegistry                            │
│                    (指标注册中心)                            │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │   Counter   │  │    Timer    │  │       Gauge         │  │
│  │             │  │             │  │                     │  │
│  │ _count      │  │ _count      │  │ (回调读取当前值)    │  │
│  │             │  │ _sum        │  │                     │  │
│  │             │  │ _max        │  │                     │  │
│  │             │  │ histogram   │  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │              MeterFilter(标签过滤/转换)               │   │
│  │  • commonTags():统一添加标签                          │   │
│  │  • ignoreTags():忽略指定标签                          │   │
│  │  • maximumAllowableTags():限制标签组合数               │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
              ┌───────────────────────────────┐
              │     PrometheusMeterRegistry   │
              │    (定时 scrape 时拉取数据)   │
              └───────────────────────────────┘

读图导引:MeterRegistry 是 Micrometer 的核心。所有 Meter 注册到 Registry 中,MeterFilter 在注册时做标签转换和过滤,最终由具体的 Registry 实现(如 PrometheusMeterRegistry)在采集时输出数据。

Cardinality 爆炸问题

这是使用 Micrometer 时最常见的生产事故:

java 复制代码
// 错误示例:将用户 ID 作为 tag
Counter.builder("user.requests")
    .tag("userId", userId)  // 每个用户一个时间序列!
    .register(registry);

如果系统有 10 万用户,这个 Counter 就会产生 10 万个时间序列。Prometheus 的内存占用随时间序列线性增长,最终导致 OOM。

安全实践:

java 复制代码
// 正确:将高基数维度聚合为低基数分组
Counter.builder("user.requests")
    .tag("userType", getUserType(userId))  // 分组:VIP / NORMAL
    .tag("region", getRegion(userId))      // 分组:CN / US / EU
    .register(registry);

或通过 MeterFilter 限制:

java 复制代码
@Bean
public MeterRegistryCustomizer<MeterRegistry> limitTags() {
    return registry -> registry.config()
        .maximumAllowableTags("http.requests", "uri", 100, (k, v) -> "TOO_MANY");
}

4. SpringBoot 3.x Observation 内幕

SpringBoot 3.x 使用 ObservationHandler 机制将 Metrics、Tracing、Logging 统一:

java 复制代码
// ObservationRegistry 注册多个 Handler
ObservationRegistry registry = ObservationRegistry.create();

// 1. Metrics Handler:产生 Counter 和 Timer
registry.observationConfig()
    .observationHandler(new DefaultMeterObservationHandler(meterRegistry));

// 2. Tracing Handler:产生 Trace Span
registry.observationConfig()
    .observationHandler(new DefaultTracingObservationHandler(tracer));

// 3. Logging Handler:向 MDC 注入 traceId
registry.observationConfig()
    .observationHandler(new LoggingObservationHandler());

当一个 Observation 被创建时,所有注册的 Handler 都会收到通知:

复制代码
Observation.start("order.create")
    │
    ├── DefaultMeterObservationHandler
    │       └── Timer.start("order.create")
    │
    ├── DefaultTracingObservationHandler
    │       └── Tracer.nextSpan().name("order.create").start()
    │
    └── LoggingObservationHandler
            └── MDC.put("traceId", span.context().traceId())
                MDC.put("spanId", span.context().spanId())

读图导引:Observation 是"一分发三"的广播模型。开发者只需调用一次 Observation.start(),背后的 Handler 链会自动完成指标、追踪和日志的关联。这种设计消除了"指标和追踪对不上"的问题——它们来自同一个 Observation 上下文。

Propagation 与 Baggage

分布式追踪需要在服务间传递上下文(traceId、spanId)。SpringBoot 3.x 使用 Micrometer Tracing + Brave/OpenTelemetry 实现:

java 复制代码
// 自动传播的字段(W3C TraceContext 标准)
// traceparent: 00-traceId-parentId-flags

// Baggage:业务自定义的传递字段
Baggage.create("userId").set(userId);
// 自动注入到下游请求的 HTTP Header 中

Baggage 会随请求传播到所有下游服务,但每个 Baggage 字段都会增加请求头大小,滥用会导致 HTTP 头过大(超过 Nginx/Envoy 默认限制 8KB)。

5. 日志可观测性

动态调整日志级别

bash 复制代码
# 查看某个 logger 的当前级别
GET /actuator/loggers/com.example.order

# 动态调整为 DEBUG(无需重启)
POST /actuator/loggers/com.example.order
Content-Type: application/json

{"configuredLevel": "DEBUG"}

这在排查线上问题时非常有用——可以临时开启 DEBUG 日志,排查完成后立即恢复,避免长期 DEBUG 导致磁盘 I/O 飙升。

结构化日志(JSON 格式)

xml 复制代码
<!-- logback-spring.xml -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <includeContext>true</includeContext>
    <includeMdcKeyName>traceId</includeMdcKeyName>
    <includeMdcKeyName>spanId</includeMdcKeyName>
</encoder>

输出:

json 复制代码
{
  "@timestamp": "2024-06-01T10:00:00.000Z",
  "level": "INFO",
  "logger_name": "com.example.order.OrderService",
  "message": "订单创建成功",
  "traceId": "abc123",
  "spanId": "def456",
  "thread_name": "http-nio-8080-exec-1"
}

结构化日志的优势:可直接被 Elasticsearch/Logstash 解析,无需 grok 正则匹配;traceId 和 spanId 的注入让日志和追踪自动关联。

实战/源码

1. 自定义 HealthIndicator

场景:微服务依赖一个第三方风控 API,需要将其健康状态纳入整体健康检查。

java 复制代码
@Component
public class RiskApiHealthIndicator implements HealthIndicator {

    @Autowired
    private RiskApiClient riskApiClient;

    @Override
    public Health health() {
        // 使用独立超时,避免阻塞整体 health 端点
        try {
            boolean healthy = riskApiClient.ping()
                .timeout(Duration.ofMillis(500))
                .block();
            if (healthy) {
                return Health.up()
                    .withDetail("api", "risk-service")
                    .withDetail("latency", "50ms")
                    .build();
            } else {
                return Health.down()
                    .withDetail("api", "risk-service")
                    .withDetail("reason", "ping returned false")
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withDetail("api", "risk-service")
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}

关键设计:健康检查必须设置超时。如果依赖服务无响应,没有超时的健康检查会让 /actuator/health 端点挂死,进而导致 K8s 的探针超时,触发连锁故障。

2. 用 Timer 记录 HTTP 接口耗时

java 复制代码
@Component
public class OrderControllerMetrics {

    private final Timer orderCreateTimer;
    private final Counter orderCreateCounter;

    public OrderControllerMetrics(MeterRegistry registry) {
        this.orderCreateTimer = Timer.builder("order.create.duration")
            .description("订单创建接口耗时")
            .tag("api", "order.create")
            .publishPercentileHistogram()  // 生成 histogram,支持 Prometheus histogram_quantile
            .sla(Duration.ofMillis(100), Duration.ofMillis(500))
            .register(registry);

        this.orderCreateCounter = Counter.builder("order.create.count")
            .tag("api", "order.create")
            .register(registry);
    }

    public void record(Runnable action) {
        orderCreateCounter.increment();
        orderCreateTimer.record(action);
    }
}

@RestController
public class OrderController {

    @Autowired
    private OrderControllerMetrics metrics;

    @PostMapping("/orders")
    public Order createOrder(@RequestBody OrderRequest request) {
        return metrics.record(() -> {
            // 业务逻辑
            return orderService.create(request);
        });
    }
}

SpringBoot 3.x 的自动观测:如果使用 WebMVC/WebFlux,SpringBoot 3.x 会自动为所有 HTTP 端点创建 http.server.requests 指标,无需手动打点。但如果需要业务语义标签(如订单类型、用户等级),仍然需要自定义 Timer。

3. Prometheus 对接与告警规则

Prometheus 抓取配置

yaml 复制代码
# prometheus.yml
scrape_configs:
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['order-service:8080', 'user-service:8080']
    # 抓取间隔
    scrape_interval: 15s

Grafana 查询示例

promql 复制代码
# HTTP QPS
rate(http_server_requests_seconds_count[1m])

# HTTP P99 延迟
histogram_quantile(0.99,
  rate(http_server_requests_seconds_bucket[5m])
)

# JVM 堆内存使用率
jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}

# 错误率
rate(http_server_requests_seconds_count{status=~"5.."}[5m])
  /
rate(http_server_requests_seconds_count[5m])

Alertmanager 告警规则

yaml 复制代码
# alerts.yml
groups:
  - name: spring-boot-alerts
    rules:
      # 高错误率告警
      - alert: HighErrorRate
        expr: |
          rate(http_server_requests_seconds_count{status=~"5.."}[5m])
            / rate(http_server_requests_seconds_count[5m]) > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "服务 {{ $labels.application }} 错误率超过 5%"

      # 高延迟告警
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            rate(http_server_requests_seconds_bucket[5m])
          ) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "服务 {{ $labels.application }} P99 延迟超过 1s"

      # JVM 内存告警
      - alert: JvmMemoryHigh
        expr: |
          jvm_memory_used_bytes{area="heap"}
            / jvm_memory_max_bytes{area="heap"} > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "服务 {{ $labels.application }} JVM 堆内存使用率超过 85%"

4. SpringBoot 3.x 分布式追踪实战

java 复制代码
@Service
public class OrderService {

    @Autowired
    private ObservationRegistry observationRegistry;

    @Autowired
    private PaymentClient paymentClient;

    public Order createOrder(OrderRequest request) {
        return Observation.createNotStarted("order.create", observationRegistry)
            .contextualName("创建订单")
            .lowCardinalityKeyValue("order.type", request.getType())
            .highCardinalityKeyValue("order.userId", request.getUserId())
            .observe(() -> {
                // 1. 创建订单
                Order order = saveOrder(request);

                // 2. 调用支付服务(trace 自动传播)
                PaymentResult result = paymentClient.charge(order);

                // 3. 更新订单状态
                order.setStatus(result.isSuccess() ? "PAID" : "FAILED");
                return updateOrder(order);
            });
    }
}

关键区分

  • lowCardinalityKeyValue:低基数标签(如订单类型、地区),会作为 Metric tag 和 Trace tag
  • highCardinalityKeyValue:高基数标签(如用户 ID、订单 ID),只作为 Trace tag,不会进入 Metric(避免 cardinality 爆炸)

日志关联

xml 复制代码
<!-- logback-spring.xml -->
<pattern>
    %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}
    [traceId=%X{traceId},spanId=%X{spanId}]
    - %msg%n
</pattern>

输出:

复制代码
2024-06-01 10:00:00.123 [http-nio-8080-exec-1] INFO c.e.o.OrderService
[traceId=abc123,spanId=def456]
- 订单创建成功,orderId=ORDER_20240601_001

5. K8s 探针配置最佳实践

yaml 复制代码
# deployment.yml
spec:
  template:
    spec:
      containers:
        - name: app
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3  # 3 次失败才重启
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 30  # 给慢启动应用最多 150 秒

探针分离的意义

  • livenessProbe/actuator/health/liveness:只检查应用是否"活着"(进程没死、没死锁),不应检查外部依赖
  • readinessProbe/actuator/health/readiness:检查应用是否"准备好接收流量",包括外部依赖

如果 livenessProbe 配置了 /actuator/health(包含数据库检查),数据库故障时 K8s 会认为 Pod 已死并重启它——但重启并不能解决数据库问题,只会增加数据库连接压力(连接风暴)。

SpringBoot 2.3+ 支持探针分组:

yaml 复制代码
management:
  endpoint:
    health:
      probes:
        enabled: true
      group:
        liveness:
          include: ping  # 只包含 ping,不检查外部依赖
        readiness:
          include: db, redis, diskSpace  # 检查外部依赖

常见问题

Q1:Actuator 的 /health 返回 DOWN,但进程还在,K8s 为什么不重启 Pod?

K8s 的 livenessProbe 决定是否重启 Pod,readinessProbe 决定是否将 Pod 加入 Service 负载均衡。如果 livenessProbe 配置的是 /actuator/health(包含外部依赖检查),外部依赖故障会导致 Pod 被反复重启。正确做法是:

  • livenessProbe 使用 /actuator/health/liveness,只检查进程是否存活
  • readinessProbe 使用 /actuator/health/readiness,检查应用是否能处理请求
  • 两者分离,避免"外部依赖故障 → Pod 重启 → 连接风暴 → 数据库更不可用"的级联故障

Q2:Micrometer 的 Counter、Timer、Gauge 有什么区别?使用场景分别是什么?

  • Counter:单调递增的计数器,适合记录"发生了多少次"(请求数、错误数)。不能减少,但可以重置(如应用重启后从 0 开始)
  • Timer:专门记录耗时的工具,自动产生 count、sum、max 和分位数。适合记录"接口响应时间"。底层通常基于 Counter + DistributionSummary 实现
  • Gauge:瞬时值,可增可减。适合记录"当前有多少"(内存使用量、连接数、队列长度)。Gauge 的值通过回调函数在采集时读取,不是由应用主动推送
  • DistributionSummary:记录任意数值的分布,不限于时间。适合记录"订单金额分布"、"文件大小分布"

Q3:Prometheus 的 histogram 和 summary 怎么选?

  • Client-side Summary(Micrometer 的 publishPercentiles):客户端计算分位数,精度高(T-Digest 算法),但不可跨实例聚合。适合关注单实例延迟的场景
  • Client-side Histogram(Micrometer 的 publishPercentileHistogram + SLO):客户端维护 bucket 计数器,服务端用 histogram_quantile() 计算分位数。精度受 bucket 边界限制,但可跨实例聚合。适合"所有实例的 P99 是多少"这类问题

生产建议:对 HTTP 接口延迟使用 histogram(因为需要看整体 P99),对特定批处理任务使用 summary(因为只看单实例精度)。

Q4:SpringBoot 3.x 的 Observation API 和 2.x 的 Sleuth 有什么区别?

维度 SpringBoot 2.x + Sleuth SpringBoot 3.x + Micrometer Tracing
API Sleuth 独立的 Tracer API Micrometer Observation(统一 API)
指标 Sleuth 不管指标 Observation 同时产生 Metric + Trace
日志 Sleuth 向 MDC 注入 traceId Observation Handler 统一注入
后端 Zipkin 为主 Zipkin / Jaeger / OTLP 都支持
baggage Sleuth Baggage Micrometer Baggage

核心变化:3.x 将"指标 + 追踪 + 日志"统一到一个 Observation API 中,消除了"指标和追踪对不上"的问题。但迁移成本是:所有 Sleuth 相关的代码需要重写为 Observation API。

Q5:监控指标采集多了有什么副作用?

最大的风险是 Cardinality 爆炸(时间序列数指数增长):

  • Prometheus 的内存占用 ∝ 时间序列数
  • 每个标签组合都是一个独立的时间序列
  • 高基数标签(用户 ID、IP 地址、订单 ID)会导致时间序列数暴增

示例:

  • http_requests_total{uri="/orders", method="GET"} → 1 个时间序列
  • http_requests_total{uri="/orders/{id}", method="GET", userId="xxx"} → N 个时间序列(每个用户一个)

如果 10 万用户每人产生 100 个时间序列,就是 1000 万时间序列。Prometheus 单实例通常只能稳定处理 100-200 万时间序列。

防护措施:

  1. 用 MeterFilter 限制每个指标的最大标签组合数
  2. 高基数维度用 highCardinalityKeyValue(只进 Trace,不进 Metric)
  3. 定期审查 Prometheus 的 prometheus_tsdb_head_series 指标

Q6:为什么生产环境不建议暴露 /actuator/beans 和 /actuator/env?

安全原因:

  • /actuator/beans 暴露了应用中所有 Bean 的完整信息,包括可能包含敏感信息的 Bean 名称和类型
  • /actuator/env 暴露了所有环境属性,可能包含数据库密码、API Key(如果配置不当)
  • /actuator/heapdump 可以直接下载 JVM 堆内存 dump,包含内存中的所有对象数据(用户会话、缓存数据等)

最佳实践:

  • 生产环境只暴露 healthinfometricsprometheus
  • 敏感端点通过防火墙或 VPN 访问,不暴露到公网
  • 使用 Spring Security 对 Actuator 端点做认证授权

总结

SpringBoot 的可观测性体系是一个"分层暴露"的结构:

  1. Actuator 是内窥窗口——它将应用的内部状态(健康、指标、环境、线程、堆内存)暴露为标准化端点。但窗口开得太大(暴露敏感端点到公网)就是安全漏洞。

  2. Micrometer 是抽象层——它让开发者用同一套 API(Counter、Timer、Gauge)记录指标,而不关心后端是 Prometheus、InfluxDB 还是 CloudWatch。但抽象层不能防止错误使用——高基数标签导致的 cardinality 爆炸是生产环境最常见的监控事故。

  3. SpringBoot 3.x Observation 是统一框架——它将 Metrics、Traces、Logs 统一到 Observation API 中,一次方法调用自动产生三类数据。这种设计消除了"指标和追踪对不上"的历史顽疾,但代价是 Sleuth 用户需要全面迁移。

  4. 健康检查的设计哲学是"分层判断"——liveness 回答"我是否还活着",readiness 回答"我是否能处理流量"。将两者混为一谈(都用 /actuator/health)是 K8s 环境下最常见的配置错误,后果是外部依赖故障导致 Pod 反复重启,引发连接风暴。

  5. 监控的终极边界是"采集不等于可观测"。Prometheus 采集了 10 万个指标,但告警规则只有 3 条,Grafana dashboard 没人看——这不是可观测性,这是数据囤积。好的可观测性遵循"关键指标少数派"原则:只监控能指导行动的指标(错误率、P99 延迟、饱和度),而不是所有能采集的指标。