Spring Boot 中的健康指标(Health Indicators)

1、概览

Spring Boot 提供了几种不同的方法来检查运行中的应用及其组件的状态和健康状况。在这些方法中,HealthContributorHealthIndicator API 是值得注意的两种。

本文将带你了解这些 API 的原理以及如何向它们提供自定义信息。

2、依赖

Health Information Contributor(健康信息贡献者)是 Spring Boot Actuator 模块的一部分:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

3、内置的 HealthIndicator

Spring Boot 注册了许多开箱即用的 HealthIndicator 来报告应用特定方面的健康状况。

其中一些指标默认已经注册了,如 DiskSpaceHealthIndicatorPingHealthIndicator。前者报告磁盘的当前状态,后者则作为应用的 ping 端点。

Spring Boot 也会有条件地注册一些指标。也就是说,如果 classpath 上存在某些依赖或满足某些其他条件,Spring Boot 可能也会注册一些其他的 HealthIndicator。例如,如果使用了关系型数据库,那么 Spring Boot 就会注册 DataSourceHealthIndicator。同样,如果使用的是 Cassandra 作为数据存储,它会注册 CassandraHealthIndicator

可以调用 /actuator/health 端点来检查 Spring Boot 应用的健康状态。该端点会报告所有已注册的 HealthIndicator 的汇总结果。

如果要查看某个特定指标的健康报告,可以调用 /actuator/health/{name} 端点。例如,调用 /actuator/health/diskSpace 端点将返回 DiskSpaceHealthIndicator 的状态报告:

{
  "status": "UP",
  "details": {
    "total": 499963170816,
    "free": 134414831616,
    "threshold": 10485760,
    "exists": true
  }
}

4、自定义 HealthIndicator

除了内置的 HealthIndicator,还可以注册自定义 HealthIndicator 来报告组件或子系统的健康状况。只需将 HealthIndicator 接口的实现注册为 Spring Bean 即可。

例如,以下实现会随机报告故障:

@Component
public class RandomHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        double chance = ThreadLocalRandom.current().nextDouble();
        Health.Builder status = Health.up();
        if (chance > 0.9) {
            status = Health.down();
        }
        return status.build();
    }
}

根据该指标的健康报告,应用应该只有 90% 的时间处于运行状态。在这里,使用了 Health builder 来报告健康信息。

在响应式应用中,应该注册一个 ReactiveHealthIndicator 类型的Bean,响应式方法 health() 返回的是 Mono<Health>,而不是简单的 Health。除此之外,两种 Web 应用类型的其他细节是相同的。

4.1、指标名称

要查看上述这个特定指标的报告,可以调用 /actuator/health/random 端点。如下:

{"status": "UP"}

/actuator/health/random URL 中的 random 是该 Indicator 的标识符。特定 HealthIndicator 实现的标识符等于不含 HealthIndicator 后缀的 Bean 名称。由于 Bean 名称是 randomHealthIdenticator,因此 random 前缀就是标识符。

如果将 Bean 名称改为 rand

@Component("rand")
public class RandomHealthIndicator implements HealthIndicator {
    // 忽略。。。
}

那么 Indicator 标识符将是 rand,而不是 random

4.2、禁用 Indicator

要禁用某个指标,可以将配置属性 management.health.<indicator_identifier>.enabled 设为 false

例如,在 application.properties 中添加以下内容:

management.health.random.enabled=false

然后,Spring Boot 将禁用 RandomHealthIndicator。要激活此配置属性,还要在 Indicator 上添加 @ConditionalOnEnabledHealthIndicator 注解:

@Component
@ConditionalOnEnabledHealthIndicator("random")
public class RandomHealthIndicator implements HealthIndicator { 
    // 忽略
}

现在,如果调用 /actuator/health/random,Spring Boot 会返回 404 Not Found HTTP 错误响应:

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = "management.health.random.enabled=false")
class DisabledRandomHealthIndicatorIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void givenADisabledIndicator_whenSendingRequest_thenReturns404() throws Exception {
        mockMvc.perform(get("/actuator/health/random"))
          .andExpect(status().isNotFound());
    }
}

注意,禁用内置或自定义指标的方式相似。因此,也可以将相同的配置应用于内置指标。

4.3、详情

除了报告状态外,还可以使用 withDetail(key, value) 方法附加 Key/Value 格式的细节信息:

public Health health() {
    double chance = ThreadLocalRandom.current().nextDouble();
    Health.Builder status = Health.up();
    if (chance > 0.9) {
        status = Health.down();
    }

    return status
      .withDetail("chance", chance)
      .withDetail("strategy", "thread-local")
      .build();
}

如上,向状态报告添加了两条信息。此外,还可以通过向 withDetails(map) 方法传递 Map<String, Object> 来实现同样的目的:

Map<String, Object> details = new HashMap<>();
details.put("chance", chance);
details.put("strategy", "thread-local");
        
return status.withDetails(details).build();

