3W字干货深入分析基于Micrometer和Prometheus实现度量和监控的方案(下)

本文涉及的产品
可观测监控 Prometheus 版,每月50GB免费额度
可观测可视化 Grafana 版,10个用户账号 1个月
EMR Serverless StarRocks,5000CU*H 48000GB*H
简介: 最近线上的项目使用了spring-actuator做度量统计收集,使用Prometheus进行数据收集,Grafana进行数据展示,用于监控生成环境机器的性能指标和业务数据指标。一般,我们叫这样的操作为"埋点"。SpringBoot中的依赖spring-actuator中集成的度量统计API使用的框架是Micrometer,官网是micrometer.io。在实践中发现了业务开发者滥用了Micrometer的度量类型Counter,导致无论什么情况下都只使用计数统计的功能。这篇文章就是基于Micrometer分析其他的度量类型API的作用和适用场景。

基于SpirngBoot、Prometheus、Grafana集成



集成了Micrometer框架的JVM应用使用到MicrometerAPI收集的度量数据位于内存之中,因此,需要额外的存储系统去存储这些度量数据,需要有监控系统负责统一收集和处理这些数据,还需要有一些UI工具去展示数据,一般情况下大佬或者老板只喜欢看炫酷的仪表盘或者动画。常见的存储系统就是时序数据库,主流的有InfluxDatadog等。比较主流的监控系统(主要是用于数据收集和处理)就是Prometheus(一般叫普罗米修斯,下面就这样叫吧)。而展示的UI目前相对用得比较多的就是Grafana。另外,Prometheus已经内置了一个时序数据库的实现,因此,在做一套相对完善的度量数据监控的系统只需要依赖目标JVM应用,Prometheus组件和Grafana组件即可。下面花一点时间从零开始搭建一个这样的系统,之前写的一篇文章基于Windows系统,操作可能跟生产环境不够接近,这次使用CentOS7


SpirngBoot中使用Micrometer


SpringBoot中的spring-boot-starter-actuator依赖已经集成了对Micrometer的支持,其中的metrics端点的很多功能就是通过Micrometer实现的,prometheus端点默认也是开启支持的,实际上actuator依赖的spring-boot-actuator-autoconfigure中集成了对很多框架的开箱即用的API,其中prometheus包中集成了对Prometheus的支持,使得使用了actuator可以轻易地让项目暴露出prometheus端点,使得应用作为Prometheus收集数据的客户端,Prometheus(服务端软件)可以通过此端点收集应用中Micrometer的度量数据。


微信截图_20220513114835.pngjvm-m-1.png

我们先引入spring-boot-starter-actuatorspring-boot-starter-web,实现一个CounterTimer作为示例。依赖:


<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.22</version>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>1.1.0</version>
        </dependency>
    </dependencies>
复制代码


接着编写一个下单接口和一个消息发送模块,模拟用户下单之后向用户发送消息:


//实体
@Data
public class Message {
    private String orderId;
    private Long userId;
    private String content;
}
@Data
public class Order {
    private String orderId;
    private Long userId;
    private Integer amount;
    private LocalDateTime createTime;
}
//控制器和服务类
@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;
    @PostMapping(value = "/order")
    public ResponseEntity<Boolean> createOrder(@RequestBody Order order) {
        return ResponseEntity.ok(orderService.createOrder(order));
    }
}
@Slf4j
@Service
public class OrderService {
    private static final Random R = new Random();
    @Autowired
    private MessageService messageService;
    public Boolean createOrder(Order order) {
        //模拟下单
        try {
            int ms = R.nextInt(50) + 50;
            TimeUnit.MILLISECONDS.sleep(ms);
            log.info("保存订单模拟耗时{}毫秒...", ms);
        } catch (Exception e) {
            //no-op
        }
        //记录下单总数
        Metrics.counter("order.count", "order.channel", order.getChannel()).increment();
        //发送消息
        Message message = new Message();
        message.setContent("模拟短信...");
        message.setOrderId(order.getOrderId());
        message.setUserId(order.getUserId());
        messageService.sendMessage(message);
        return true;
    }
}
@Slf4j
@Service
public class MessageService implements InitializingBean {
    private static final BlockingQueue<Message> QUEUE = new ArrayBlockingQueue<>(500);
    private static BlockingQueue<Message> REAL_QUEUE;
    private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
    private static final Random R = new Random();
    static {
        REAL_QUEUE = Metrics.gauge("message.gauge", Tags.of("message.gauge", "message.queue.size"), QUEUE, Collection::size);
    }
    public void sendMessage(Message message) {
        try {
            REAL_QUEUE.put(message);
        } catch (InterruptedException e) {
            //no-op
        }
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        EXECUTOR.execute(() -> {
            while (true) {
                try {
                    Message message = REAL_QUEUE.take();
                    log.info("模拟发送短信,orderId:{},userId:{},内容:{},耗时:{}毫秒", message.getOrderId(), message.getUserId(),
                            message.getContent(), R.nextInt(50));
                } catch (Exception e) {
                    throw new IllegalStateException(e);
                }
            }
        });
    }
}
//切面类
@Component
@Aspect
public class TimerAspect {
    @Around(value = "execution(* club.throwable.smp.service.*Service.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        Timer timer = Metrics.timer("method.cost.time", "method.name", method.getName());
        ThrowableHolder holder = new ThrowableHolder();
        Object result = timer.recordCallable(() -> {
            try {
                return joinPoint.proceed();
            } catch (Throwable e) {
                holder.throwable = e;
            }
            return null;
        });
        if (null != holder.throwable) {
            throw holder.throwable;
        }
        return result;
    }
    private class ThrowableHolder {
        Throwable throwable;
    }
}
复制代码


