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 编写测试,以防指标在每次重构后悄悄损坏。

spring-bootactuatormicrometer监控prometheus