使用Spring Boot构建系统监控层

20 服务监控:如何使用 Actuator 组件实现系统监控?

这一讲我们将介绍 Spring Boot 中一个非常有特色的主题------系统监控。

系统监控是 Spring Boot 中引入的一项全新功能,它对应用程序运行状态的管理非常有效。而 Spring Boot Actuator 组件主要通过一系列 HTTP 端点提供的系统监控功能来实现系统监控。因此,接下来我们将引入 Spring Boot Actuator 组件,介绍如何使用它进行系统监控,以及如何对 Actuator 端点进行扩展。

引入 Spring Boot Actuator 组件

在初始化 Spring Boot 系统监控功能之前,首先我们需要引入 Spring Boot Actuator 组件,具体操作为在 pom 中添加如下所示的 Maven 依赖:

xml 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

请注意,引入 Spring Boot Actuator 组件后,并不是所有的端点都对外暴露。例如,启动 customer-service 时,我们就可以在启动日志中发现如下所示内容:

复制代码
Exposing 2 endpoint(s) beneath base path '/actuator'

访问 http://localhost:8080/actuator 端点后,我们也会得到如下所示结果。

json 复制代码
{
     "_links":{
         "self":{
             "href":"http://localhost:8080/actuator",
             "templated":false
         },
         "health-path":{
             "href":"http://localhost:8080/actuator/health/{*path}",
             "templated":true
         },
         "health":{
             "href":"http://localhost:8080/actuator/health",
             "templated":false
         },
         "info":{
             "href":"http://localhost:8080/actuator/info",
             "templated":false
         }
     }
 }

这种结果就是 HATEOAS 风格的 HTTP 响应。如果我们想看到默认情况下看不到的所有端点,则需要在配置文件中添加如下所示的配置信息。

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: "*"  

重启应用后,我们就能获取到 Spring Boot Actuator 暴露的所有端点,如下代码所示:

json 复制代码
{
     "_links":{
         "self":{
             "href":"http://localhost:8080/actuator",
             "templated":false
         },
         "beans":{
             "href":"http://localhost:8080/actuator/beans",
             "templated":false
         },
         "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
         },
         "conditions":{
             "href":"http://localhost:8080/actuator/conditions",
             "templated":false
         },
         "configprops":{
             "href":"http://localhost:8080/actuator/configprops",
             "templated":false
         },
         "env":{
             "href":"http://localhost:8080/actuator/env",
             "templated":false
         },
         "env-toMatch":{
             "href":"http://localhost:8080/actuator/env/{toMatch}",
             "templated":true
         },
         "loggers":{
             "href":"http://localhost:8080/actuator/loggers",
             "templated":false
         },
         "loggers-name":{
             "href":"http://localhost:8080/actuator/loggers/{name}",
             "templated":true
         },
         "heapdump":{
             "href":"http://localhost:8080/actuator/heapdump",
             "templated":false
         },
         "threaddump":{
             "href":"http://localhost:8080/actuator/threaddump",
             "templated":false
         },
         "metrics-requiredMetricName":{
             "href":"http://localhost:8080/actuator/metrics/{requiredMetricName}",
             "templated":true
         },
         "metrics":{
             "href":"http://localhost:8080/actuator/metrics",
             "templated":false
         },
         "scheduledtasks":{
             "href":"http://localhost:8080/actuator/scheduledtasks",
             "templated":false
         },
         "mappings":{
             "href":"http://localhost:8080/actuator/mappings",
             "templated":false
         }
     }
 }

根据端点所起到的作用,我们把 Spring Boot Actuator 提供的原生端点分为如下三类。

  • 应用配置类: 主要用来获取应用程序中加载的应用配置、环境变量、自动化配置报告等配置类信息,它们与 Spring Boot 应用密切相关。
  • 度量指标类: 主要用来获取应用程序运行过程中用于监控的度量指标,比如内存信息、线程池信息、HTTP 请求统计等。
  • 操作控制类: 在原生端点中只提供了一个关闭应用的端点,即 /shutdown 端点。

根据 Spring Boot Actuator 默认提供的端点列表,我们将部分常见端点的类型、路径和描述梳理在如下表格中,仅供参考。

通过访问上表中的各个端点,我们就可以获取自己感兴趣的监控信息了。例如访问了http://localhost:8082/actuator/health端点,我们就可以得到如下所示的 account-service 基本状态。

json 复制代码
{
     "status":"UP"
 }

此时,我们看到这个健康状态信息非常简单。

那有没有什么办法可以获取更详细的状态信息呢?答案是:有,而且办法很简单,我们只需要在配置文件中添加如下所示的配置项即可。

yaml 复制代码
management: 
  endpoint:
    health:
      show-details: always

上述配置项指定了针对 health 端点需要显示它的详细信息。这时,如果我们重启 Spring Boot 应用程序,并重新访问 http://localhost:8082/actuator/health 端点,就可以获取如下所示的详细信息。

json 复制代码
{
     "status":"UP",
     "components":{
         "diskSpace":{
             "status":"UP",
             "details":{
                 "total":201649549312,
                 "free":3434250240,
                 "threshold":10485760
             }
         },
         "ping":{
             "status":"UP"
         }
     }
 }

