第 5 章:面向生产的 Spring Boot
在 4.1.2 节中,我们介绍了 Spring Boot 的四大核心组成部分,第 4 章主要介绍了其中的起步依赖与自动配置,本章将重点介绍 Spring Boot Actuator,包括如何通过 Actuator 提供的各种端点(endpoint)了解系统的运行情况,使用 Micrometer 为各种监控系统提供度量指标数据,最后还要了解如何打包部署 Spring Boot 应用程序。
5.1 Spring Boot Actuator 概述
Spring Boot Actuator 是 Spring Boot 的重要功能模块,能为系统提供一系列在生产环境中运行所必需的功能,比如监控、度量、配置管理等。只需引入 org.springframework.boot:spring-boot-starter-actuator
起步依赖后,我们就可以通过 HTTP 来访问这些功能(也可以使用 JMX 来访问)。Spring Boot 还为我们预留了很多配置,可以根据自己的需求对 Spring Boot Actuator 的功能进行定制。
5.1.1 端点概览
不知道大家有没有尝试解决过类似下面的问题:
- Spring 上下文中到底存在哪些 Bean
- Spring Boot 中的哪些自动配置最终生效了
- 应用究竟获取到了哪些配置项
- 系统中存在哪些 URL,它们又映射到了哪里
在没有 Spring Boot Actuator 的时候,获取这些信息还是需要费一番功夫的;但现在就不一样了,Spring Boot Actuator 内置了大量的端点,这些端点可以帮助大家了解系统内部的运行情况,并针对一些功能做出调整。
根据功能的不同,我们可以将这些端点划分成四类:信息类端点、监控类端点、操作类端点、集成类端点,其中部分端点需要引入特定的依赖,或者配置特定的 Bean。接下来我们会依次介绍这四类端点,首先是用于获取系统运行信息的端点,如表 5-1 所示。
表 5-1 Spring Boot Actuator 中的信息类端点列表
端点 ID | 默认开启 HTTP | 默认开启 JMX | 端点说明 |
---|---|---|---|
auditevents | 否 | 是 | 提供系统的审计信息 |
beans | 否 | 是 | 提供系统中的 Bean 列表 |
caches | 否 | 是 | 提供系统中的缓存信息 |
conditions | 否 | 是 | 提供配置类的匹配情况及条件运算结果 |
configprops | 否 | 是 | 提供 @ConfigurationProperties 的列表 |
env | 否 | 是 | 提供 ConfigurableEnvironment 中的属性信息 |
flyway | 否 | 是 | 提供已执行的 Flyway 数据库迁移信息 |
httptrace | 否 | 是 | 提供 HTTP 跟踪信息,默认最近 100 条 |
info | 是 | 是 | 显示事先设置好的系统信息 |
integrationgraph | 否 | 是 | 提供 Spring Integration 图信息 |
liquibase | 否 | 是 | 提供已执行的 Liquibase 数据库迁移信息 |
logfile | 否 | 无此功能 | 如果设置了 logging.file.name 或 logging.file.path 属性,则显示日志文件内容 |
mappings | 否 | 是 | 提供 @RequestMapping 的映射列表 |
scheduledtasks | 否 | 是 | 提供系统中的调度任务列表 |
第二类端点是监控与度量相关的端点,具体如表 5-2 所示。
表 5-2 Spring Boot Actuator 中的监控类端点列表
端点 ID | 默认开启 HTTP | 默认开启 JMX | 端点说明 |
---|---|---|---|
health | 是 | 是 | 提供系统运行的健康状态 |
metrics | 否 | 是 | 提供系统的度量信息 |
prometheus | 否 | 无此功能 | 提供 Prometheus 系统可解析的度量信息 |
我们对第 1 章的 helloworld
示例稍作调整,在其 pom.xml 的 <dependencies/>
中增加如下内容即可引入 Spring Boot Actuator 的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
从上述两张表中我们可以发现,默认只有 info
和 health
两个端点是开启了 HTTP 访问的,因此在运行程序后,通过浏览器或者其他方式访问 http://localhost:8080/actuator/health
就能访问到 health
端点的信息。如果在 macOS 或 Linux 上,我们可以使用 curl
命令 1,具体运行结果如下:
▸ curl -v http://localhost:8080/actuator/health
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /actuator/health HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: application/vnd.spring-boot.actuator.v3+json
< Transfer-Encoding: chunked
< Date: Fri, 10 Jul 2020 15:38:54 GMT
<
* Connection #0 to host localhost left intact
{"status":"UP"}* Closing connection 0
如果使用浏览器,访问的效果如图 5-1 所示。
图 5-1 通过 Chrome 浏览器查看 health
端点
第三类端点可以执行一些实际的操作,例如调整日志级别,具体如表 5-3 所示。
表 5-3 Spring Boot Actuator 中的操作类端点列表
端点 ID | 默认开启 HTTP | 默认开启 JMX | 端点说明 |
---|---|---|---|
heapdump | 否 | 无此功能 | 执行 Heap Dump 操作 |
loggers | 否 | 是 | 查看并修改日志信息 |
sessions | 否 | 是 | 针对使用了 Spring Session 的系统,可获取或删除用户的 Session |
shutdown | 否 | 否 | 优雅地关闭系统 |
threaddump | 否 | 是 | 执行 Thread Dump 操作 |
最后一类端点比较特殊,它的功能与集成有关,就只有一个 jolokia
,见表 5-4。
表 5-4 Spring Boot Actuator 中的集成类端点列表
端点 ID | 默认开启 HTTP | 默认开启 JMX | 端点说明 |
---|---|---|---|
jolokia | 否 | 无此功能 | 通过 HTTP 来发布 JMX Bean |
5.1.2 端点配置
在了解了 Spring Boot 提供的端点后,我们就要将它们投入具体的生产使用当中了。Spring Boot Actuator 非常灵活,它提供了大量的开关配置,还有两种不同的访问方式可供我们选择。接下来大家就来一起了解一下这些配置。
-
开启或禁用端点
默认情况下,除了
shutdown
以外,所有的端点都是处于开启状态的,只是访问方式不同,或者保护状态不同。如果要开启或者禁用某个端点,可以调整management.endpoint.<id>.enabled
属性。例如,想要开启shutdown
端点,就可以这样配置:management.endpoint.shutdown.enabled=true
也可以调整默认值,禁用所有端点,随后开启指定端点。例如,只开启
health
端点:management.endpoints.enabled-by-default=false management.endpoint.health.enabled=true
-
通过 HTTP 访问端点
默认仅有
health
和info
端点是可以通过 HTTP 方式来访问的,在 5.1.1 节中,我们已经看到了如何通过curl
命令和浏览器来访问health
端口。那如何才能开启其他端点的 HTTP 访问功能呢?可以使用management.endpoints.web.exposure.include
和management.endpoints.web.exposure.exclude
这两个属性来控制哪些端点可以通过 HTTP 方式发布,哪些端点不行。前者的默认值为health,info
,后者的默认值为空。例如,我们希望在原有基础上再增加
beans
和env
端点,就可以这样来设置:management.endpoints.web.exposure.include=beans,env,health,info
如果希望放开所有的端点,让它们都能通过 HTTP 方式来访问,我们就可以将上述属性设置为
*
:management.endpoints.web.exposure.include=*
要是一个端点同时出现在
management.endpoints.web.exposure.include
和management.endpoints.web.exposure.exclude
这两个属性里,那么后者的优先级会高于前者,也就是说该端点会被排除。如果我们希望了解 HTTP 方式可以访问哪些端点,可以直接访问
/actuator
地址,会得到类似下面的 JSON 信息:{ "_links": { "health": { "href": "http://localhost:8080/actuator/health", "templated": false }, "health-path": { "href": "http://localhost:8080/actuator/health/{*path}", "templated": true }, "info": { "href": "http://localhost:8080/actuator/info", "templated": false }, "self": { "href": "http://localhost:8080/actuator", "templated": false } } }
其中
templated
为true
的 URL 可以用具体的值去代替{}
里的内容,比如,http://localhost:8080/actuator/metrics/
的 `` 就可以用http://localhost:8080/actuator/metrics
里所罗列的名称代替。需要特别说明一点,要发布 HTTP 端点,必须要有 Web 支持,因此项目需要引入
spring-boot-starter-web
起步依赖。 -
通过 JMX 访问端点
与 HTTP 方式类似,JMX 也有两个属性,即
management.endpoints.jmx.exposure.include
和management.endpoints.jmx.exposure.exclude
。前者的默认值为*
,后者的默认值为空。有不少工具可以用来访问 JMX 端点,比如 JVisualVM 和 JConsole,它们都是 JDK 自带的工具。以 JConsole 为例,启动 JConsole 后会弹出新建连接界面,从中可以选择想要连接的本地 Java 进程,也可以通过指定信息连接远程的进程,具体如图 5-2 所示。
图 5-2 JConsole 的新建连接界面
选中目标进程后,点击连接按钮,稍过一段时间后,就能连上目标进程了。随后选中 MBean 标签页,在
org.springframework.boot
目录下找到Endpoint
,其中列出的就是可以访问的 JMX 端点。图 5-3 就是选择了Health
后界面的样子,点击health
按钮即可获得健康检查的 JSON 结果。图 5-3 通过 JMX 方式访问
health
端点 -
保护端点
如果在工程中引入了 Spring Security,那么 Spring Boot Actuator 会自动对各种端点进行保护,例如,默认通过浏览器访问时,浏览器会显示一个页面,要求输入用户名和密码。如果我们没有配置过相关信息,那么在系统启动时,可以在日志中查找类似下面的日志:
Using generated security password: 4fbc8059-fdb8-46f9-a54e-21d5cb2e9eb2
默认的用户名是
user
,密码就是上面这段随机生成的内容。关于 Spring Security 的更多细节,我们会在第 10 章中详细展开。此处,给出两个示例,它们都需要在 pom.xml 中增加spring-boot-starter-actuator
和spring-boot-starter-security
起步依赖。这里先来演示如何不用登录页,而是使用 HTTP Basic 的方式进行验证(但health
端点除外),具体的配置如代码示例 5-1 所示 4,注意其中的EndpointRequest
用的是org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest
。代码示例 5-1 需要认证才可访问端点的配置代码片段
@Configuration public class ActuatorSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatcher(EndpointRequest.toAnyEndpoint().excluding("health")). authorizeRequests((requests) -> requests.anyRequest().authenticated()); http.httpBasic(); } }
第二个演示针对所有端点,可以提供匿名访问,具体如代码示例 5-2 所示。
代码示例 5-2 可匿名访问端点的配置代码片段
@Configuration public class ActuatorSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests((requests) -> requests. anyRequest().anonymous()); http.httpBasic(); } }
茶歇时间:针对 Web 和 Actuator 使用不同端口的好处
试想一下,我们的系统在对外提供 HTTP 服务,一般会在集群前增加一个负载均衡设备(比如 Nginx),将外部对
80
端口的请求转发至系统的8080
端口。如果 Spring Boot Actuator 的端口也在8080
,而我们又没对端点做足够的保护,黑客很轻松地就能获取系统的信息。就算做了保护,黑客也能通过端点信息推断出这个系统是通过 Spring Boot 实现的、可能存在哪些漏洞。一般我们都会在系统启动后,通过
health
端点来判断系统的健康情况,如果对这个端点做了过多保护,反而不便于获取健康检查结果。一种做法是在防火墙或者负载均衡层面,禁止外部访问 Spring Boot Actuator 的 URL,例如,直接禁止访问
/actuator
及其子路径。另一种做法,就是索性让 Actuator 的端点暴露在与业务代码不同的 HTTP 端口上,比如,不再共用8080
端口,而是单独提供一个8081
端口,而防火墙和负载均衡设备只知道8080
端口的存在,也只会转发请求到8080
端口,就不用担心外部能访问到8081
端口的问题了。通过
management.server.port=8081
能实现设置 Actuator 专属端口的功能。更进一步,我们还可以使用management.server.base-path
属性(以前是management.server.servlet.context-path
)为 Spring Boot Actuator 设置 Servlet 上下文,默认为空;使用management.endpoints.web.base-path
属性来调整/actuator
这个默认的基础路径。如果像下面这样来做设置,就能将health
端点访问的 URL 调整为http://localhost:8081/management/my-actuator/health
了:management.server.port=8081 management.server.base-path=/management management.endpoints.web.base-path=/my-actuator
5.1.3 定制端点信息
Spring Boot Actuator 中的每个端点或多或少会有一些属于自己的配置属性,大家可以在 org.springframework.boot:spring-boot-actuator-autoconfigure
包中查看各种以 Properties
结尾的属性类,也可以直接通过 configprops
端点来查看属性类。
例如, EnvironmentEndpointProperties
就对应了 management.endpoint.env
中的属性,其中的 keysToSanitize
就是环境中要过滤的自定义敏感信息键名清单,根据代码注释,其中可以设置匹配的结尾字符串,也可以使用正则表达式。在设置了 management.endpoint.env.keys-to-sanitize=java.*,sun.*
后, env
端点返回的属性中,所有 java
和 sun
打头的属性值都会以 *
显示。
Spring Boot Actuator 默认为 info
和 health
端点开启了 HTTP 访问支持,那么就让我们来详细了解一下这两个端点有哪些可以定制的地方吧。
-
定制 info 端点信息
根据
InfoEndpointAutoConfiguration
可以得知,InfoEndpoint
中会注入 Spring 上下文中的所有InfoContributor
Bean 实例。InfoContributorAutoConfiguration
自动注册了env
、git
和build
这三个InfoContributor
,Spring Boot Actuator 提供的InfoContributor
列表如表 5-5 所示。表 5-5 内置
InfoContributor
列表
类名 | 默认开启 | 说明 |
---|---|---|
BuildInfoContributor | 是 | 提供 BuildProperties 的信息,通过 spring.info.build 来设置,默认读取 META-INF/build-info.properties |
EnvironmentInfoContributor | 是 | 将配置中以 info 打头的属性通过端点暴露 |
GitInfoContributor | 是 | 提供 GitProperties 的信息,通过 spring.info.git 来设置,默认读取git.properties |
InfoPropertiesInfoContributor | 否 | 抽象类,一般作为其他 InfoContributor 的父类 |
MapInfoContributor | 否 | 将内置 Map 作为信息输出SimpleInfoContributor 否仅包含一对键值对的信息 |
假设在配置文件中设置了如下内容:
info.app=HelloWorld
info.welcome=Welcome to the world of Spring.
再提供如下的 Bean:
@Bean
public SimpleInfoContributor simpleInfoContributor() {
return new SimpleInfoContributor("simple", "HelloWorld!");
}
那 info 端点输出的内容大致如下所示:
{
"app": "HelloWorld",
"simple": "HelloWorld!",
"welcome": "Welcome to the world of Spring."
}
-
定制 health 端点信息
健康检查是一个很常用的功能,可以帮助我们了解系统的健康状况,例如,系统在启动后是否准备好对外提供服务了,所依赖的组件是否已就绪等。
健康检查主要是依赖
HealthIndicator
5 的各种实现来完成的。Spring Boot Actuator 内置了近 20 种不同的实现,表 5-6 列举了一些常用的HealthIndicator
实现,基本可以满足日常使用的需求。表 5-6 常用的
HealthIndicator
实现
实现类 | 作用 |
---|---|
DataSourceHealthIndicator | 检查 Spring 上下文中能取到的所有 DataSource 是否健康 |
DiskSpaceHealthIndicator | 检查磁盘空间 |
LivenessStateHealthIndicator | 检查系统的存活(Liveness)情况,一般用于 Kubernetes 中 |
ReadinessStateHealthIndicator | 检查系统是否处于就绪(Readiness)状态,一般用于 Kubernetes 中 |
RedisHealthIndicator | 检查所依赖的 Redis 健康情况 |
每个 HealthIndicator
检查后都会有自己的状态信息,Spring Boot Actuator 最后会根据所有结果的状态信息综合得出系统的最终状态。 org.springframework.boot.actuate.health.Status
定义了几种默认状态,按照优先级降序排列分别为 DOWN
、 OUT_OF_SERVICE
、 UP
和 UNKNOWN
,所有结果状态中优先级最高的状态会成为 health
端点的最终状态。如果有需要,我们也可以通过 management.endpoint.health.status.order
来更改状态的优先级。
Spring Boot Actuator 默认开启了所有的 HealthIndicator
,它们会根据情况自行判断是否生效,也可以通过 management.health.defaults.enabled=false
开关(默认关闭),随后使用 management.health.<name>.enabled
选择性地开启 HealthIndicator
。例如, DataSourceHealthContributorAutoConfiguration
是这样定义的:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ JdbcTemplate.class, AbstractRoutingDataSource.class })
@ConditionalOnBean(DataSource.class)
@ConditionalOnEnabledHealthIndicator("db")
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class DataSourceHealthContributorAutoConfiguration extends
CompositeHealthContributorConfiguration
<AbstractHealthIndicator, DataSource>
implements InitializingBean {}
那么它的生效条件是这样的:
- CLASSPATH 中存在
JdbcTemplate
和AbstractRoutingDataSource
类; - Spring 上下文中存在
DataSource
类型的 Bean; - 默认开关打开,或者
management.health.db.enabled=true
,此处@ConditionalOnEnabledHealthIndicator
中的db
就是name
。
知道了 health
中的各个 HealthIndicator
后,怎么才能看到结果呢?我们可以通过配置 management.endpoint.health.show-details
和 management.endpoint.health.show-components
的属性值来查看结果,默认是 never
,将其调整为 always
后就会始终显示具体内容了。如果依赖中存在 Spring Security,也可以仅对授权后的用户开放,将属性值配置为 when-authorized
,这时我们可以通过 management.endpoint.health.roles
来设置可以访问的用户的角色。一般情况下,可以考虑将其调整为 always
。
如今越来越多的系统运行在 Kubernetes 环境中,而 Kubernetes 需要检查系统是否存活,是否就绪,对此, health
端点也提供了对应的 LivenessStateHealthIndicator
和 ReadinessStateHealthIndicator
,默认 URL 分别为 /actuator/health/liveness
和 /actuator/health/readiness
。
5.1.4 开发自己的组件与端点
在上一节中,我们看到的基本都是对现有端点与组件的配置,Spring Boot Actuator 提供了让我们自己扩展端点或者实现新端点的功能。例如,在进行健康检查时,我们可以加入自己的检查逻辑,只需实现 HealthIndicator
即可。
-
开发自己的 HealthIndicator
为了增加自己的健康检查逻辑,我们可以定制一个 HealthIndicator 实现,通常会选择扩展
AbstractHealthIndicator
类,实现其中的doHealthCheck()
方法。根据
HealthEndpointConfiguration
类的代码,我们可以知道healthContributorRegistry
会从 Spring 上下文获取所有HealthContributor
类型(HealthIndicator
继承了这个接口)的 Bean,并进行注册,所以我们也只需要把写好的HealthIndicator
配置为 Bean 即可。@Configuration(proxyBeanMethods = false) class HealthEndpointConfiguration { @Bean @ConditionalOnMissingBean HealthContributorRegistry healthContributorRegistry(ApplicationContext applicationContext, HealthEndpointGroups groups) { Map<String, HealthContributor> healthContributors = new LinkedHashMap<>(applicationContext .getBeansOfType(HealthContributor.class)); if (ClassUtils.isPresent("reactor.core.publisher.Flux", applicationContext.getClassLoader())) { healthContributors.putAll(new AdaptedReactiveHealthContributors(applicationContext).get()); } return new AutoConfiguredHealthContributorRegistry(healthContributors, groups.getNames()); } // 省略其他代码 }
接下来,我们以第 4 章的 BinaryTea 项目作为基础,在其中添加自己的 HealthIndicator
。在 ShopReadyHealthIndicator
上添加 @Component
注解,以便在扫描到它后就能将其注册为 Bean。通过构造方法注入 BinaryTeaProperties
,检查时如果没有 binaryTeaProperties
或者属性中的 ready
为 false
,检查即为失败,除此之外都算成功。具体如代码示例 5-3 所示。
代码示例 5-3 ShopReadyHealthIndicator
健康检查器
@Component
public class ShopReadyHealthIndicator extends AbstractHealthIndicator{
private BinaryTeaProperties binaryTeaProperties;
public ShopReadyHealthIndicator(ObjectProvider<BinaryTeaProperties> binaryTeaProperties) {
this.binaryTeaProperties = binaryTeaProperties.getIfAvailable();
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if (binaryTeaProperties == null || !binaryTeaProperties.isReady()) {
builder.down();
} else {
builder.up();
}
}
}
运行代码前还需要在 pom.xml 中加入 Spring Web 和 Spring Boot Actuator 的依赖,具体如下:
<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>
同时在 application.properties
中增加 management.endpoint.health.show-details=always
,以便可以看到 ShopReadyHealthIndicator
的效果。运行 BinaryTeaApplication
后,在浏览器中访问 http://localhost:8080/actuator/health,就能看到类似下面的 JSON 输出:
{
"components": {
// 省略部分内容
"ping": {
"status": "UP"
},
"shopReady": {
"status": "UP"
}
},
"status": "UP"
}
为了能够进行自动测试,我们还可以在 ShopConfigurationEnableTest
和 ShopConfigurationDisableTest
中分别加入对应的测试用例,再写个测试检查是否注册了对应的信息,如代码示例 5-4 所示。
代码示例 5-4 针对 ShopReadyHealthIndicator
的各种单元测试用例
// ShopConfigurationEnableTest中的测试用例
@Test
void testIndicatorUp() {
ShopReadyHealthIndicator indicator = applicationContext.getBean(ShopReadyHealthIndicator.class);
assertEquals(Status.UP, indicator.getHealth(false).getStatus());
}
// ShopConfigurationDisableTest中的测试用例
@Test
void testIndicatorDown() {
ShopReadyHealthIndicator indicator = applicationContext.getBean(ShopReadyHealthIndicator.class);
assertEquals(Status.DOWN, indicator.getHealth(false).getStatus());
}
// 独立的ShopReadyHealthIndicatorTest,测试是否注册了shopReady
@SpringBootTest
public class ShopReadyHealthIndicatorTest {
@Autowired
private HealthContributorRegistry registry;
@Test
void testRegistryContainsShopReady() {
assertNotNull(registry.getContributor("shopReady"));
}
}
如果一切顺利,执行 mvn test
后我们应该就能看到测试通过的信息了:
[INFO] Results:
[INFO]
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
茶歇时间:为什么要优先通过
ObjectProvider
获取 Bean当我们需要从 Spring 上下文中获取其他 Bean 时,最直接的方法是使用
@Autowired
注解,但系统运行时的不确定性太多了,比如不确定是否存在需要的依赖,这时就需要加上required=false
;也有可能目标类型的 Bean 不止一个,而我们只需要一个;构造方法有多个参数……这时就该
ObjectProvider<T>
上场了,它大多用于构造方法注入的场景,让我们有能力处理那些尴尬的场面,其中的getIfAvailable()
方法在存在对应 Bean 时返回对象,不存在时则返回null
;getIfUnique()
方法在有且仅有一个对应 Bean 时返回对象,Bean 不存在或不唯一,且不唯一时没有标注Primary
的情况下返回null
。再加上一些检查和遍历的方法,通过明确的编码,我们就可以确保自己的代码获取到必要的依赖,或判断出缺少的东西,并加以处理。
-
开发自己的端点
如果内置的端点无法满足我们的需求,那最后一招就是写一个自己的端点。好在 Spring Boot Actuator 支持到位,只需简单几步就能帮助我们实现一个端点。
首先,在 Bean 上添加
@Endpoint
注解,其中带有@ReadOperation
、@WriteOperation
和@DeleteOperation
的方法能被发布出来,而且能通过 JMX 或者 HTTP 的方式访问到这些方法接下来,如果我们希望限制只用其中的一种方式来发布,则可以将
@Endpoint
替换为@JmxEndpoint
或@WebEndpoint
。如果是通过 HTTP 方式访问的,默认的 URL 是
/actuator/<id>
,其中的id
就是@Endpoint
注解中指定的id
,而@ReadOperation
、@WriteOperation
和@DeleteOperation
的方法分别对应了 HTTP 的GET
、POST
和DELETE
方法。HTTP 的响应码则取决于方法的返回值,如果存在返回内容,则响应码是200 OK
,否则@ReadOperation
方法会返回404 Not Found
,而另两个则返回204 No Content
;对于需要参数但又获取不到的情况,方法会返回400 Bad Request
。我们为 BinaryTea 编写了一个返回商店状态的端点,具体如代码示例 5-5 所示,大部分逻辑与
ShopReadyHealthIndicator
是类似的,这里就不再赘述了。代码示例 5-5
ShopEndpoint
代码片段@Component @Endpoint(id = "shop") public class ShopEndpoint { private BinaryTeaProperties binaryTeaProperties; public ShopEndpoint(ObjectProvider<BinaryTeaProperties> binaryTeaProperties) { this.binaryTeaProperties = binaryTeaProperties.getIfAvailable(); } @ReadOperation public String state() { if (binaryTeaProperties == null || !binaryTeaProperties.isReady()) { return "We're not ready."; } else { return "We open " + binaryTeaProperties.getOpenHours() + "."; } } }
为了能访问到我们的端点,需要在
application.properties
中允许它以 Web 形式发布:management.endpoints.web.exposure.include=health,info,shop
启动系统后,通过
http://localhost:8080/actuator/shop
即可访问ShopEndpoint
的输出。
5.2 基于 Micrometer 的系统度量
系统在生产环境中运行时,我们需要通过各种方式了解系统的运作是否正常。之前提到的 health
端点只能判断最基本的情况,至于更多细节,还需要获取详细的度量指标, metrics
端点就是用来提供系统度量信息的。
5.2.1 Micrometer 概述
从 2.0 版本开始,Spring Boot 就把 Micrometer 作为默认的系统度量指标获取途径了 8,因此本节也先从 Micrometer 讲起。
Java 开发者应该都很熟悉 SLF4J(Simple Logging Facade for Java),它提供了一套日志框架的抽象,屏蔽了底层不同日志框架(比如 Commons Logging、Log4j 和 Logback)实现上的差异,让开发者能以统一的方式在代码中打印日志。如果说 Micrometer 的目标就是成为度量界的 SLF4J,相信大家就能理解 Micrometer 是干什么的了。
Micrometer 为很多主流的监控系统提供了一套简单且强大的客户端门面,先是定义了一套 SPI(Service Provider Interface),再为不同的监控系统提供实现。接入 Micrometer 后,开发者通过 Micrometer 进行埋点时就不会被绑定在某个特定的监控系统上。它支持的监控系统,以及这些系统的特性,如表 5-7 所示。
表 5-7 Micrometer 支持的监控系统清单
监控系统 | 是否支持多维度 | 数据聚合方式 | 数据获取方式 |
---|---|---|---|
AppOptics | 是 | 客户端聚合 | 客户端推 |
Atlas | 是 | 客户端聚合 | 客户端推 |
Azure Monitor | 是 | 客户端聚合 | 客户端推 |
Cloudwatch | 是 | 客户端聚合 | 客户端推 |
Datadog | 是 | 客户端聚合 | 客户端推 |
Datadog StatsD | 是 | 客户端聚合 | 服务端拉 |
Dynatrace | 是 | 客户端聚合 | 客户端推 |
Elastic | 是 | 客户端聚合 | 客户端推 |
Etsy StatsD | 否 | 客户端聚合 | 服务端拉 |
Ganglia | 否 | 客户端聚合 | 客户端推 |
Graphite | 否 | 客户端聚合 | 客户端推 |
Humio | 是 | 客户端聚合 | 客户端推 |
Influx | 是 | 客户端聚合 | 客户端推 |
JMX | 否 | 客户端聚合 | 客户端推 |
KairosDB | 是 | 客户端聚合 | 客户端推 |
New Relic | 是 | 客户端聚合 | 客户端推 |
Prometheus | 是 | 服务端聚合 | 服务端拉 |
SignalFx | 是 | 客户端聚合 | 客户端推 |
Sysdig StatsD | 是 | 客户端聚合 | 服务端拉 |
Telegraf StatsD | 是 | 客户端聚合 | 服务端拉 |
Wavefront | 是 | 服务端聚合 | 客户端推 |
Micrometer 通过 Meter
接口来收集系统的度量数据,由 MeterRegistry
来创建并管理 Meter
,Micrometer 支持的各种监控系统都有自己的 MeterRegistry
实现。内置的 Meter
实现分为几种,具体如表 5-8 所示。
表 5-8 几种主要的 Meter
实现
Meter
类型
说明
Timer
计时器,用来记录一个事件的耗时
Counter
计数器,用来表示一个单调递增的值
Gauge
计量仪,用来表示一个变化的值,通常能用 Counter
就不用 Gauge
DistributionSummary
分布统计,用来记录事件的分布情况,可以设置一个范围,获取范围内的直方图和百分位数
LongTaskTimer
长任务计时器,记录一个长时间任务的耗时,可以记录已经耗费的时间
FunctionCounter
函数计数器,追踪某个单调递增函数的计数器
FunctionTimer
函数计时器,追踪两个单调递增函数,一个计数,另一个计时
要创建 Meter
,既可以通过 MeterRegistry
上的方法,例如 registry.timer("foo")
,也可以通过 Fluent 风格的构建方法,例如 Timer.builder("foo").tags("bar").register(registry)
。 Meter
的命名采用以 .
分隔的全小写单词组合,不同的监控系统功能有不同的命名方式,Micrometer 会负责将 Meter
的名称转为合适的方式,在官方文档中就给出了这样一个例子:
registry.timer("http.server.requests");
在使用 Prometheus 时,这个 Timer
的名字就会被转为 http_server_requests_duration_seconds
。
标签也遵循一样的命名方式,Micrometer 同样也会负责帮我们将 Timer
的标签转换成不同监控系统所推荐的名称。下面的标签名为 uri
,值为 /api/orders
:
registry.timer("http.server.requests", "uri", "/api/orders");
针对通用的标签,Micrometer 还贴心地提供了公共标签的功能,在 MeterRegistry
上设置标签:
registry.config().commonTags("prod", "region", "cn-shanghai-1");
5.2.2 常用度量指标
Spring Boot Actuator 中提供了 metrics
端点,通过 /actuator/metrics
我们可以获取系统的度量值。而且 Spring Boot 还内置了很多实用的指标,可以直接拿来使用。
首先介绍的是 Micrometer 本身支持的 JVM 相关指标,具体见表 5-9。
表 5-9 Micrometer 支持的 JVM 度量指标
度量指标
说明
ClassLoaderMetrics
收集加载和卸载的类信息
JvmMemoryMetrics
收集 JVM 内存利用情况
JvmGcMetrics
收集 JVM 的 GC 情况
ProcessorMetrics
收集 CPU 负载情况
JvmThreadMetrics
收集 JVM 中的线程情况
用下面的语句就能绑定一个 ClassLoaderMetrics
:
new ClassLoaderMetrics().bindTo(registry);
但在 Spring Boot Actuator 的帮助下,我们无须自己来绑定这些度量指标,Spring Boot 中 JvmMetricsAutoConfiguration
之类的自动配置类已经替我们做好了绑定的工作。此外,它还在此基础上提供了 Spring MVC、Spring WebFlux、HTTP 客户端和数据源等其他度量指标。
仍然以 5.1.4 节的 binarytea-endpoint 为例,我们在 application.properties
中做些修改,将 metrics
端点加入 Web 可访问的端点中:
management.endpoints.web.exposure.include=health,info,shop,metrics
启动程序后,通过 http://localhost:8080/actuator/metrics
可以看到类似下面这样的一个清单,其中列举的 names
就是具体的度量指标名称:
{
"names": [
"http.server.requests",
"jvm.buffer.count",
"jvm.buffer.memory.used",
"jvm.buffer.total.capacity",
"jvm.classes.loaded",
"jvm.classes.unloaded",
"jvm.gc.live.data.size",
"jvm.gc.max.data.size",
"jvm.gc.memory.allocated",
"jvm.gc.pause",
"jvm.memory.max",
"jvm.memory.used",
"jvm.threads.live",
"jvm.threads.peak",
"logback.events",
"process.cpu.usage",
"process.files.max",
"process.start.time",
"process.uptime",
"system.cpu.usage",
"system.load.average.1m",
"tomcat.sessions.active.current",
"tomcat.sessions.active.max",
"tomcat.sessions.rejected"
// 列表中省略了一些内容
]
}
在 URI 后增加具体的名称,例如, http://localhost:8080/actuator/metrics/jvm.classes.loaded
,就能查看具体的度量信息:
{
"availableTags": [
],
"baseUnit": "classes",
"description": "The number of classes that are currently loaded in the Java virtual machine",
"measurements": [
{
"statistic": "VALUE",
"value": 7232.0
}
],
"name": "jvm.classes.loaded"
}
-
Spring MVC
默认所有基于 Spring MVC 的 Web 请求都会被记录下来,通过
/actuator/metrics/http.server.requests
我们可以查看类似下面这样的输出,其中包含了大量的信息,比如用过的 HTTP 方法、访问过的地址、返回的 HTTP 响应码、请求的总次数和总耗时,以及最大单次耗时等:{ "availableTags": [ { "tag": "exception", "values": ["None"] }, { "tag": "method", "values": ["GET"] }, { "tag": "uri", "values": [ "/actuator/metrics/", "/actuator/metrics" ] }, { "tag": "outcome", "values": ["SUCCESS"] }, { "tag": "status", "values": ["200"] } ], "baseUnit": "seconds", "description": null, "measurements": [ { "statistic": "COUNT", "value": 5.0 }, { "statistic": "TOTAL_TIME", "value": 0.085699938 }, { "statistic": "MAX", "value": 0.042694752 } ], "name": "http.server.requests" }
management.metrics.web.server.request.autotime.enabled
默认为true
,它能自动统计所有的 Web 请求,如果我们将它设置为false
,则需要自己在类上或者方法上添加@Timed
来标记要统计的Controller
方法 10,@Timed
注解可以做些更精细的设置,例如,添加额外的标签,计算百分位数,等等:@Controller public class SampleController { @RequestMapping("/") @Timed(extraTags = { "region", "cn-shanghai-1" }, percentiles = 0.99) public String foo() {} }
-
HTTP 客户端
当使用
RestTemplate
和WebClient
访问 HTTP 服务时 11,Spring Boot Actuator 提供了针对 HTTP 客户端的度量指标,但这并不是自动的,还需要做一些配置它们才能生效。这里建议通过如下方式来创建RestTemplate
:@Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); }
使用
RestTemplateBuilder
来构建RestTemplate
时,会对其应用所有配置在 Spring 上下文里的RestTemplateCustomizer
,而其中就有与度量相关的MetricsRestTemplateCustomizer
。针对WebClient
也是一样的,可以用WebClient.Builder
。如果大家有通过
RestTemplate
访问过 HTTP 服务,访问/actuator/metrics/http.client.requests
后就能看到类似下面这样的输出:{ "availableTags": [ { "tag": "method", "values": [ "GET" ] }, { "tag": "clientName", "values": [ "localhost" ] }, { "tag": "uri", "values": [ "/actuator/metrics" ] }, { "tag": "outcome", "values": [ "SUCCESS" ] }, { "tag": "status", "values": [ "200" ] } ], "baseUnit": "seconds", "description": "Timer of RestTemplate operation", "measurements": [ { "statistic": "COUNT", "value": 1.0 }, { "statistic": "TOTAL_TIME", "value": 0.115555898 }, { "statistic": "MAX", "value": 0.115555898 } ], "name": "http.client.requests" }
-
数据源
对于那些使用了数据库的系统,了解数据源的具体情况是非常有必要的,例如,当前的连接池配置的大小是什么样的,有多少个连接处于活跃状态等。如果连接池经常被占满,导致业务代码无法获取连接,那么无论是让业务线程等待连接,还是等待超时后报错都可能影响业务。
只要在 Spring 上下文中存在
DataSource
12 Bean,Spring Boot Actuator 就会自动配置对应的度量指标,如果我们使用的是 HikariCP,还会有额外的信息。访问/actuator/metrics
后能看到下面这些显示:{ "names": [ "hikaricp.connections", "hikaricp.connections.acquire", "hikaricp.connections.active", "hikaricp.connections.creation", "hikaricp.connections.idle", "hikaricp.connections.max", "hikaricp.connections.min", "hikaricp.connections.pending", "hikaricp.connections.timeout", "hikaricp.connections.usage", "jdbc.connections.max", "jdbc.connections.min" // 省略其他无关的内容 ] }
而具体到
/actuator/metrics/hikaricp.connections
则是这样的:{ "availableTags": [ { "tag": "pool", "values": [ "HikariPool-1" ] } ], "baseUnit": null, "description": "Total connections", "measurements": [ { "statistic": "VALUE", "value": 10.0 } ], "name": "hikaricp.connections" }
通过这些度量指标,我们可以轻松地掌握系统中数据源的大概情况,在遇到问题时更好地进行决策。
5.2.3 自定义度量指标
Spring Boot Actuator 内置的度量指标可以帮助我们掌握系统的情况,但光了解系统是远远不够的,系统运行正常,但业务指标却一路下滑的情况并不少见,因此还需要针对各种业务做对应的数据埋点,通过业务指标我们也可以反过来进一步了解系统的情况。
有两种绑定 Meter
的方法,一般可以考虑使用后者:
- 注入 Spring 上下文中的
MeterRegistry
,通过它来绑定Meter
; - 让 Bean 实现
MeterBinder
,在其bindTo()
方法中绑定Meter
。
下面,让我们通过二进制奶茶店项目中的一个例子来了解一下如何自定义度量指标。
需求描述 门店开始营业后,我们要频繁关注经营情况,像订单总笔数、总金额、客单价等都是常见的经营指标,最好能有个地方可以让经营者方便地看到这些信息。
以 5.1.4 节的 binarytea-endpoint 作为基础,增加如表 5-10 所示的三个经营指标,用来记录自开始营业起的经营情况,相信是个经营者都关心自己的店是不是赚钱吧?大家可以在本书配套示例的 ch5/binarytea-metrics 目录中找到这个例子。
表 5-10 三个经营指标
指标名称
类型
含义
order.count
Counter
总订单数
order.amount.sum
Counter
总订单金额
order.amount.average
Gauge
客单价
除此之外,为了演示 DistributionSummary
的用法,我们还额外增加了一个 order.summary
的指标,可以输出 95 分位的订单情况 13。对应的 Meter
配置与绑定代码如代码示例 5-6 所示,其中绑定了四个 Meter
(为了方便演示,客单价使用了一个整数),还提供了一个新的下订单方法,用来改变各 Meter
的值。
代码示例 5-6
SalesMetrics
的代码片段
@Component
public class SalesMetrics implements MeterBinder {
private Counter orderCount;
private Counter totalAmount;
private DistributionSummary orderSummary;
private AtomicInteger averageAmount = new AtomicInteger();
@Override
public void bindTo(MeterRegistry registry) {
this.orderCount = registry.counter("order.count", "direction", "income");
this.totalAmount = registry.counter("order.amount.sum", "direction", "income");
this.orderSummary = registry.summary("order.summary", "direction", "income");
registry.gauge("order.amount.average", averageAmount);
}
public void makeNewOrder(int amount) {
orderCount.increment();
totalAmount.increment(amount);
orderSummary.record(amount);
averageAmount.set((int) orderSummary.mean());
}
}
由于现阶段还没有开发下单的客户端,我们可以在 BinaryTeaApplication
这个主程序中通过 2.4.2 节中介绍的定时任务来定时下单,如代码示例 5-7 所示,其中金额为 0 至 100 元内的随机整数 14,每隔 5 秒会下一单并打印日志。
代码示例 5-7 可以定时下单的主程序代码片段
@SpringBootApplication
@EnableScheduling
public class BinaryTeaApplication {
private static Logger logger = LoggerFactory.getLogger(BinaryTeaApplication.class);
private Random random = new Random();
@Autowired
private SalesMetrics salesMetrics;
public static void main(String[] args) {
SpringApplication.run(BinaryTeaApplication.class, args);
}
@Scheduled(fixedRate = 5000, initialDelay = 1000)
public void periodicallyMakeAnOrder() {
int amount = random.nextInt(100);
salesMetrics.makeNewOrder(amount);
logger.info("Make an order of RMB {} yuan.", amount);
}
}
此时,通过 /actuator/metrics
我们会看到多出了如下几个指标:
"names": [
"order.amount.average",
"order.amount.sum",
"order.count",
"order.summary",
// 省略其他内容
]
}
具体访问 /actuator/metrics/order.summary
则能看到类似下面这样的输出:
{
"availableTags": [
{ "tag": "direction", "values": [ "income" ] }
],
"baseUnit": null,
"description": null,
"measurements": [
{ "statistic": "COUNT", "value": 168.0 },
{ "statistic": "TOTAL", "value": 7890.0 },
{ "statistic": "MAX", "value": 95.0 }
],
"name": "order.summary"
}
Spring Boot Actuator 中可以对 Micrometer 的度量指标做很多定制,我们既可以按照 Micrometer 的官方做法用 MeterFilter
精确地进行调整,也可以简单地使用配置来做些常规的改动。
例如,可以像下面这样来设置公共标签,将 region
标签的值设置为 cn-shanghai-1
:
management.metrics.tags.region=cn-shanghai-1
针对每个 Meter
也有一些对应的属性,如表 5-11 所示,表中给出的是前缀,在其后面带上具体的度量指标名称后,即可有针对性地进行设置了,例如 management.metrics.enable.order.amount.average=false
。
表 5-11 部分针对单个 Meter
的属性前缀
属性前缀
说明
适用范围
management.metrics.enable
是否开启
全部
management.metrics.distribution.minimum-expected-value
分布统计时范围的最小值
Timer
与 DistributionSummary
management.metrics.distribution.maximum-expected-value
分布统计时范围的最大值
Timer
与 DistributionSummary
management.metrics.distribution.percentiles
分布统计时希望计算的百分位值
Timer
与 DistributionSummary
如果希望输出 95 分位的订单情况,可以像代码示例 5-8 那样修改 application.properties
文件。
代码示例 5-8 修改后的
application.properties
binarytea.ready=true
binarytea.open-hours=8:30-22:00
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=health,info,shop,metrics
management.metrics.distribution.percentiles.order.summary=0.95
配置后会多出一个 order.summary.percentile
的度量指标,具体的内容大致如下所示:
{
"availableTags": [
{ "tag": "phi", "values": [ "0.95" ] },
{ "tag": "direction", "values": [ "income" ] }
],
"baseUnit": null,
"description": null,
"measurements": [
{ "statistic": "VALUE", "value": 87 }
],
"name": "order.summary.percentile"
}
茶歇时间:性能分析时的 95 线与 99 线是什么含义
说到衡量一个接口的耗时怎么样,大家的第一反应大多是使用平均响应时间。的确,平均耗时能代表大部分情况下接口的表现。假设一个接口的最小耗时为 4 毫秒,平均耗时为 8 毫秒—看起来性能挺好的,但如果经常会有那么几个请求的时间超过 1000 毫秒,那我们是否应该继续去优化它呢?
答案是肯定的,对于这类长尾的请求,我们还需要去做进一步的分析,这其中必然隐藏着一些问题。在性能测试中,我们往往会更多地关注 TP95 或者 TP99(TP 是 Top Percentile 的缩写),也就是通常所说的 95 线和 99 线指标。在 100 个请求中,按耗时从小到大排序,第 95 个就是耗时的 95 线,95%的请求都能在这个时间内完成。
还有更苛刻的条件是要去分析 TP9999,也就是 99.99%的情况,这样才能确保绝大部分请求的耗时都达到要求。
5.2.4 度量值的输出
通过 /actuator/metrics
虽然可以看到每个度量值的情况,但我们无法一直盯着这个 URI 看输出。在实际生产环境中,我们需要一个更成熟的度量值输出和收集的方案。好在 Micrometer 和 Spring Boot 早已经考虑到了这些,为我们准备好了。
-
输出到日志
Micrometer 提供了几个基本的
MeterRegistry
,其中之一就是LoggingMeterRegistry
,它可以定时将系统中的各个度量指标输出到日志中。有了结构化的日志信息,就能通过 ELK(Elasticsearch、Logstash 和 Kibana)等方式将它们收集起来,并加以分析。即使不做后续处理,把这些日志放着,作为日后回溯的材料也是可以的。前一节的例子中,在
BinaryTeaApplication
里加上代码示例 5-9 的代码 15,用来定义一个组合的MeterRegistry
,其中添加了基础的SimpleMeterRegistry
和输出日志的LoggingMeterRegistry
。代码示例 5-9 组合多个
MeterRegistry
的定义@Bean public MeterRegistry customMeterRegistry() { CompositeMeterRegistry meterRegistry = new CompositeMeterRegistry(); meterRegistry.add(new SimpleMeterRegistry()); meterRegistry.add(new LoggingMeterRegistry()); return meterRegistry; }
运行后,过一段时间就能在控制台输出的日志中看到类似下面的内容:
2022-02-06 22:44:00.029 INFO 50342 --- [trics-publisher] i.m.c.i.logging.LoggingMeterRegistry : jvm.gc. pause throughput=0.033333/s mean=0.012s max=0.012s 2022-02-06 22:44:00.030 INFO 50342 --- [trics-publisher] i.m.c.i.logging.LoggingMeterRegistry : order. summary throughput=0.133333/s mean=40.125 max=84
在生产环境中使用时,我们可以调整日志的配置文件,将
LoggingMeterRegistry
输出的日志打印到单独的日志中,方便管理。 -
输出到 Prometheus
在介绍 Mircometer 时,我们提到过它支持多种不同的监控系统,将度量信息直接输出到监控系统也是一种常见做法。下面以 Prometheus 为例,介绍一下它在 Spring Boot 系统中该如何操作。
首先,需要在项目的 pom.xml 中添加 Micrometer 为 Prometheus 编写的
MeterRegistry
依赖,有了这个依赖,后面的事交给 Spring Boot 的自动配置即可:<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>
随后,在
application.properties
中放开对对应端点的控制,让prometheus
端点可以通过 Web 访问:management.endpoints.web.exposure.include=health,info,shop,metrics,prometheus
再次运行我们的程序,在浏览器中访问
/actuator/prometheus
就能看到一个文本输出,Prometheus 在经过适当配置后会读取其中的内容,可以看到其中的名称已经从 Micrometer 的以点分隔,变为了 Prometheus 的下划线分隔,这也是 Micrometer 实现的:# 省略了很多内容,以下仅为片段 jvm_memory_max_bytes 2.44105216E8 # HELP order_count_total # TYPE order_count_total counter order_count_total 7.0 # HELP order_summary_max # TYPE order_summary_max gauge order_summary_max 76.0 # HELP order_summary # TYPE order_summary summary order_summary 79.5 order_summary_count 7.0 order_summary_sum 347.0 # HELP tomcat_sessions_active_current_sessions # TYPE tomcat_sessions_active_current_sessions gauge tomcat_sessions_active_current_sessions 0.0
转载自:https://juejin.cn/post/7377546788020011048