1. 前言 这里我们将介绍 Spring Boot 中一个非常有特色的主题,系统监控。
系统监控是 Spring Boot 中引入的一项全新功能,它对应用程序运行状态的管理非常有效。而 Spring Boot Actuator 组件主要通过一系列 HTTP 端点提供的系统监控功能来实现系统监控。
因此,接下来我们将引入 Spring Boot Actuator 组件,以支付系统为例来介绍如何使用它进行系统监控,以及如何对 Actuator 端点进行扩展。并结合 Prometheus 、Grafana 来更加直观的展示这些信息。
2. 引入 Spring Boot Actuator 在初始化 Spring Boot 系统监控功能之前,首先我们需要引入 Spring Boot Actuator 组件,具体操作为在 pom 中添加如下所示的 Maven 依赖:
1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
访问 http://localhost:8080/actuator 端点后,我们也会得到如下所示结果。
这种结果就是 HATEOAS 风格的 HTTP 响应。如果我们想看到默认情况下看不到的所有端点,则需要在配置文件中添加如下所示的配置信息。
1 2 3 4 5 management: endpoints: web: exposure: include: "*"
2.1 原生端点 根据端点所起到的作用,我们把 Spring Boot Actuator 提供的原生端点分为如下三类。
应用配置类 : 主要用来获取应用程序中加载的应用配置、环境变量、自动化配置报告等配置类信息,它们与 Spring Boot 应用密切相关。度量指标类 : 主要用来获取应用程序运行过程中用于监控的度量指标,比如内存信息、线程池信息、HTTP 请求统计等。操作控制类 : 在原生端点中只提供了一个关闭应用的端点,即 /shutdown 端点。根据 Spring Boot Actuator 默认提供的端点列表,我们将部分常见端点的类型、路径和描述梳理在如下表格中,仅供参考。
如果 Spring Boot Actuator 默认提供的端点信息不能满足业务需求,我们可以对其进行修改和扩展。此时,常见实现方案有两种,一种是扩展现有的监控端点,另一种是自定义新的监控端点。关于自定义新的监控端点的内容限于文章篇幅,这里不再展开讨论,下面我们以扩展 metrics 监控端点为例来介绍下如何自定义 metrcis 指标。
2.1 Actuator 的度量指标 我们知道 Spring Boot Actuator 提供的原生端点有应用配置类、度量指标类和操作控制类这三类,这里我们更多关注与度量指标相关的内容。同时,我们还将给出如何创建自定义 Actuator 端点 metrics 指标的实现方法,以便应对默认端点无法满足需求的应用场景。再这之前我们先了解 Actuator 中的度量指标。
对于系统监控而言,度量是一个很重要的维度。在 Spring Boot 2.X 版本中,Actuator 组件主要使用内置的 Micrometer 库实现度量指标的收集和分析。
Micrometer 是一款监控指标的度量类库,为 Java 平台上的性能数据收集提供了一套通用的 API。在应用程序中,我们只使用 Micrometer 提供的通用 API 即可收集度量指标。
下面我们先来简要介绍 Micrometer 中包含的几个核心概念。
2.1.1 Meter 接口 首先我们需要介绍的是计量器 Meter,它是一个接口,代表的是需要收集的性能指标数据。关于 Meter 的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public interface Meter extends AutoCloseable { Id getId () ; Iterable<Measurement> measure () ; enum Type { COUNTER, GAUGE, LONG_TASK_TIMER, TIMER, DISTRIBUTION_SUMMARY, OTHER } }
通过上述代码,我们注意到 Meter 中存在一个 Id 对象,该对象的作用是定义 Meter 的名称和标签。从 Type 的枚举值中,我们不难看出 Micrometer 中包含的所有计量器类型。
接下来我们先说明两个概念。
Meter 的名称:对于计量器来说,每个计量器都有自己的名称,而且在创建时它们都可以指定一系列标签。 Meter 的标签:标签的作用在于监控系统可以通过这些标签对度量进行分类过滤。 2.1.2 计量器类型 在日常开发过程中,常用的计量器类型主要分为计数器 Counter、计量仪 Gauge 和计时器 Timer 这三种。
Counter:这个计量器的作用和它的名称一样,就是一个不断递增的累加器,我们可以通过它的 increment 方法实现累加逻辑。 Gauge:与 Counter 不同,Gauge 所度量的值并不一定是累加的,我们可以通过它的 gauge 方法指定数值。 Timer:这个计量器比较简单,就是用来记录事件的持续时间。 2.1.3 计量器创建 既然我们已经明确了常用的计量器及其使用场景,那么如何创建这些计量器呢?
在 Micrometer 中,提供了 Metrics 类以针对不同的 Meter 提供了对应的创建方法。以创建一个 Counter为例。通常,我们会采用如下所示代码进行创建:
1 Counter counter = Metrics.counter(name, tags);
其中 name 为 Meter 的名称,tags 为一个不定长数组,表示 Meter 的标签。下文我们会示例来介绍。
2.1.4 自定义 Metrics 指标 前面介绍 Micrometer 时,我们已经提到 Metrics 指标体系中包含支持 Counter 的度量指标。
通过将 Counter 注入业务代码中,我们就可以记录自己想要的度量指标。其中,Counter 用来暴露 increment() 方法
下面我们以 Counter 为例介绍在业务代码中嵌入自定义 Metrics 指标的方法,以模拟实现支付接口 pv 指标的监控。
首先我们定义工具类 Counter 工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf 4jpublic class CounterMonitorUtil { public static final String FAIL = "0" ; public static final String SUCCESS = "1" ; public static void countEvent (String name, String... tags) { try { Counter counter = Metrics.counter(name, tags); counter.increment(); log.info("countEvent:{}" , JSONObject.toJSONString(counter)); } catch (Exception e) { log.error("CounterMonitor countEvent failed:{}" , e.getMessage()); } } }
我们定义 controller 类,并实现 mock 支付接口 commitPay。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Slf 4j@RequestMapping ("/api/vi/pay" )@RestController public class PayController { @PayMonitorCollect @RequestMapping (value = "/commitPay" , method = RequestMethod.POST) public CommitPayResponse commitPay (@Validated @RequestBody CommitPayRequest request) { CommitPayResponse response = new CommitPayResponse(); log.info("commitPay request: {}" , JSONObject.toJSONString(request)); response.setIsSuccess(Boolean.TRUE); return response; } } @Data public class CommitPayRequest implements Serializable { private String payMethod; private String orderNo; private Long payAmount; } @Data public class CommitPayResponse implements Serializable { private Boolean isSuccess; }
观察上述代码,我们重点关注注解 PayMonitorCollect 及其对应的切面 PayEventAspect,如下是该切面的实现,我们通过该切面实现 commitPay 接口的监控:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 @Slf 4j@Component @Aspect public class PayEventAspect { @Resource private PayMetricsProperties payMetricsProperties; @Pointcut ("@annotation(com.shoto.prometheus.annotations.PayMonitorCollect)" ) public void collectCut () { } @Around ("collectCut()" ) public Object invokeAround (ProceedingJoinPoint jp) throws Throwable { Object result = null ; boolean isSuccess = false ; try { result = jp.proceed(); isSuccess = obtainPayResult(result); } finally { dealCollect(isSuccess ? CounterMonitorUtil.SUCCESS : CounterMonitorUtil.FAIL, jp, result); } return result; } private void dealCollect (String isSuccess, ProceedingJoinPoint jp, Object result) { String methodName = Strings.EMPTY; try { MethodSignature signature = (MethodSignature) jp.getSignature(); methodName = signature.getName(); String metricsName = payMetricsProperties.getPayEventPv(); CounterMonitorUtil.countEvent(metricsName, "event" , methodName, "isSuccess" , isSuccess, "payMethod" , obtainPayMethod(jp)); } catch (Exception ex) { log.error("dealCollect failed, method {}, exec result {}" , methodName, JSONObject.toJSONString(result), ex); } } private boolean obtainPayResult (Object result) { try { if (Objects.isNull(result)) { return false ; } JSONObject payRequestJb = JSONObject.parseObject(JSON.toJSONString(result)); return Optional.ofNullable(payRequestJb) .map(e -> e.getBoolean("isSuccess" )).filter(Boolean.TRUE::equals).isPresent(); } catch (Exception ex) { log.error("PayEventAspect obtainPayResult exec failed" , ex); } return false ; } private String obtainPayMethod (ProceedingJoinPoint jp) { try { Object[] args = jp.getArgs(); if (args == null || args.length == 0 ) { return "" ; } JSONObject payRequestJb = JSONObject.parseObject(JSON.toJSONString(args[0 ])); Optional<String> optional = Optional.ofNullable(payRequestJb) .map(e -> e.getString("payMethod" )).filter(StringUtils::isNotEmpty); return optional.orElse("" ); } catch (Exception ex) { log.error("PayEventAspect obtainPayMethod exec failed" , ex); } return "" ; } }
请求 commitPay 接口后通过访问 http://localhost:8080/actuator/metrics
便可以看到我们自定义的 metrics 指标。
访问 http://localhost:8081/actuator/metrics/pay_event_pv
可以看到我们业务程序上报的信息,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 "name": "pay_event_pv", "description": null, "baseUnit": null, "measurements": [ { "statistic": "COUNT", // 请求一次 commitPay 接口, value 值会累加 "value": 2 } ], "availableTags": [ // ... { "tag": "application", "values": [ "prometheus-alert" ] }, { "tag": "payMethod", "values": [ "wechat" ] }, { "tag": "event", "values": [ "commitPay" ] }, { "tag": "isSuccess", "values": [ "1" ] } ] }
然后需要引入 prometheus 依赖,以让 micrometer 支持 prometheus:
1 2 3 4 5 <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <scope>runtime</scope> </dependency>
通过访问 http://localhost:8081/actuator/prometheus
我们可以看到暴露到 prometheus 的指标信息,我们可以通过 prometheus 和 grafana 来保存和展示这些信息:
1 2 3 # HELP pay_event_pv_total # TYPE pay_event_pv_total counter pay_event_pv_total{application="prometheus-alert",event="commitPay",isSuccess="1",payMethod="wechat",task_execution_id="unknown",task_external_execution_id="unknown",task_name="unknown",task_parent_execution_id="unknown",} 2.0
3. 引入 Prometheus 和 Grafana 关于 Prometheus 和 Grafana 的搭建,这里推荐阅读 prometheus-book ,本文不在赘述。搭载好 prometheus 后,我们访问 http://你的IP:9090
进入到 prometheus 管理后台,我们可以通过执行 promQL 来查询统计指标信息,例如执行 up
查询节点的运行状态:
我们也可以通过 sum (round(delta(pay_event_pv_total{event="commitPay",isSuccess="1", payMethod="wechat"}[60s])))
语句来查询微信支付请求支付接口成功的曲线,在 grafana 显示如下:
4. 引入 Prometheus Alert 关于 Prometheus Alert 的搭建,这里推荐阅读 prometheus-book ,本文不在赘述。这里只讲下搭载 Alert 组件并通过企业微信告警过程中遇到的几个坑:
Alert 组件推荐使用最新的版本,根据 prometheus-book 使用的 0.15 版本会导致告警出错,并报 missing agentId 的异常 企业微信告警需要添加可信用 IP,再添加可信用 IP 之前需要添加企业可信域名。本人根据网上提供的方式比如阿里云创建函数最后无法行得通。由于本人使用 hexo 搭建过个人博客,并购买了个人域名。因此只需要将从企业微信下载的 txt 文件放在根目录即可。 以对服务运行状态的监控为例,如下是企业微信服务宕机和恢复的告警例图:
5. 推荐阅读