如果 Spring Boot Actuator 默认提供的端点信息不能满足业务需求,我们可以对其进行修改和扩展。此时,常见实现方案有两种,一种是扩展现有的监控端点,另一种是自定义新的监控端点。这两种方案我们都会逐一介绍,不过这一讲先来关注如何在现有的监控端点上添加定制化功能。

扩展 Actuator 端点

前面我们介绍了 Spring Boot 默认暴露了日常开发中最常见的两个端点:Info 端点和 Health 端点。接下来,我们讨论下如何对这两个端点进行扩展。

扩展 Info 端点

Info 端点用于暴露 Spring Boot 应用的自身信息。在 Spring Boot 内部,它把这部分工作委托给了一系列 InfoContributor 对象,而 Info 端点会暴露所有 InfoContributor 对象所收集的各种信息。

在Spring Boot 中包含了很多自动配置的 InfoContributor 对象,常见的 InfoContributor 及其描述如下表所示:

以上表中的 EnvironmentInfoContributor 为例,通过在配置文件中添加格式以"info"作为前缀的配置段,我们就可以定义 Info 端点暴露的数据。添加完成后,我们将看到所有在"info"配置段下的属性都将被自动暴露。

比如你可以将如下所示配置信息添加到配置文件 application.yml 中:

yaml 复制代码
info:
	app:
	    encoding: UTF-8
	    java:
	        source: 1.8.0_31
	        target: 1.8.0_31

现在访问 Info 端点,我们就能得到如下的 Environment 信息。

json 复制代码
{
     "app":{
         "encoding":"UTF-8",
         "java":{
             "source":"1.8.0_31",
             "target":"1.8.0_31"
         }
     }
 }

同时,我们还可以在服务构建时扩展 Info 属性,而不是硬编码这些值。假设使用 Maven,我们就可以按照如下所示的配置重写前面的示例并得到同样的效果。

yaml 复制代码
info: 
	app:
	    encoding: @project.build.sourceEncoding@
	    java:
	      source: @java.version@
	      target: @java.version@

很多时候,Spring Boot 自身提供的 Info 端点并不能满足我们的业务需求,这就需要我们编写一个自定义的 InfoContributor 对象。

方法也很简单,我们直接实现 InfoContributor 接口的 contribute() 方法即可。例如,我们希望在 Info 端点中暴露该应用的构建时间,就可以采用如下所示的代码进行操作。

java 复制代码
@Component
public class CustomBuildInfoContributor implements InfoContributor {
	@Override
  	public void contribute(Builder builder) {
    	builder.withDetail("build",	Collections.singletonMap("timestamp", new Date())); 
  	}
}

重新构建应用并访问 Info 端口后,我们就能获取如下所示信息。

json 复制代码
{
     "app":{
         "encoding":"UTF-8",
         "java":{
             "source":"1.8.0_31",
             "target":"1.8.0_31"
         }
     },
     "build":{
         "timestamp":1604307503710
     }
 }

这里我们可以看到,CustomBuildInfoContributor 为 Info 端口新增了时间属性。

扩展 Health 端点

Health 端点用于检查正在运行的应用程序健康状态,而健康状态信息由 HealthIndicator 对象从 Spring 的 ApplicationContext 中获取。

和 Info 端点一样,Spring Boot 内部也提供了一系列 HealthIndicator 对象供我们实现定制化。在默认情况下,HealthAggregator 会根据 HealthIndicator 的有序列表对每个状态进行排序,从而得到最终的系统状态。

常见的 HealthIndicator 如下表所示:

HealthIndicator 名称 描述
DiskSpaceHealthIndicator 检查磁盘空间是否足够
DataSourceHealthIndicator 检查是否可以获得连接 DataSource
ElasticsearchHealthIndicator 检查 Elasticsearch 集群是否启动
JmsHealthIndicator 检查 JMS 代理是否启动
MailHealthIndicator 检查邮件服务器是否启动
MongoHealthIndicator 检查 Mongo 数据库是否启动
RabbitHealthIndicator 检查 RabbitMQ 服务器是否启动
RedisHealthIndicator 检查 Redis 服务器是否启动
SolrHealthIndicator 检查 Solr 服务器是否已启动

Health 端点信息的丰富程度取决于当下应用程序所处的环境,而一个真实的 Health 端点信息如下代码所示:

json 复制代码
{
     "status":"UP",
     "components":{
         "db":{
             "status":"UP",
             "details":{
                 "database":"MySQL",
                 "result":1,
                 "validationQuery":"/* ping */ SELECT 1"
             }
         },
         "diskSpace":{
             "status":"UP",
             "details":{
                 "total":201649549312,
                 "free":3491287040,
                 "threshold":10485760
             }
         },
         "ping":{
             "status":"UP"
         }
	}
}

通过以上这些信息,我们就可以判断该环境中是否包含了 MySQL 数据库。

现在,我们还想在 Health 端点中暴露 customer-service 当前运行时状态。

为了进一步明确该服务的状态,我们可以自定义一个 CustomerServiceHealthIndicator 端点专门展示 customer-service 的状态信息,CustomerServiceHealthIndicator 的定义如下所示:

