问题引入
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) | 无 |
暗面:DataSourceHealthIndicator 的 SELECT 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 taghighCardinalityKeyValue:高基数标签(如用户 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 万时间序列。
防护措施:
- 用 MeterFilter 限制每个指标的最大标签组合数
- 高基数维度用
highCardinalityKeyValue(只进 Trace,不进 Metric) - 定期审查 Prometheus 的
prometheus_tsdb_head_series指标
Q6:为什么生产环境不建议暴露 /actuator/beans 和 /actuator/env?
安全原因:
/actuator/beans暴露了应用中所有 Bean 的完整信息,包括可能包含敏感信息的 Bean 名称和类型/actuator/env暴露了所有环境属性,可能包含数据库密码、API Key(如果配置不当)/actuator/heapdump可以直接下载 JVM 堆内存 dump,包含内存中的所有对象数据(用户会话、缓存数据等)
最佳实践:
- 生产环境只暴露
health、info、metrics、prometheus - 敏感端点通过防火墙或 VPN 访问,不暴露到公网
- 使用 Spring Security 对 Actuator 端点做认证授权
总结
SpringBoot 的可观测性体系是一个"分层暴露"的结构:
-
Actuator 是内窥窗口——它将应用的内部状态(健康、指标、环境、线程、堆内存)暴露为标准化端点。但窗口开得太大(暴露敏感端点到公网)就是安全漏洞。
-
Micrometer 是抽象层——它让开发者用同一套 API(Counter、Timer、Gauge)记录指标,而不关心后端是 Prometheus、InfluxDB 还是 CloudWatch。但抽象层不能防止错误使用——高基数标签导致的 cardinality 爆炸是生产环境最常见的监控事故。
-
SpringBoot 3.x Observation 是统一框架——它将 Metrics、Traces、Logs 统一到
ObservationAPI 中,一次方法调用自动产生三类数据。这种设计消除了"指标和追踪对不上"的历史顽疾,但代价是 Sleuth 用户需要全面迁移。 -
健康检查的设计哲学是"分层判断"——
liveness回答"我是否还活着",readiness回答"我是否能处理流量"。将两者混为一谈(都用/actuator/health)是 K8s 环境下最常见的配置错误,后果是外部依赖故障导致 Pod 反复重启,引发连接风暴。 -
监控的终极边界是"采集不等于可观测"。Prometheus 采集了 10 万个指标,但告警规则只有 3 条,Grafana dashboard 没人看——这不是可观测性,这是数据囤积。好的可观测性遵循"关键指标少数派"原则:只监控能指导行动的指标(错误率、P99 延迟、饱和度),而不是所有能采集的指标。