基于MicroMeter实现应用监控指标

提到“监控”(Moniter),你的第一反应是什么?

是老传统监控软件Zabbix、Nagios?还是近几年火爆IT圈的Promethos?

别急着比较系统,这篇文章,我们先聊聊应用监控指标。

顾名思义,“应用监指标”就是根据监控需求,在我们的应用系统中预设埋点,并支持与监控系统对接。

典型的监控项如:接口请求次数、接口响应时间、接口报错次数….

我们将介绍MicroMeter开源项目,并使用它实现Spring MVC的应用监控指标。

MicroMeter简介

Micrometer是社区最流行的监控项项目之一,它提供了一个抽象、强大、易用的抽象门面接口,可以轻松的对接包括Prometheus、JMX等在内的近20种监控系统。它的作用和Slf4j类似,只不过它关注的不是日志,而是应用指标(application metrics)。

自定义应用监控项初探

下面,我们来开始micrometer之旅。

由于网上关于micrometer对接Prometheus的文章已经很多了,这里我特意选择了JMX。

通过JMX Bean暴露的监控项,你可以轻松的对接Zabbix等老牌监控系统。

这里提醒的是JMX不支持类似Prometheus的层级结构,而只支持一级结构(tag会被打平),具体可以参见官方文档。当然,这在代码实现上是完全透明的。

首先,我们新建一个简单的Spring Boot项目,并引入pom文件:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-actuator</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>io.micrometer</groupId>
  11. <artifactId>micrometer-registry-jmx</artifactId>
  12. <version>1.8.7</version>
  13. </dependency>

然后开发如下的Spring MVC接口:

  1. package com.coder4.homs.micrometer.web;
  2. import com.coder4.homs.micrometer.web.data.UserVO;
  3. import io.micrometer.core.instrument.Counter;
  4. import io.micrometer.core.instrument.MeterRegistry;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.web.bind.annotation.GetMapping;
  7. import org.springframework.web.bind.annotation.PathVariable;
  8. import org.springframework.web.bind.annotation.RestController;
  9. import javax.annotation.PostConstruct;
  10. @RestController
  11. public class UserController {
  12. @Autowired
  13. private MeterRegistry meterRegistry;
  14. private Counter COUNTER_GET_USER;
  15. @PostConstruct
  16. public void init() {
  17. COUNTER_GET_USER = meterRegistry.counter("app_requests_method_count", "method", "UserController.getUser");
  18. }
  19. @GetMapping(path = "/users/{id}")
  20. public UserVO getUser(@PathVariable int id) {
  21. UserVO user = new UserVO();
  22. user.setId(id);
  23. user.setName(String.format("user_%d", id));
  24. COUNTER_GET_USER.increment();
  25. return user;
  26. }
  27. }

在上面的代码中:

  1. 我们实现了UserController这个REST接口,他之中的/users/{id}可以获取用户。

  2. UserController注册了一个Counter,Counter由名字和tag组成,用过Prometheus的应该很熟悉这种套路了。

  3. 每次请求时,会将上述Counter加一操作。

我们来测试一下,执行2次

  1. curl "127.0.0.1:8080/users/1"
  2. {"id":1,"name":"user_1"}

然后打开本地的jconsole,可以发现JMX Bean暴露出了了metrics、gauge等分类,我们打开”metrics/apprequests_method…”这个指标,点击进去,可以发现具体的值也就是2。

f

借助拦截器批量统计监控项目

上述代码可以实现功能,但是你应该发现了,实现起来很繁琐,如果我们有10个接口,那岂不是要写很多无用代码?

相信你已经想到了,可以用类似AOP (切面编程)的思路,解决问题。

不过针对Spring MVC这个场景,使用AOP有点“大炮打蚊子”的感觉,我们可以使用拦截器实现。

首先自定义拦截器的自动装配:

  1. package com.coder4.homs.micrometer.configure;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. @Configuration
  7. public class MeterConfig implements WebMvcConfigurer {
  8. @Bean
  9. public MeterInterceptor getMeterInterceptor() {
  10. return new MeterInterceptor();
  11. }
  12. @Override
  13. public void addInterceptors(InterceptorRegistry registry){
  14. registry.addInterceptor(getMeterInterceptor())
  15. .addPathPatterns("/**")
  16. .excludePathPatterns("/error")
  17. .excludePathPatterns("/static/*");
  18. }
  19. }

上面代码很简单,就是新增了新的拦截器MeterInterceptor。