java 复制代码
@Component
public class CustomerServiceHealthIndicator implements HealthIndicator {
	@Override
    public Health health() {
        try {
			URL url = new URL("http://localhost:8083/health/");
			HttpURLConnection conn = (HttpURLConnection) 
			url.openConnection();
            int statusCode = conn.getResponseCode();
            if (statusCode >= 200 && statusCode < 300) {
                return Health.up().build();
            } else {
                return Health.down().withDetail("HTTP Status Code", statusCode).build();
            }
        } catch (IOException e) {
            return Health.down(e).build();
        }
    }
}

我们需要提供 health() 方法的具体实现并返回一个 Health 结果。该 Health 结果应该包括一个状态,并且可以根据需要添加任何细节信息。

以上代码中,我们使用了一种简单且直接的方式判断配置中心服务"customerservice"是否正在运行。然后我们构建一个 HTTP 请求,并根据 HTTP 响应得出了健康诊断的结论。

如果 HTTP 响应的状态码处于 200~300 之间,我们认为该服务正在运行,此时,Health.up().build() 方法就会返回一种 Up 响应,如下代码所示:

json 复制代码
{
    "status": "UP",
    "details": {
        "customerservice":{
            "status": "UP"
        }
        ...
    }
}

如果状态码不处于这个区间(例如返回 404,代表服务不可用),Health.down().withDetail().build() 方法就会返回一个 Down 响应,并给出具体的状态码,如下代码所示:

json 复制代码
{
    "status": "DOWN",
    "details": {
        "customerservice":{
            "status": "DOWN",
            "details": {
                "HTTP Status Code": "404"
            }
        },
        ...
    }
}

如果 HTTP 请求直接抛出了异常,Health.down().build() 方法同样会返回一个 Down 响应,并返回异常信息,效果如下代码所示:

json 复制代码
{
    "status": "DOWN",
    "details": {
        "customerservice":{
            "status": "DOWN",
            "details": {
                "error": "java.net.ConnectException: Connection refused: connect"
            }
        },
        ...
    }
}

显然,通过扩展 Health 端点为我们实时监控系统中各个服务的正常运行状态提供了很好的支持,我们也可以根据需要构建一系列有用的 HealthIndicator 实现类,并添加报警等监控手段。

小结与预告

Spring Boot 内置的 Actuator 组件使得开发人员在管理应用程序运行的状态有了更加直接且高效的手段。

这一讲,我们引入了 Actuator 组件并介绍了该组件提供的一系列核心端点,同时重点分析了 Info 和 Health 这两个基础端点,并给出了对它们进行扩展的系统方法。

系统监控的一大目标是收集和分析系统运行时的度量指标,并基于这些指标判断当前的运行时状态,因此,21 讲我们将讨论如何在系统中嵌入自定义度量指标的实现技巧。

21 指标定制:如何实现自定义度量指标和 Actuator 端点?

20 讲中我们引入了 Spring Boot Actuator 组件来满足 Spring Boot 应用程序的系统监控功能,并重点介绍了如何扩展常见的 Info 和 Health 监控端点的实现方法。

这一讲我们继续讨论如何扩展 Actuator 端点,但更多关注与度量指标相关的内容。同时,我们还将给出如何创建自定义 Actuator 的实现方法,以便应对默认端点无法满足需求的应用场景。

Actuator 中的度量指标

对于系统监控而言,度量是一个很重要的维度。 在 Spring Boot 2.X 版本中,Actuator 组件主要使用内置的 Micrometer 库实现度量指标的收集和分析。

Micrometer 度量库

Micrometer 是一款监控指标的度量类库,为 Java 平台上的性能数据收集提供了一套通用的 API。在应用程序中,我们只使用 Micrometer 提供的通用 API 即可收集度量指标。

下面我们先来简要介绍 Micrometer 中包含的几个核心概念。

首先我们需要介绍的是计量器 Meter,它是一个接口,代表的是需要收集的性能指标数据。关于 Meter 的定义如下:

java 复制代码
public interface Meter extends AutoCloseable {
    //Meter 的唯一标识,是名称和标签的一种组合
    Id getId();

    //一组测量结果
	Iterable<Measurement> measure();

	//Meter 的类型枚举值
    enum Type {
        COUNTER,
        GAUGE,
        LONG_TASK_TIMER,
        TIMER,
        DISTRIBUTION_SUMMARY,
        OTHER
    }
}

通过上述代码,我们注意到 Meter 中存在一个 Id 对象,该对象的作用是定义 Meter 的名称和标签。从 Type 的枚举值中,我们不难看出 Micrometer 中包含的所有计量器类型。

接下来我们先说明两个概念。

Meter 的名称 :对于计量器来说,每个计量器都有自己的名称,而且在创建时它们都可以指定一系列标签。 Meter 的标签:标签的作用在于监控系统可以通过这些标签对度量进行分类过滤。

在日常开发过程中,常用的计量器类型主要分为计数器 Counter、计量仪 Gauge 和计时器 Timer 这三种。

  • Counter:这个计量器的作用和它的名称一样,就是一个不断递增的累加器,我们可以通过它的 increment 方法实现累加逻辑。
  • Gauge:与 Counter 不同,Gauge 所度量的值并不一定是累加的,我们可以通过它的 gauge 方法指定数值。
  • Timer:这个计量器比较简单,就是用来记录事件的持续时间。

既然我们已经明确了常用的计量器及其使用场景,那么如何创建这些计量器呢?