yaml的配置如下:


server:
  port: 9091
management:
  server:
    port: 10091
  endpoints:
    web:
      exposure:
        include: '*'
      base-path: /management  
复制代码


注意多看spring官方文档关于Actuator的详细描述,在SpringBoot2.x之后,配置Web端点暴露的权限控制和SpringBoot1.x有很大的不同。总结一下就是:除了shutdown端点之外,其他端点默认都是开启支持的(这里仅仅是开启支持,并不是暴露为Web端点,端点必须暴露为Web端点才能被访问),禁用或者开启端点支持的配置方式如下:


management.endpoint.${端点ID}.enabled=true/false
复制代码


可以查看actuator-api文档查看所有支持的端点的特性,这个是2.1.0.RELEASE版本的官方文档,不知道日后链接会不会挂掉。端点只开启支持,但是不暴露为Web端点,是无法通过http://{host}:{management.port}/{management.endpoints.web.base-path}/{endpointId}访问的。暴露监控端点为Web端点的配置是:


management.endpoints.web.exposure.include=info,health
management.endpoints.web.exposure.exclude=prometheus
复制代码


management.endpoints.web.exposure.include用于指定暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔。

management.endpoints.web.exposure.exclude用于指定不暴露为Web端点的监控端点,指定多个的时候用英文逗号分隔。management.endpoints.web.exposure.include默认指定的只有infohealth两个端点,我们可以直接指定暴露所有的端点:management.endpoints.web.exposure.include=*,如果采用YAML配置,记得要在星号两边加上英文单引号。暴露所有Web监控端点是一件比较危险的事情,如果需要在生产环境这样做,请务必先确认http://{host}:{management.port}不能通过公网访问(也就是监控端点访问的端口只能通过内网访问,这样可以方便后面说到的Prometheus服务端通过此端口收集数据)。


Prometheus的安装和配置



Prometheus目前的最新版本是2.5,鉴于笔者当前没深入玩过Docker,这里还是直接下载它的压缩包解压安装。


wget https://github.com/prometheus/prometheus/releases/download/v2.5.0/prometheus-2.5.0.linux-amd64.tar.gz
tar xvfz prometheus-*.tar.gz
cd prometheus-*
复制代码


先编辑解压出来的目录下的Prometheus配置文件prometheus.yml,主要修改scrape_configs节点的属性:


scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'
    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.
 # 这里配置需要拉取度量信息的URL路径,这里选择应用程序的prometheus端点
    metrics_path: /management/prometheus
    static_configs:
 # 这里配置host和port
      - targets: ['localhost:10091']
复制代码


配置拉取度量数据的路径为localhost:10091/management/metrics,此前记得把前一节提到的应用在虚拟机中启动。接着启动Prometheus应用:


# 可选参数 --storage.tsdb.path=存储数据的路径,默认路径为./data
./prometheus --config.file=prometheus.yml
复制代码


Prometheus引用的默认启动端口是9090,启动成功后,日志如下:


微信截图_20220513114853.png

jvm-m-2.png

此时,访问http://${虚拟机host}:9090/targets就能看到当前Prometheus中执行的Job


微信截图_20220513114902.png

jvm-m-3.png

访问http://${虚拟机host}:9090/graph可以查找到我们定义的度量Meterspring-boot-starter-actuator中已经定义好的一些关于JVM或者Tomcat的度量Meter。我们先对应用的/order接口进行调用,然后查看一下监控前面在应用中定义的order_count_totalmethod_cost_time_seconds_sum


微信截图_20220513114908.png

jvm-m-4.png


微信截图_20220513114915.pngjvm-m-5.png


可以看到,Meter的信息已经被收集和展示,但是显然不够详细和炫酷,这个时候就需要使用Grafana的UI做一下点缀。


Grafana的安装和使用



Grafana的安装过程如下:


wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.3.4-1.x86_64.rpm 
sudo yum localinstall grafana-5.3.4-1.x86_64.rpm
复制代码


安装完成后,通过命令service grafana-server start启动即可,默认的启动端口为3000,通过http://${host}:3000访问即可。初始的账号密码都为admin,权限是管理员权限。接着需要在Home面板添加一个数据源,目的是对接Prometheus服务端从而可以拉取它里面的度量数据。数据源添加面板如下:


微信截图_20220513114923.png

jvm-m-6.png


其实就是指向Prometheus服务端的端口就可以了。接下来可以天马行空地添加需要的面板,就下单数量统计的指标,可以添加一个Graph的面板:


微信截图_20220513114930.png

jvm-m-7.png


配置面板的时候,需要在基础(General)中指定Title:


微信截图_20220513114938.png

jvm-m-9.png


接着比较重要的是Metrics的配置,需要指定数据源和Prometheus的查询语句:


微信截图_20220513114946.png

jvm-m-8.png


最好参考一下Prometheus的官方文档,稍微学习一下它的查询语言PromQL的使用方式,一个面板可以支持多个PromQL查询。前面提到的两项是基本配置,其他配置项一般是图表展示的辅助或者预警等辅助功能,这里先不展开,可以去Grafana的官网挖掘一下使用方式。然后我们再调用一下下单接口,过一段时间,图表的数据就会自动更新和展示:


微信截图_20220513114953.png

jvm-m-10.png


接着添加一下项目中使用的Timer的Meter,便于监控方法的执行时间,完成之后大致如下:


微信截图_20220513115001.png

jvm-m-11.png


上面的面板虽然设计相当粗糙,但是基本功能已经实现。设计面板并不是一件容易的事,如果有需要可以从Github中搜索一下grafana dashboard关键字找现成的开源配置使用或者二次加工后使用。


小结



常言道:工欲善其事,必先利其器。MicrometerJVM应用的一款相当优异的度量框架,它提供基于Tag和丰富的度量类型和API便于多维度地进行不同角度度量数据的统计,可以方便地接入Prometheus进行数据收集,使用Grafana的面板进行炫酷的展示,提供了天然的spring-boot体系支持。但是,在实际的业务代码中,度量类型Counter经常被滥用,一旦工具被不加思考地滥用,就反而会成为混乱或者毒瘤。因此,这篇文章就是对Micrometer中的各种Meter的使用场景基于个人的理解做了调研和分析,后面还会有系列的文章分享一下这套方案在实战中的经验和踩坑经历。


参考资料:


(本文完 To be continue c-10-d n-e-20181102 最近有点忙,没办法经常更新)


相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
相关文章
|
5天前
|
Prometheus Kubernetes 监控
Kubernetes(K8S) 监控 Prometheus + Grafana
Kubernetes(K8S) 监控 Prometheus + Grafana
20 2
|
11天前
|
Prometheus 监控 Cloud Native
在 HBase 集群中,Prometheus 通常监控哪些类型的性能指标?
在 HBase 集群中,Prometheus 通常监控哪些类型的性能指标?
|
17天前
|
数据采集 弹性计算 Prometheus
重磅升级!从自建Prometheus到阿里云托管:无缝迁移,监控能力全面飞跃
【8月更文挑战第2天】如何从自建开源 Prometheus 迁移到阿里云托管 Prometheus 服务
29 2
|
17天前
|
Prometheus 监控 Cloud Native
|
12天前
|
Prometheus 监控 Cloud Native
基于Prometheus搭建监控平台
基于Prometheus搭建监控平台
|
3月前
|
Prometheus 监控 Kubernetes
如何用 Prometheus Operator 监控 K8s 集群外服务?
如何用 Prometheus Operator 监控 K8s 集群外服务?
|
2月前
|
Prometheus 监控 Cloud Native
基于Prometheus和Grafana的监控平台 - 环境搭建
基于Prometheus和Grafana的监控平台 - 环境搭建
|
22天前
|
Prometheus 监控 Cloud Native
Prometheus结合Consul采集多个MySQL实例的监控指标
将 Prometheus 与 Consul 结合使用,实现对多个 MySQL 实例的自动发现与监控,不仅提高了监控的效率和准确性,也为管理动态扩缩容的数据库环境提供了强大的支持。通过细致配置每一部分,业务可以获得关键的性能指标和运行健康状况的即时反馈,进而优化资源配置,提高系统的稳定性和可用性。
38 3
|
1月前
|
Prometheus 监控 Cloud Native
使用Spring Boot和Prometheus进行监控
使用Spring Boot和Prometheus进行监控
|
2月前
|
Prometheus 监控 Kubernetes
深入理解Prometheus: Kubernetes环境中的监控实践
Kubernetes简介 在深入Prometheus与Kubernetes的集成之前,首先简要回顾一下Kubernetes的核心概念。Kubernetes是一个开源的容器编排平台,用于自动化容器的部署、扩展和管理。它提供了高度的可扩展性和灵活性,使得它成为微服务和云原生应用的理想选择。 核心组件 • 控制平面(Control Plane):集群管理相关的组件,如API服务器、调度器等。 • 工作节点(Nodes):运行应用容器的机器。 • Pods:Kubernetes的基本运行单位,可以容纳一个或多个容器。