现在,如果调用 /actuator/health/random,可能会看到类似如下的内容:

{
  "status": "DOWN",
  "details": {
    "chance": 0.9883560157173152,
    "strategy": "thread-local"
  }
}

也可以通过自动测试来进行验证:

mockMvc.perform(get("/actuator/health/random"))
  .andExpect(jsonPath("$.status").exists())
  .andExpect(jsonPath("$.details.strategy").value("thread-local"))
  .andExpect(jsonPath("$.details.chance").exists());

有时,在与数据库或磁盘等系统组件通信时会出现异常。可以使用 withException(ex)方法报告此类异常:

if (chance > 0.9) {
    status.withException(new RuntimeException("Bad luck"));
}

还可以将异常传递给前面提到的 down(ex) 方法:

if (chance > 0.9) {
    status = Health.down(new RuntimeException("Bad Luck"));
}

现在,健康报告将包含 Stack Trace:

{
  "status": "DOWN",
  "details": {
    "error": "java.lang.RuntimeException: Bad Luck",
    "chance": 0.9603739107139401,
    "strategy": "thread-local"
  }
}

4.4、控制详情的暴露

management.endpoint.health.show-details 配置属性可控制每个健康状况端点可显示的详细信息级别。

例如,如果将此属性设置为 always,那么 Spring Boot 将始终在健康报告中返回 details 字段,就像上面的例子一样。

如果将此属性设置为 never,那么 Spring Boot 将始终从输出中省略 details 。还有一个 when_authorized 值,它只为授权用户提供额外的 details。当且仅当以下情况时,用户才被授权:

  • 经过认证(登录)
  • 拥有 management.endpoint.health.roles 配置属性中指定的角色

4.5、健康状态

默认情况下,Spring Boot 定义了四种不同的 Health Status 值:

  • UP — 组件或子系统按预期运行
  • DOWN — 组件无法运行
  • OUT_OF_SERVICE — 组件暂时无法使用
  • UNKNOWN — 组件状态未知

这些状态被声明 public static final 实例,而不是 Java 枚举。因此,可以使用 status(name) 方法来定义自己的自定义健康状态。

Health.Builder warning = Health.status("WARNING");

健康状态会影响 health 端点的 HTTP 状态码。默认情况下,Spring Boot 会将 DOWNOUT_OF_SERVICE 状态映射为 503 状态码。而 UP 和任何其他未映射的状态将被转换为 200 OK 状态码。

要自定义此映射,可以将 management.endpoint.health.status.http-mapping.<status> 配置属性设置为所需的 HTTP 状态码:

management.endpoint.health.status.http-mapping.down=500
management.endpoint.health.status.http-mapping.out_of_service=503
management.endpoint.health.status.http-mapping.warning=500

如上,Spring Boot 把 DOWN 状态映射为 500,把 OUT_OF_SERVICE 映射为 503,把 WARNING 映射为 500 HTTP 状态码:

mockMvc.perform(get("/actuator/health/warning"))
  .andExpect(jsonPath("$.status").value("WARNING"))
  .andExpect(status().isInternalServerError());

可以注册一个 HttpCodeStatusMapper 类型的 Bean 来定制 HTTP 状态码映射:

@Component
public class CustomStatusCodeMapper implements HttpCodeStatusMapper {

    @Override
    public int getStatusCode(Status status) {
        if (status == Status.DOWN) {
            return 500;
        }
        
        if (status == Status.OUT_OF_SERVICE) {
            return 503;
        }
        
        if (status == Status.UNKNOWN) {
            return 500;
        }

        return 200;
    }
}

getStatusCode(status) 方法接收健康状态(Status)作为输入参数,并返回 HTTP 状态码。这也可以映射自定义的 Status 实例。

if (status.getCode().equals("WARNING")) {
    return 500;
}

默认情况下,Spring Boot 会注册一个简单的接口实现 SimpleHttpCodeStatusMapper,并使用默认映射。该实现还能够从配置文件中读取映射,如前所述。

5、健康信息与指标

非复杂应用通常包含几个不同的组件。例如,考虑一个使用 Cassandra 作为数据库、Apache Kafka 作为发布子平台、Hazelcast 作为内存数据网格的 Spring Boot 应用。

应该使用 HealthIndicator 来查看应用是否能与这些组件通信。如果通信链路出现故障,或者组件本身出现故障或运行缓慢,那么就应该注意组件是否处于不健康状态。换句话说,这些指标应该用来报告不同组件或子系统的健康状况。

应该避免使用 HealthIndicator 来测量值、计算事件或测量持续时间。而应该使用指标。简而言之,指标是报告 CPU 使用率、平均负载、堆栈大小、HTTP 响应分布等的更好工具。

6、总结

本文介绍了 Spring Boot 中的 HealthIndicator。了解了如何向 Actuator Health 端点提供更多详细信息,以及 Health API 中的不同组件,如 HealthStatusHttpCodeStatusMapper


Ref:https://www.baeldung.com/spring-boot-health-indicators