在 Micrometer 中,我们提供了一个计量器注册表 MeterRegistry,它主要负责创建和维护各种计量器。关于 MeterRegistry 的创建方法如下代码所示:

java 复制代码
public abstract class MeterRegistry implements AutoCloseable {
    protected abstract <T> Gauge newGauge(Meter.Id id, @Nullable T obj, ToDoubleFunction<T> valueFunction);
    protected abstract Counter newCounter(Meter.Id id);
    protected abstract Timer newTimer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig, PauseDetector pauseDetector);
	...
}

以上代码只是创建 Meter 的一种途径,从中我们可以看到 MeterRegistry 针对不同的 Meter 提供了对应的创建方法。

而创建 Meter 的另一种途径是使用某个 Meter 的具体 builder 方法。以 Counter 为例,它的定义中包含了一个 builder 方法和一个 register 方法,如下代码所示:

java 复制代码
public interface Counter extends Meter {
    static Builder builder(String name) {
        return new Builder(name);
	}

    default void increment() {
        increment(1.0);
	}

	void increment(double amount);

	double count();

    @Override
    default Iterable<Measurement> measure() {
        return Collections.singletonList(new Measurement(this::count, Statistic.COUNT));
	}

    ...

    public Counter register(MeterRegistry registry) {
        return registry.counter(new Meter.Id(name, tags, baseUnit, description, Type.COUNTER));
    }
}

注意到最后的 register 方法就是将当前的 Counter 注册到 MeterRegistry 中,因此我们需要创建一个 Counter。通常,我们会采用如下所示代码进行创建。

java 复制代码
Counter counter = Counter.builder("counter1")
        .tag("tag1", "value1")
        .register(registry);

了解了 Micrometer 框架的基本概念后,接下来我们回到 Spring Boot Actuator,一起来看看它提供的专门针对度量指标管理的 Metrics 端点。

扩展 Metrics 端点

在 Spring Boot 中,它为我们提供了一个 Metrics 端点用于实现生产级的度量工具。访问 actuator/metrics 端点后,我们将得到如下所示的一系列度量指标。

json 复制代码
{
     "names":[
         "jvm.memory.max",
         "jvm.threads.states",
         "jdbc.connections.active",
         "jvm.gc.memory.promoted",
         "jvm.memory.used",
         "jvm.gc.max.data.size",
         "jdbc.connections.max",
         "jdbc.connections.min",
         "jvm.memory.committed",
         "system.cpu.count",
         "logback.events",
         "http.server.requests",
         "jvm.buffer.memory.used",
         "tomcat.sessions.created",
         "jvm.threads.daemon",
         "system.cpu.usage",
         "jvm.gc.memory.allocated",
         "hikaricp.connections.idle",
         "hikaricp.connections.pending",
         "jdbc.connections.idle",
         "tomcat.sessions.expired",
         "hikaricp.connections",
         "jvm.threads.live",
         "jvm.threads.peak",
         "hikaricp.connections.active",
         "hikaricp.connections.creation",
         "process.uptime",
         "tomcat.sessions.rejected",
         "process.cpu.usage",
         "jvm.classes.loaded",
         "hikaricp.connections.max",
         "hikaricp.connections.min",
         "jvm.gc.pause",
         "jvm.classes.unloaded",
         "tomcat.sessions.active.current",
         "tomcat.sessions.alive.max",
         "jvm.gc.live.data.size",
         "hikaricp.connections.usage",
         "hikaricp.connections.timeout",
         "jvm.buffer.count",
         "jvm.buffer.total.capacity",
         "tomcat.sessions.active.max",
         "hikaricp.connections.acquire",
         "process.start.time"
     ]
 }

以上代码中涉及的指标包括常规的系统内存总量、空闲内存数量、处理器数量、系统正常运行时间、堆信息等,也包含我们引入 JDBC 和 HikariCP 数据源组件之后的数据库连接信息等。此时,如果我们想了解某项指标的详细信息,在 actuator/metrics 端点后添加对应指标的名称即可。

例如我们想了解当前内存的使用情况,就可以通过 actuator/metrics/jvm.memory.used 端点进行获取,如下代码所示。

json 复制代码
{
     "name":"jvm.memory.used",
     "description":"The amount of used memory",
     "baseUnit":"bytes",
     "measurements":[
         {
             "statistic":"VALUE",
             "value":115520544
         }
     ],
     "availableTags":[
         {
             "tag":"area",
             "values":[
                 "heap",
                 "nonheap"
             ]
         },
         {
             "tag":"id",
             "values":[
                 "Compressed Class Space",
                 "PS Survivor Space",
                 "PS Old Gen",
                 "Metaspace",
                 "PS Eden Space",
                 "Code Cache"
             ]
         }
     ]
 }

前面介绍 Micrometer 时,我们已经提到 Metrics 指标体系中包含支持 Counter 和 Gauge 这两种级别的度量指标。通过将 CounterGauge注入业务代码中,我们就可以记录自己想要的度量指标。其中,Counter 用来暴露 increment() 方法,而 Gauge 用来提供一个 value() 方法。

下面我们以 Counter 为例介绍在业务代码中嵌入自定义 Metrics 指标的方法,如下代码所示:

json 复制代码
@Component
public class CounterService {
    public CounterService() {
        Metrics.addRegistry(new SimpleMeterRegistry());
    }

