SpringBoot集成Prometheus

文摘   2024-04-10 11:30   江苏  
  1. 背景

    微服务背景下,团队中的各同学如果有一种先见之明的超能力,能感知到自己的负责的业务健康状态,那很多问题都可以提前介入定位、去解决。但根据什么来判断呢?如果都是靠问运维同学,那问的同学多了,运维直接一天啥也不干了,况且我相信大家也不希望用这样的方式。运维嫌麻烦,重复的问题反复答,开发嫌不智能,每次都要问。那有没有一种方案能让大家都省心呢?答案是肯定的,如果有一个数据大盘,那大家都能释放出来做该做的事。

  2. 调研

    总体来说还是分自研和找一些市面上存在的组件,采集自己需要的各种维度(应用层面、数据层面)的数据,由于这些数据多数情况下都相同,一些优秀的组件便应运而生了,如Grafana、InfluxDb、Nagios、Zabbix、Elastic Stack :

    Grafana:Grafana是一种流行的开源数据可视化工具,可以与多个数据源集成,包括Prometheus。它可以通过可视化仪表板展示和分析Prometheus收集的数据。

    InfluxDB:InfluxDB是一种开源时间序列数据库,专门用于处理和存储大量时间序列数据,如机器指标、事件日志等。和Prometheus类似,InfluxDB也具备数据采集和查询功能。

    Nagios:Nagios是一种广泛使用的开源网络监控系统,可以监测网络设备、服务器和应用程序的运行状况。与Prometheus不同的是,Nagios主要基于主动式监控,而不是Prometheus的基于抓取的方式。

    Zabbix:Zabbix是一种功能强大的开源网络监控系统,可以监控各种网络设备、服务器和应用程序。它提供了多种监控方式,包括主动和被动式监控,并支持自定义的监控和警报设置。

    Elastic Stack:Elastic Stack(前身是ELK Stack)是一个集成了Elasticsearch、Logstash和Kibana的解决方案,能够处理和分析大量的日志和指标数据。Elasticsearch可以用于存储和查询数据,Logstash用于数据采集和清洗,Kibana则提供了数据可视化和仪表板功能。

    既然上面很多都提到了Prometheus,加上团队有的同学例会也提过,既然出现了人传人的现象,那我们就选Prometheus,这样遇到问题也许解决成本会低一点

  3. Prometheus

    结合起来看感觉作者取名有点故意的意思。具体来说究竟是干啥的呢?我们老规矩官网(https://prometheus.io/)看看一句话概述的中心思想:

    译:一款在指标和报警方面领先的开源监控解决方案,让你通过指标去洞察你的系统。


  4. 动手

    a. 安装:去 https://prometheus.io/download/ 根据操作系统下载对应的Prometheus版本,我还是大家熟知的穷鬼,下载windows版本玩玩儿就好了。


    然后解压编辑里面的配置文件,也可以先不动,直接启动找找感觉:


    Doc或PowerShell运行可执行文件:

    // 也可以不带配置文件,默认是这个,除非你的名字变了prometheus.exe --config.file=prometheus.yml




    配置文件中默认是9090,所以我们直接访问:http://localhost:9090 就可以看到Prometheus服务启动了:

    b. 配置:打开Prometheus.yaml 修改配置,增加你本机的应用端口,这里要注意,1处是Prometheus对自己的监控,不能修改,要新增,我刚开始没注意就报错了,希望你绕道而行:


    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: 'actuator/prometheus'
    # metrics_path defaults to '/metrics' # scheme defaults to 'http'.
    static_configs: - targets: ["localhost:9090"]  // 以下是我增加的 2个服务 - job_name: "your_server_name_one" metrics_path: 'actuator/prometheus' static_configs: - targets: ["localhost:8083"]
    - job_name: "your_server_name_two" metrics_path: 'actuator/prometheus' static_configs: - targets: ["localhost:8198"]


  5. 整合

    a.前置:Prometheus是监控系统,数据是通过扩展 SpringBoot 而来。你如果有SpringBoot项目,可以访问自带的:http://${ip}:${port}/${context-path}/actuator/health 接口看看返回的数据。如下图:左侧,监控返回了磁盘空间、ping、nacos、mail等指标,右侧是对应的数据结构:

    // 如果其中某个指标不想受监控 可以通过以下配置关闭 ,// ${jar}文件中meat_info下的文件的spring-configuration-metadata.json 查看management.health.mail.enabled=falsemanagement.health.diskspace.enabled=false

    b. 项目集成:

    增加maven依赖,我没加版本,会自动随着项目parent下载对应版本:

    <!-- spring-boot-actuator依赖 --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-actuator</artifactId></dependency><!-- prometheus依赖 --><dependency>    <groupId>io.micrometer</groupId>    <artifactId>micrometer-registry-prometheus</artifactId></dependency><!-- 上面2个就够了,加下面这个是为了把nacos注册也加到监控中,否则不行 --><dependency>    <groupId>com.alibaba.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>    <version>2.2.2.RELEASE</version></dependency>

    修改项目配置文件,增加相应的Promethues配置项:

    spring.application.name = ${application_name}
    server.servlet.context-path = ${app_context_path}// 指定promethues采集数据的接口spring.cloud.nacos.discovery.metadata.metricpath=${server.servlet.context-path}/actuator/prometheus
    spring.cloud.nacos.discovery.metadata.management.context-path=${server.servlet.context-path}/actuator
    management.metrics.export.prometheus.enabled=truemanagement.metrics.export.jmx.enabled=truemanagement.metrics.tags.application=${spring.application.name}

    如果你用的是nacos,yaml可以通过工具(https://toyaml.com/index.html)转换配置格式:



    启动promethues服务,验证是否纳入管理:


    上面我服务断了,重新启动访问指定的接口查看指标,发现有很多(也可以自定义指标),随便截个图,如下:

    对比发现:actuator/health接口只提供了当前服务的状态,可按需扩展,自行编码实现更多维度的健康检查,数据放置在一个Map结构中;prometheus 直接提供了诸如http请求状态码、QPS、内存等方面的数据,经团队讨论,扩展health产生的数据不便于运维同学统计,基于prometheus 更能节约时间也符合团队当前想要监控的纬度,所以最终确定监控以prometheus提供的数据为主,在各维度提供监控报警。

  6. 自定义指标

    实际工作中,难免会出现除上述Prometheus默认在很多监控的指标外,还需要自定义一些指标,比如一个请求的执行耗时、JVM堆中类的实例数量和占用空间大小。

    查看源码发现在io.micrometer.core.instrument.MeterRegistry下有很多子类,要操作Prometheus向其中注入数据自然要拿到该类型才可以。


    观察已有的指标发现Prometheus中有指标名称、所属应用、指标值,指标标识等信息:


    让自定义的指标也能清楚表示出所属应用,需要先将向容器注入一个Bean:

    @Configuration@Datapublic class MicroMeterConfig {
    @Value("${spring.application.name}") private String springApplicationName;
    @Bean MeterRegistryCustomizer<MeterRegistry> meterRegistryCustomizer(MeterRegistry meterRegistry) { //全局标明所有指标归属 return registry -> meterRegistry.config().commonTags("application", springApplicationName); }}


    http耗时:

    假如我们现在想统计一下:收到请求到返回响应结果这段时间的后端逻辑处理时间,那我们可以定义一个工具类:

    @Slf4j@Componentpublic class MicroMeterTool {    @Autowired    private MeterRegistry meterRegistry;
    //存储某个URI对应的耗时 private final ConcurrentHashMap<String, AtomicLong> appAccessTimeGaugeMap = new ConcurrentHashMap<>();
    /** * 统计某个URI的业务耗时 * @param uri 做一个AOP拦截请求 获取 URI * @param time output:System.currentTimeMillis()-input:System.currentTimeMillis() */ public void appAccessTimeGauge(String uri,Long time) { log.debug("appAccessTimeGauge uri:{} time:{}",uri,time);
    //自定义指标名称 String uk ="http_req_cost_time"; AtomicLong val = appAccessTimeGaugeMap.get(uri); if (null != val) { val.set(time); return; } try { val = new AtomicLong(time); meterRegistry.gauge(uk, Arrays.asList(Tag.of("uri",uri)),val,AtomicLong::get); appAccessTimeGaugeMap.put(uri, val); } catch (Exception e) { log.error("metrics appAccessTimeGauge error,key:{},method:{},time:{}", uk, val, e); } }}

    上述代码块在业务需要的调用调用一下就好,后续准备专门写一篇AOP切面拦截http请求耗时,这里就当是定义出指标,后续会集成。以下是自定义指标展示图:



    JVM:

    与http耗时一样,我们同样可以写2个方法用于写入JVM实例数大小和字节占用大小。二者思路一致:都用一个Map容器来装类对应实例数或字节码,然后不断忘容器中写,需要注意的是meterRegistry会与这个Map关联,Map中key对应的value变更后自动会在Prometheus中刷新,所以注入meterRegistry只需一次,无需每次执行meterRegistry.gauge(..)。

     private final ConcurrentHashMap<String, AtomicLong> jvmByteGaugeMap = new ConcurrentHashMap<>();    public void jvmUsageByteGauge(String key,Long bytes) {        log.debug("jvmUsageByteGauge key:{} bytes:{}",key,bytes);        String uk ="app_jvm_bytes";        AtomicLong val = jvmByteGaugeMap.get(key);        if (null != val) {            val.set(bytes);            return;        }      //以下代码每个类之跑一次,后续自动与Map关联        try {            val = new AtomicLong(bytes);            meterRegistry.gauge(uk, Arrays.asList(Tag.of("name", "jvm_usage"), Tag.of("type", "bytes"),Tag.of("class_name",key)),val,AtomicLong::get);            jvmByteGaugeMap.put(key, val);        } catch (Exception e) {            log.error("metrics jvmByteGaugeMap error,key:{},method:{},time:{}", uk, val, e);        }    }    private final ConcurrentHashMap<String, AtomicLong> jvmInstanceGaugeMap = new ConcurrentHashMap<>();    public void jvmUsageInstanceGauge(String key,Long instances) {        log.debug("jvmUsageInstanceGauge key:{} instances:{}",key,instances);
    String uk ="app_jvm_instances"; AtomicLong val = jvmInstanceGaugeMap.get(key); if (null != val) { val.set(instances); return; } try { val = new AtomicLong(instances); meterRegistry.gauge(uk, Arrays.asList(Tag.of("name", "jvm_usage"), Tag.of("type", "instances"),Tag.of("class_name",key)),val,AtomicLong::get); jvmInstanceGaugeMap.put(key, val); } catch (Exception e) { log.error("metrics jvmInstanceGaugeMap error,key:{},method:{},time:{}", uk, val, e); } }



    [I、[J... 会在后续文章中给出解释


    一些注入Prometheus后,如果程序逻辑跑完会被JVM回收,为尽量确保开发同学每次打开大盘时看到的数据有参考意义,需要在注入后再补一个清除逻辑:

    public void clearHistoryKey(Collection<String> keys,String key){    Collection<Meter> meters = meterRegistry.get(key).meters();    for (Meter meter : meters) {        Meter.Id id = meter.getId();        String className = id.getTag("class_name");        if (!StringUtils.isBlank(className) &&  !keys.contains(className)){            meterRegistry.remove(meter);            jvmInstanceGaugeMap.remove(className);            jvmByteGaugeMap.remove(className);            log.debug("clearHistoryKey key:{} clzName:{}",key,className);        }    }}


  7. 效果展示

    需要说明的是:相关指标注入到Prometheus后与运维同学沟通,对方会访问每个服务对外提供的actuator/promethues接口,将拿到的数据解析后再与其它工具集成展示 (目前我们是Grafana),开发同学关注时只需要登录上去瞄一眼就行。从此相看,两生欢喜



  8. 动态配置

    Promethues多久采集一次数据(默认 15s)这些是可以通过配置修改的,有兴趣的可以自行

    试试,这里面:https://prometheus.io/docs/prometheus/latest/getting_started/ 有详细的说明。

    多数情况下配置文件变更后,需要更新配置到程序内存里,有两种方式,第一种简单粗暴,就是重启第二种是动态更新的方式(nginx也有)。如何实现动态的更新Prometheus配置,步骤如下:

    step1.启动Prometheus的时候带上启动参数:--web.enable-lifecycleprometheus.exe --config.file=/xxx/prometheus.yml --web.enable-lifecyclestep2.更新Prometheus配置 step3.更新完配置后,通过Post请求的方式,动态更新配置:curl -v --request POST 'http://localhost:9090/-/reload'


    P.S:自定义指标部分http耗时JVM字节码都暂时只列出了代码块,考虑到二者是完全独立的功能,所以业务调用侧的代码会分别在后续篇章再给出。


    划水专用:

    https://prometheus.io/

    https://zhuanlan.zhihu.com/p/488898953?utm_id=0

    https://blog.csdn.net/gaowenhui2008/article/details/131598092

    https://blog.csdn.net/jilo88/article/details/131424717


    有问题欢迎指出,听说吐槽与关注更配哦


    书山有路勤为径,学海无涯苦作舟。

    ——韩愈《劝学》

晚霞程序员
一位需要不断学习的30+程序员……