我们看下拦截器做了什么:

  1. package com.coder4.homs.micrometer.configure;
  2. import io.micrometer.core.instrument.Counter;
  3. import io.micrometer.core.instrument.DistributionSummary;
  4. import io.micrometer.core.instrument.MeterRegistry;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.web.method.HandlerMethod;
  7. import org.springframework.web.servlet.HandlerInterceptor;
  8. import javax.servlet.http.HttpServletRequest;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.util.Optional;
  11. public class MeterInterceptor implements HandlerInterceptor {
  12. @Autowired
  13. private MeterRegistry meterRegistry;
  14. private ThreadLocal<Long> tlTimer = new ThreadLocal<>();
  15. private static Optional<String> getMethod(HttpServletRequest request, Object handler) {
  16. if (handler instanceof HandlerMethod) {
  17. return Optional.of(String.format("%s_%s_%s", ((HandlerMethod) handler).getBeanType().getSimpleName(),
  18. ((HandlerMethod) handler).getMethod().getName(), request.getMethod()));
  19. } else {
  20. return Optional.empty();
  21. }
  22. }
  23. private void recordTimeDistribution(HttpServletRequest request, Object handler, long ms) {
  24. Optional<String> methodOp = getMethod(request, handler);
  25. if (methodOp.isPresent()) {
  26. DistributionSummary.builder("app_requests_time_ms")
  27. .tag("method", methodOp.get())
  28. .publishPercentileHistogram()
  29. .register(meterRegistry)
  30. .record(ms);
  31. }
  32. }
  33. public Optional<Counter> getCounterOfTotalCounts(HttpServletRequest request, Object handler) {
  34. Optional<String> methodOp = getMethod(request, handler);
  35. if (methodOp.isPresent()) {
  36. return Optional.of(meterRegistry.counter("app_requests_total_counts", "method",
  37. methodOp.get()));
  38. } else {
  39. return Optional.empty();
  40. }
  41. }
  42. public Optional<Counter> getCounterOfExceptionCounts(HttpServletRequest request, Object handler) {
  43. Optional<String> methodOp = getMethod(request, handler);
  44. if (methodOp.isPresent()) {
  45. return Optional.of(meterRegistry.counter("app_requests_exption_counts", "method",
  46. methodOp.get()));
  47. } else {
  48. return Optional.empty();
  49. }
  50. }
  51. public Optional<Counter> getCounterOfRespCodeCounts(HttpServletRequest request, HttpServletResponse response,
  52. Object handler) {
  53. Optional<String> methodOp = getMethod(request, handler);
  54. if (methodOp.isPresent()) {
  55. return Optional.of(meterRegistry.counter(String.format("app_requests_resp%d_counts", response.getStatus()),
  56. "method", methodOp.get()));
  57. } else {
  58. return Optional.empty();
  59. }
  60. }
  61. @Override
  62. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  63. tlTimer.set(System.currentTimeMillis());
  64. return true;
  65. }
  66. @Override
  67. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  68. // record time
  69. recordTimeDistribution(request, handler, System.currentTimeMillis() - tlTimer.get());
  70. tlTimer.remove();
  71. // total counts
  72. getCounterOfTotalCounts(request, handler).ifPresent(counter -> counter.increment());
  73. // different response code count
  74. getCounterOfRespCodeCounts(request, response, handler).ifPresent(counter -> counter.increment());
  75. if (ex != null) {
  76. // exception counts
  77. getCounterOfExceptionCounts(request, handler).ifPresent(counter -> counter.increment());
  78. }
  79. }
  80. }

代码有点长,解释一下:

  1. 自动注入MeterRegistry,老套路了

  2. getCounterOfXXX几个方法,通过request、handler来生成具体的监控项名称和标签,形如:app_requests_method_count.method.UserController.getUser。

  3. preHandle中预设了ThreadLocal的定时器

  4. recordTimeDistribution使用了Distribution,这是一个可以统计百分位的MicroMeter组件,类似Prometheus的histogram功能的你应该能秒懂。

  5. afterCompletion根据前面定时器,计算本次请求时间,并记录到Distributon中。

  6. afterCompletion记录总请求数、分resp.code的请求数、出错请求数。

我们打开jconsole看下:

f

在之前meters的基础上,新增了histogram分类,里面会详细记录请求时间,比如我这里做了一些本地压测后,.99时间是12ms,.95时间是1ms。

在上面的基础上稍做修改,就可以投入使用了。

感兴趣的话,你可以探索如何对Dubbo、gRPC等RPC接口进行应用程序监控项。

本篇文章的代码,我放到了homs-micrometer这个github项目中,感兴趣的话可以查阅。