    public void counter(String name, String... tags) {
        Counter counter = Metrics.counter(name, tags);
        counter.increment();
    }
}

在这段代码中,我们构建了一个公共服务 CounterService,并开放了一个 Counter 方法供业务系统进行使用。当然,你也可以自己实现类似的工具类完成对各种计量器的封装。

另外,Micrometer 还提供了一个 MeterRegistry 工具类供我们创建度量指标。因此,我们也十分推荐使用 MeterRegistry 对各种自定义度量指标的创建过程进行简化。

使用 MeterRegistry

再次回到 SpringCSS 案例,此次我们来到 customer-service 的 CustomerTicketService 中。

比如我们希望系统每创建一个客服工单,就对所创建的工单进行计数,并作为系统运行时的一项度量指标,该效果的实现方式如下代码所示:

json 复制代码
@Service
public class CustomerTicketService {
    @Autowired
    private MeterRegistry meterRegistry;

	public CustomerTicket generateCustomerTicket(Long accountId, String orderNumber) {
    	CustomerTicket customerTicket = new CustomerTicket();

    	...

     	meterRegistry.summary("customerTickets.generated.count").record(1);
        	return customerTicket;
    	}   
}

在上述 generateCustomerTicket 方法中,通过 MeterRegistry 我们实现了每次创建 CustomerTicket 时自动添加一个计数的功能。

而且,MeterRegistry 还提供了一些类工具方法用于创建自定义度量指标。这些类工具方法除了常规的 counter、gauge、timer 等对应具体 Meter 的工具方法之外,还包括上述代码中的 summary 方法,且 Summary 方法返回的是一个 DistributionSummary 对象,关于它的定义如下代码所示:

java 复制代码
public interface DistributionSummary extends Meter, HistogramSupport {
    static Builder builder(String name) {
        return new Builder(name);
    }

    //记录数据
    void record(double amount);
    
    //记录操作执行的次数
    long count();

    //记录数据的数量
    double totalAmount();

    //记录数据的平均值
    default double mean() {
        return count() == 0 ? 0 : totalAmount() / count();
    }

    //记录数据的最大值
	double max();
	...
}

因为 DistributionSummary 的作用是记录一系列的事件并对这些事件进行处理,所以在 CustomerTicketService 中添加的meterRegistry.summary("customertickets.generated.count").record(1) 这行代码相当于每次调用 generateCustomerTicket 方法时,我们都会对这次调用进行记录。

现在访问 actuator/metrics/customertickets.generated.count 端点,我们就能看到如下所示的随着服务调用不断递增的度量信息。

json 复制代码
{
     "name":"customertickets.generated.count",
     "measurements":[
         {
             "statistic":"Count",
             "value":1
         },
         {
             "statistic":"Total",
             "value":19
         }
     ] 
 }

显然,通过 MeterRegistry 实现自定义度量指标的使用方法更加简单。这里,你也可以结合业务需求尝试该类的不同功能。

接下来我们再来看一个相对比较复杂的使用方式。在 customer-service 中,我们同样希望系统存在一个度量值,该度量值用于记录所有新增的 CustomerTicket 个数,这次的示例代码如下所示:

java 复制代码
@Component
public class CustomerTicketMetrics extends AbstractRepositoryEventListener<CustomerTicket> {
    private MeterRegistry meterRegistry;

    public CustomerTicketMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Override
    protected void onAfterCreate(CustomerTicket customerTicket) {                 meterRegistry.counter("customerTicket.created.count").increment();  
	}
}

首先,这里我们使用了 MeterRegistry 的 Counter 方法初始化一个 counter,然后调用它的 increment 方法增加度量计数(这部分内容我们已经很熟悉了)。

注意到这里,我们同时还引入了一个 AbstractRepositoryEventListener 抽象类,这个抽象类能够监控 Spring Data 中 Repository 层操作所触发的事件 RepositoryEvent,例如实体创建前后的 BeforeCreateEvent 和 AfterCreateEvent 事件、实体保存前后的 BeforeSaveEvent 和 AfterSaveEvent 事件等。

针对这些事件,AbstractRepositoryEventListener 能捕捉并调用对应的回调函数。关于 AbstractRepositoryEventListener 类的部分实现如下代码所示:

java 复制代码
public abstract class AbstractRepositoryEventListener<T> implements ApplicationListener<RepositoryEvent> {
    public final void onApplicationEvent(RepositoryEvent event) {
       	...

        Class<?> srcType = event.getSource().getClass();

        if (event instanceof BeforeSaveEvent) {
            onBeforeSave((T) event.getSource());
        } else if (event instanceof BeforeCreateEvent) {
            onBeforeCreate((T) event.getSource());
        } else if (event instanceof AfterCreateEvent) {
            onAfterCreate((T) event.getSource());
        } else if (event instanceof AfterSaveEvent) {
            onAfterSave((T) event.getSource());
        }
        ...
	}
}

在这段代码中,我们可以看到 AbstractRepositoryEventListener 直接实现了 Spring 容器中的 ApplicationListener 监听器接口,并在 onApplicationEvent 方法中根据所传入的事件类型触发了回调函数。

以案例中的需求场景为例,我们可以在创建 Account 实体之后执行度量操作。也就是说,可以把度量操作的代码放在 onAfterCreate 回调函数中,正如案例代码中所展示那样。

