Spring Boot Actuator:健康检查与指标
1. 概述
生产环境中运行的服务需要回答两个问题:“它还活着吗?“和”它运行得怎么样?“。Spring Boot Actuator 通过内置的端点回答这两个问题,主要是 /actuator/health 和 /actuator/metrics。
在本文中,你将启用 Actuator、编写自定义的 HealthIndicator 并为 Kubernetes 配置 liveness/readiness probe。接下来,你将使用 Micrometer 记录业务指标,导出到 Prometheus,并为这两部分编写测试。
2. 安装
你需要添加 Actuator 的 starter。本文的示例还使用了 spring-boot-starter-web 以便通过 HTTP 提供这些端点:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启动应用程序并尝试调用:
curl http://localhost:8080/actuator
{"_links":{"self":{...},"health":{...},"health-path":{...}}}
默认情况下,Spring Boot 3.x 仅通过 HTTP 公开 health 端点。其他端点仍然存在,但无法从外部访问。这是一个有意的安全选择。
2.1. 公开更多端点
在 application.properties 中明确列出想要打开的端点:
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
第一行打开三个端点:health、info 和 metrics。第二行允许 /actuator/health 返回每个组件的详细信息,而不是仅返回一个总体状态。
注意: 避免在生产环境使用
management.endpoints.web.exposure.include=*。像env、heapdump或configprops这样的端点可能会泄露密码、内部配置和内存中的数据。
3. 健康检查
3.1. 默认端点
使用上述配置,调用 /actuator/health 将返回如下结果:
{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": { "total": 499963174912, "free": 213456789504, "threshold": 10485760 }
},
"ping": { "status": "UP" }
}
}
Spring Boot 根据 classpath 自动注册 HealthIndicator。如果有 DataSource,你会看到额外的 db。如果有 Redis,则会添加 redis,其他组件也类似。总体状态是合并结果:只要有一个组件是 DOWN,整体就是 DOWN,HTTP 状态代码返回 503。
3.2. 编写自定义 HealthIndicator
假设你的订单服务依赖于一个外部支付网关。首先,我们有一个简单的客户端:
public interface PaymentGatewayClient {
boolean ping();
}
然后编写 indicator 来检查该客户端:
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentGatewayClient client;
public PaymentGatewayHealthIndicator(PaymentGatewayClient client) {
this.client = client;
}
@Override
public Health health() {
try {
if (client.ping()) {
return Health.up().withDetail("provider", "VNPay").build();
}
return Health.down().withDetail("reason", "ping returned false").build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
需要注意几点:
- bean 名称是
paymentGatewayHealthIndicator。Spring Boot 会去掉HealthIndicator后缀,因此 JSON 中的 key 是paymentGateway。 withDetail(...)添加附加信息,仅在show-details允许时显示。Health.down(e)自动将错误消息放入details.error部分。
当支付网关出错时的结果:
{
"status": "DOWN",
"components": {
"diskSpace": { "status": "UP", ... },
"paymentGateway": {
"status": "DOWN",
"details": { "error": "java.net.ConnectException: Connection refused" }
},
"ping": { "status": "UP" }
}
}
注意:
health()方法每次调用端点时都会运行。如果它进行网络调用而没有超时,一个缓慢的依赖会导致/actuator/health挂起。你应该始终为 health check 中使用的客户端设置短超时(例如 1-2 秒)。
3.3. Liveness 和 readiness probe
Kubernetes 区分两种类型的 probe:
- Liveness:应用程序是否被冻结?如果失败,pod 将被重启。
- Readiness:应用程序是否已准备好接收请求?如果失败,pod 将从负载均衡器中移除,但不会被重启。
当 Spring Boot 检测到运行在 Kubernetes 上时,会自动启用这两个组。在本地运行时,你需要手动启用:
management.endpoint.health.probes.enabled=true
management.endpoint.health.group.readiness.include=readinessState,paymentGateway
第一行创建两个端点:/actuator/health/liveness 和 /actuator/health/readiness。第二行将 paymentGateway 添加到 readiness 组。当支付网关宕机时,pod 只会停止接收流量,而不会被不必要地重启。
curl http://localhost:8080/actuator/health/liveness
{"status":"UP"}
Kubernetes manifest 中的相应配置:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
注意: 不要将外部依赖(数据库、支付 API)放在 liveness 组中。当数据库宕机时,所有 pod 都会失败 liveness 检查并被同时重启。重启无法修复数据库,反而会在启动时增加额外负载。
4. 使用 Micrometer 的指标
Actuator 使用 Micrometer 作为指标的抽象层。无需额外配置,你已经拥有 JVM、CPU、HTTP 请求、连接池等指标。
curl http://localhost:8080/actuator/metrics
{"names":["application.ready.time","http.server.requests","jvm.gc.pause","jvm.memory.used","process.cpu.usage", ...]}
要查看某个指标的详细信息,将其名称添加到 URL。可以使用 tag 参数按标签过滤:
curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/orders"
4.1. 订单数的计数器
系统指标还不够,你通常还需要业务指标。下面的示例计算已创建的订单数,按销售渠道分类:
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final MeterRegistry registry;
public OrderService(MeterRegistry registry) {
this.registry = registry;
}
public Order createOrder(Customer customer, String channel) {
Order order = new Order(customer.id(), channel);
// ... 保存订单
Counter.builder("orders.created")
.description("已创建的订单数")
.tag("channel", channel)
.register(registry)
.increment();
return order;
}
}
public record Customer(long id, String name) {}
public record Order(long customerId, String channel) {}
MeterRegistry 由 Spring Boot 自动配置并注入。多次使用相同名称和标签调用 register(registry) 是安全的,因为 Micrometer 会返回已存在的计数器。创建三个通过 web 渠道的订单后:
curl http://localhost:8080/actuator/metrics/orders.created
{
"name": "orders.created",
"description": "已创建的订单数",
"measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
"availableTags": [ { "tag": "channel", "values": ["web"] } ]
}
4.2. 支付处理时间的计时器
Timer 既计算调用次数,也测量总时间和最大时间:
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final Timer paymentTimer;
public PaymentService(MeterRegistry registry) {
this.paymentTimer = Timer.builder("payment.process")
.description("支付处理时间")
.register(registry);
}
public boolean pay(Order order) {
return paymentTimer.record(() -> {
// ... 调用支付网关
return true;
});
}
}
Timer 在构造函数中创建一次,然后重复使用,因为没有每次调用都改变的标签。record(Supplier) 测量 lambda 的执行时间并返回其结果。端点 /actuator/metrics/payment.process 将显示三个统计信息:COUNT、TOTAL_TIME 和 MAX。
4.3. 所有指标的公共标签
当多个服务将指标推送到一个地方时,你应该将应用名称附加到每个指标:
management.metrics.tags.application=order-service
注意: 不要使用数量大或无限制的值作为标签,例如
orderId、customerId或电子邮件。每个标签值组合创建一个单独的时间序列。数百万个订单会创建数百万个系列,导致应用程序内存膨胀和监控系统压力。标签应仅包含小的、固定的值集,如channel、status或region。
5. 导出到 Prometheus
端点 /actuator/metrics 适合快速查看。为了长期存储和绘制图表,你需要像 Prometheus 这样的系统。添加相应的 registry:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
然后公开 prometheus 端点:
management.endpoints.web.exposure.include=health,info,metrics,prometheus
curl http://localhost:8080/actuator/prometheus | grep -E "orders|payment"
# HELP orders_created_total 已创建的订单数
# TYPE orders_created_total counter
orders_created_total{application="order-service",channel="web"} 3.0
payment_process_seconds_count{application="order-service"} 5.0
payment_process_seconds_sum{application="order-service"} 0.412
payment_process_seconds_max{application="order-service"} 0.153
Micrometer 根据 Prometheus 约定自动重命名:点改为下划线,计数器带有 _total 后缀,计时器以秒为单位。剩余部分是配置 Prometheus 抓取 /actuator/prometheus 路径。
6. 测试
6.1. 测试 HealthIndicator
HealthIndicator 是一个普通类,所以你可以不需要启动 Spring context 来测试它:
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import static org.assertj.core.api.Assertions.assertThat;
class PaymentGatewayHealthIndicatorTest {
@Test
void whenPingSucceeds_thenStatusIsUp() {
var indicator = new PaymentGatewayHealthIndicator(() -> true);
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("provider", "VNPay");
}
@Test
void whenPingThrows_thenStatusIsDownWithError() {
var indicator = new PaymentGatewayHealthIndicator(() -> {
throw new IllegalStateException("timeout");
});
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails().get("error").toString()).contains("timeout");
}
}
由于 PaymentGatewayClient 只有一个方法,你可以传递 lambda 而不是 mock。
6.2. 测试指标
Micrometer 提供 SimpleMeterRegistry 将指标存储在内存中,非常适合单元测试:
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class OrderServiceTest {
@Test
void whenOrdersCreated_thenCounterIncrementsPerChannel() {
var registry = new SimpleMeterRegistry();
var service = new OrderService(registry);
var customer = new Customer(1L, "An");
service.createOrder(customer, "web");
service.createOrder(customer, "web");
service.createOrder(customer, "mobile");
assertThat(registry.get("orders.created").tag("channel", "web").counter().count())
.isEqualTo(2.0);
assertThat(registry.get("orders.created").tag("channel", "mobile").counter().count())
.isEqualTo(1.0);
}
}
如果指标不存在,registry.get(...) 会抛出 MeterNotFoundException。这样,测试也可以捕获拼写错误的指标名称。
7. 安全性和常见错误
分离管理端口。 你可以让 Actuator 在单独的端口上运行,仅在内部网络中公开:
management.server.port=8081
这样,应用程序在 8080 上提供服务,而 /actuator/** 仅在 8081 上可用。在防火墙或 ingress 处阻止此端口即可。在 Kubernetes 中,记得在 probe 配置中将端口改为 8081。
限制健康检查详情。 在生产环境中,使用 show-details=when-authorized 结合 Spring Security,而不是 always。匿名用户只会看到 {"status":"UP"}。
一些常见错误:
- 调用
/actuator/metrics收到404:你还没有将metrics添加到exposure.include。 - 健康检查随机失败:某个 indicator 进行没有超时的网络调用,或者响应时间超过了 probe 的超时时间。
- Pod 在数据库间歇性故障时持续重启:数据库在 liveness 组中。
- 内存随时间不断增长:指标使用了数量无限或值无限的标签,例如
orderId或customerId。 - 看不到
/actuator/prometheus端点:缺少micrometer-registry-prometheus或尚未公开该端点。
8. 总结
Spring Boot Actuator 几乎免费提供健康检查和指标。你的工作是正确配置它:仅公开必要的端点,为重要的依赖编写具有超时的 HealthIndicator,并将 liveness 与 readiness 分离,使 Kubernetes 做出适当的反应。
对于指标,Micrometer 帮助你只用几行代码记录业务的 Counter 和 Timer,然后通过一个依赖导出到 Prometheus。请确保标签的值集保持较小,并使用 SimpleMeterRegistry 编写测试,以防指标在每次重构后悄悄损坏。