现在我们执行生成客户工单操作,并访问对应的 Actuator 端点,同样可以看到度量数据在不断上升。

自定义 Actuator 端点

在日常开发过程中,扩展现有端点有时并不一定能满足业务需求,而自定义 Spring Boot Actuator 监控端点算是一种更灵活的方法。

假设我们需要提供一个监控端点以获取当前系统的用户信息和计算机名称,就可以通过一个独立的 MySystemEndPoint 进行实现,如下代码所示:

java 复制代码
@Configuration
@Endpoint(id = "mysystem", enableByDefault=true)
public class MySystemEndpoint { 
    @ReadOperation
    public Map<String, Object> getMySystemInfo() {
        Map<String,Object> result= new HashMap<>();
        Map<String, String> map = System.getenv();
        result.put("username",map.get("USERNAME"));
        result.put("computername",map.get("COMPUTERNAME"));
        return result;
    }
}

在这段代码中我们可以看到,MySystemEndpoint 主要通过系统环境变量获取所需监控信息。

注意,这里我们引入了一个新的注解 @Endpoint,该注解定义如下代码所示:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Endpoint {
    //端点 id
    String id() default "";
    
    //是否默认启动标志位
    boolean enableByDefault() default true;
}

这段代码中的 @Endpoint 注解主要用于设置端点 id 及是否默认启动的标志位。且在案例中,我们指定了 id 为"mysystem",enableByDefault 标志为 true。

事实上,在 Actuator 中也存在一批类似 @Endpoint 的端点注解。其中被 @Endpoint 注解的端点可以通过 JMX 和 Web 访问应用程序,对应的被 @JmxEndpoint 注解的端点只能通过 JMX 访问,而被 @WebEndpoint 注解的端点只能通过 Web 访问。

在示例代码中,我们还看到了一个 @ReadOperation 注解,该注解作用于方法,用于标识读取数据操作。在 Actuator 中,除了提供 @ReadOperation 注解之外,还提供 @WriteOperation 和 @DeleteOperation 注解,它们分别对应写入操作和删除操作。

现在,通过访问 http://localhost:8080/actuator/mysystem,我们就能获取如下所示监控信息。

json 复制代码
{
     "computername":"LAPTOP-EQB59J5P",
     "username":"user"
 }

有时为了获取特定的度量信息,我们需要对某个端点传递参数,而 Actuator 专门提供了一个 @Selector 注解标识输入参数,示例代码如下所示:

java 复制代码
@Configuration
@Endpoint(id = "account", enableByDefault = true)
public class AccountEndpoint {
    @Autowired
    private AccountRepository accountRepository;    

    @ReadOperation
    public Map<String, Object> getMySystemInfo(@Selector String arg0) {
        Map<String, Object> result = new HashMap<>();
        result.put(accountName, accountRepository.findAccountByAccountName(arg0));
        return result;
    }
}

这段代码的逻辑非常简单,就是根据传入的 accountName 获取用户账户信息。

这里请注意,通过 @Selector 注解,我们就可以使用 http://localhost:8080/**actuator/ account/account1 这样的端口地址触发度量操作了。**

小结与预告

度量是我们观测一个应用程序运行时状态的核心手段。这一讲我们介绍了 Spring Boot 中新引入的 Micrometer 度量库,以及该库中提供的各种度量组件。同时,我们还基于 Micrometer 中的核心工具类 MeterRegistry 完成了在业务系统中嵌入度量指标的实现过程。最后,我们还简要介绍了如何自定义一个 Actuator 端点的开发方法。

22 运行管理:如何使用 Admin Server 管理 Spring 应用程序?

前面 2 讲通过引入 Actuator 组件,我们为 Spring Boot 应用程序添加了系统监控功能。基于 Actuator 暴露的各种 HTTP 端点,开发人员可以获取系统的运行时状态。而端点是一种底层的监控技术,这就要求我们对 HTTP 协议和 Spring Boot 应用程序的构建方式有一定的了解。

那么,有没有更简单的、基于可视化的方式获取这些端点背后的信息呢?答案是肯定的。因此,这一讲我们将要介绍 Spring Boot Admin 组件。

引入 Spring Boot Admin 组件

Spring Boot Admin 是一个用于监控 Spring Boot 的应用程序,它的基本原理是通过统计、集成 Spring Boot Actuator 中提供的各种 HTTP 端点,从而提供简洁的可视化 WEB UI,如下图所示:

Spring Boot Admin 基本原理图

从上图中,我们不难看出,Spring Boot Admin 的整体架构中存在两大角色,即服务器端组件 Admin Server 和客户端组件 Admin Client。其中,Admin Client 实际上是一个普通的 Spring Boot 应用程序,而 Admin Server 则是一个独立服务,需要进行专门构建。

接下来,我们先介绍构建 Admin Server 的两种实现方式:一种是简单的基于独立的 Admin 服务;另一种则相对复杂,需要依赖服务注册中心的服务注册和发现机制。

基于独立服务构建 Admin Server

无论使用哪种方式实现 Admin Server,首先我们都需要创建一个 Spring Boot 应用程序,并在 pom 文件中添加如下所示的依赖项:

xml 复制代码
<dependency>
     <groupId>de.codecentric</groupId>
     <artifactId>spring-boot-admin-server</artifactId>
</dependency>

<dependency>
      <groupId>de.codecentric</groupId>
      <artifactId>spring-boot-admin-server-ui</artifactId>
</dependency>

请注意: Spring Boot Admin 组件并不是 Spring 家族官方提供的组件,而是来自一个 codecentric AG 团队。

如果我们想将普通的 Spring Boot 应用程序转变为 Spring Boot Admin Server,只需要在 Bootstrap 类上添加一个 @EnableAdminServer 注解即可,添加完该注解的 BootStrap 类如下代码所示:

java 复制代码
@SpringBootApplication
@EnableAdminServer
public class AdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(AdminApplication.class, args);
    }
}

此时,我们会发现使用这种方式构建 Spring Boot Admin Server 就是这么简单。

接下来我们启动这个 Spring Boot 应用程序,并打开 Web 界面,就能看到如下所示的效果:

Spring Boot Admin Server 启动效果图

从图中我们可以看到,目前还没有一个应用程序与 Admin Server 有关联。如果想将应用程序与 Admin Server 进行关联,我们还需要对原有的 Spring Boot 应用程序做一定的改造。

首先,我们在 Maven 依赖中引入对 Spring Boot Admin Client 组件的依赖,如下代码所示:

xml 复制代码
<dependency>
     <groupId>de.codecentric</groupId>
     <artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>

然后,我们在配置文件中添加如下配置信息,以便该应用程序能够与 Admin Server 进行关联。

yaml 复制代码
spring:
  boot:
    admin:
      client:
        url: http://localhost:9000

注意:这里的 9000 就是 Admin Server 的服务器端口。

现在我们启动这个应用程序,就会发现 Admin Server 中已经出现了这个应用的名称和地址,如下图所示:

Spring Boot Admin Server 添加了应用程序之后的效果图

在图中,我们看到 APPLICATIONS 和 INSTANCES 的数量都是 1,代表 Admin Server 管理着一个应用程序,而该应用程序只有一个运行实例。在界面的下方,我们还能看到这个应用的名称及实例地址。这里你可以尝试使用不同的端口启动应用程序的不同实例,然后观察这个列表的变化。

基于注册中心构建 Admin Server

虽然基于独立服务构建 Admin Server 和 Admin Client 非常简单,但是需要我们在每个应用程序中添加对 Spring Boot Admin 的 Maven 依赖,并指定 Admin Server 地址。这实际上是一种代码侵入,意味着应用程序与 Admin Server 之间有一种强耦合。

那么,有没有更好的办法分离或转嫁这种耦合呢?

联想到 Admin Server 和 Admin Client 之间需要建立类似服务注册的关联关系,我们可以认为这是服务注册和发现机制的一种表现形式。

在 Spring 家族中,存在一个用于构建微服务架构的 Spring Cloud 框架,而该框架中恰好存在一款专门实现服务注册和发现的组件------服务注册中心 Spring Cloud Netflix Eureka ,且 Spring Boot Admin 内置了与这款注册中心实现工具的无缝集成。

基于注册中心,Admin Server 与各个 Admin Client 之间的交互方式如下图所示:

基于 Eureka 的 Admin Server 与 Admin Client 交互图

使用 Eureka 构建注册中心的过程也很简单,首先我们创建一个独立的 Spring Boot 应用程序,并在 pom 文件中添加如下所示的用于提供 Eureka 服务端功能的 Maven 依赖:

xml 复制代码
<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

引入 Maven 依赖后,我们就可以创建 Spring Boot 的启动类。在示例代码中,我们把该启动类命名为 EurekaServerApplication,如下代码所示:

java 复制代码
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
	public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

注意:在上面的代码中,我们在启动类上加了一个@EnableEurekaServer 注解。在 SpringCloud 中,包含 @EnableEurekaServer 注解的服务也就是一个 Eureka 服务器组件。这样,Eureka 服务就构建完毕了。

同样,Eureka 服务还为我们提供了一个可视化的 UI 界面,它可以用来观察当前注册到 Eureka 中的应用程序信息,如下图所示:

Eureka 服务监控页面

接下来,我们需要 Admin Server 也做相应调整。首先,我们在 pom 文件中添加一个对 spring-cloud-starter-netflix-eureka-client 这个 Eureka 客户端组件的依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

这时 Admin Server 相当于 Eureka 的客户端,因此,我们需要在它的 BootStrap 类上添加 @EnableEurekaClient 注解,以便将 Admin Server 注册到 Eureka 上。

重构 Admin Server 的最后一步是调整配置信息,此时我们需要在配置文件中添加如下所示的配置项来指定 Eureka 服务器地址。

yaml 复制代码
eureka:
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
	  defaultZone: http://localhost:8761/eureka/

好了,现在 Admin Server 已经重构完毕,接下来我们一起看看 Admin Client。

引入注册中心的目的是降低 Admin Client 与 Admin Server 之间的耦合度,关于这点我们从 Maven 依赖上就可以得到印证。有了注册中心后,Admin Client 就不再依赖 spring-boot-admin-starter-client 组件了,而是直接使用如下所示的 Eureka 客户端组件。

xml 复制代码
<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

在配置文件中,我们需要去掉对 Admin Server 地址的引用,直接使用 Eureka 服务端地址即可,且无须对 Admin Client 中的 Bootstrap 类做任何修改。

通过以上调整,各个 Admin Client 就能通过 Eureka 注册中心完成与 Admin Server 的关联了。

使用 Admin Server 监控系统

根据 Spring Boot Admin 官方 Github 上的介绍,Admin Server 监控系统提供了一套完整的可视化方案。基于 Admin Server,健康状态、JVM、内存、Micrometer 的度量、线程、HTTP 跟踪等核心功能都可以通过可视化的 UI 界面进行展示。

监控系统运行时关键指标

注意到 Admin Server 菜单中有一个"Wallboard",点击该菜单,我们就可以看到一面应用墙,如下图所示:

Admin Server 应用墙

点击应用墙中的某个应用,我们就能进入针对该应用的监控信息主界面。在该界面的左侧,包含了监控功能的各级目录,如下图所示:

Admin Server 监控信息主界面

在图中,我们看到了最重要的"Health"信息,显然,这一信息来自 Spring Boot Actuator 组件的 Health 端点,这里你可以参考《服务监控:如何使用 Actuator 组件实现系统监控?》的内容进行回顾。

在这个界面上继续往下滑动,我们将看到一些与 JVM 相关的监控信息,比如非常有用的线程、垃圾回收、内存状态等数据,如下图所示:

Admin Server 中的 JVM 监控信息

这些 JVM 数据都是通过可视化的方式进行展现,并随着运行时状态的变化而实时更新。

在 21 讲中,我们详细讨论了 Spring Boot Actuator 中的度量指标。而在 Admin Server 中,同样存在一个"Metrics"菜单,展示效果如下图所示:

Admin Server 中的 Metrics 信息

在"Metrics"菜单中,开发人员可以通过对各种条件进行筛选,然后添加对应的度量指标。比如上图中,我们针对 HTTP 请求中 /actuator/health 端点进行了过滤,从而得到了度量结果。

接着我们一起看看系统环境方面的属性,因为这方面的属性非常之多,所以 Admin Server 也提供了一个过滤器,如下图所示:

Admin Server 中的 Environment 信息

在上图中,通过输入"spring."参数,我们就能获取一系列与该参数相关的环境属性。

日志也是我们监控系统的一个重要途径,在 Admin Server 的"Loggers"菜单中,可以看到该应用程序的所有日志信息,如下图所示:

Admin Server 中的 Loggers 信息

通过"springcss"关键词对这些日志进行过滤,我们就可以获取 SpringCSS 案例中的日志详细了,图中也显示了每个日志记录器对应的日志级别。

最后,我们来看一下 Admin Server 中的"JVM"菜单,该菜单下存在两个子菜单:"Thread Dump"和"Heap Dump"。

以"Thread Dump"为例,尽管 Actuator 提供了 /threaddump 端点,但开发人员只能获取触发该端点时的 Dump 信息,而 Admin Server 则提供了一个连续性的可视化监控界面,如下图所示:

Admin Server 中的 Thread Dump 信息

点击图中的色条,我们就可以获取每一个线程的详细信息了,这里你可以尝试做一些分析。

控制访问安全性

讲到这里,我们会发现 Admin Server 的功能非常强大,而这些功能显然也不应该暴露给所有的开发人员。因此,我们需要控制 Admin Server 的访问安全性。

想做到这一点也非常简单,我们只需要集成 Spring Security 即可。

结合《用户认证:如何基于 Spring Security 构建用户认证体系?》的内容,我们在 Spring Boot 应用程序中添加一个对 spring-boot-starter-security 的 Maven 依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

然后,我们在配置文件中添加如下配置项:

yaml 复制代码
spring:
  security:
    user:
      name: "springcss_admin"
      password: "springcss_password"

重启 Admin Server 后,再次访问 Web 界面时,就需要我们输入用户名和密码了,如下图所示:

Admin Server 的安全登录界面

小结与预告

可视化监控一直是开发和运维人员管理应用程序运行时状态的基础诉求,而 Spring Boot Admin 组件正是这样一款可视化的工具。它基于 Spring Boot Actuator 中各个端点所暴露的监控信息,并加以整合和集成。今天的内容首先介绍了构建 Admin Server 以及 Admin Client 的方法,并剖析了 Admin Server 中所具有的一整套的可视化解决方案。

相关推荐
凤山老林17 小时前
SpringBoot 启动时执行某些操作的 8 种方式
java·开发语言·spring boot·后端
PHP源码18 小时前
SpringBoot房屋租赁系统
spring boot·springboot房屋租赁·java房屋租赁系统·layui房屋租赁·springboot租房系统·java租房系统
zl97989918 小时前
SpringBoot-常用注解
java·spring boot·spring
舒克日记21 小时前
基于springboot的民谣网站的设计与实现
java·spring boot·后端
lang201509281 天前
Spring Boot配置属性:类型安全的最佳实践
spring boot
Jabes.yang1 天前
Java面试场景:从Spring Web到Kafka的音视频应用挑战
大数据·spring boot·kafka·spring security·java面试·spring webflux
程序员小凯1 天前
Spring Boot性能优化详解
spring boot·后端·性能优化
tuine1 天前
SpringBoot使用LocalDate接收参数解析问题
java·spring boot·后端