Prometheus集成实战

346 阅读12分钟

背景

当产线频繁地出问题,员工在群里疯狂吐槽时,团队领导再也忍不住了:为什么每次都是客户暴露问题,我们的监控在哪里?

这里简单介绍下项目背景,主要是开发维护公司 CRM 系统,员工可以在管理系统上服务客户,但是因为行业原因+历史遗留,有这几大痛点:

  1. 架构混乱:

系统架构迭代了好几个版本,现在还是多系统并行。从单体应用,再到微服务架构,再到领域架构,没有一次架构升级是干净的(我也不知道为什么这么叫微服务架构、淋雨架构,实际上应用边界根本就不清晰,更不用说基本的表隔离);

  1. SQL 复杂:

复杂 SQL 很多,现在产线上有些低频功能页面查不出数据或者在某些条件下才能查到数据;(因为高频页面出问题都第一时间解决,不然容易引起群愤🥹)

其实系统本身并不复杂,数据都是从上游采集来的,读场景超过 90%,架不住业务逻辑复杂,经常需要多表关联,随手一写,SQL 就是几十上百行,一度感觉自己是 SQL 工程师,面向 SQL 编程。

Prometheus 介绍

这里普米官网介绍得很详细,Prometheus 是一款开源的系统监控和告警工具,最初由 SoundCloud 开发并于 2016 年加入 Cloud Native Computing Foundation(CNCF) ,成为继 Kubernetes 之后的第二个毕业项目。它专为云原生环境设计,尤其擅长监控动态的微服务架构和容器化应用(如 Kubernetes)。

在选择普米之前,也对比了市面常见的几种监控工具,主要区别如下:

工具PrometheusZabbixNagiosGrafana
类型开源监控工具开源监控工具开源监控工具开源可视化工具
核心功能时间序列数据采集、存储、查询和告警基础设施监控、网络监控、应用监控基础设施监控、告警治理数据可视化、仪表盘构建
数据模型基于标签多维度数据模型结构化数据模型简单数据模型支持多种数据源
告警功能支持基于 PromQL 的告警规则,告警发送到 Alertmanager内置强大的告警功能内置告警功能依赖数据源的告警功能(如 Prometheus)
适用场景云原生、容器化环境、微服务架构传统基础设施监控、网络监控传统基础设施监控,针对系统服务和资源状态及程序可用性,如磁盘空间、CPU 负载等数据可视化,适用于多种监控工具的数据展示
拓展性高(支持联邦集群、远程存储)中(支持分布式监控)低(功能较为基础)高(支持多种数据源和插件)
优点- 高效的时间序列数据库 - 强大的多维数据模型 - 适合动态环境 - 开源免费- 功能全面 - 支持多种协议 - 社区活跃 - 开源免费- 简单易用 - 社区支持广泛 - 开源免费- 强大的可视化功能 - 支持多种数据源 - 开源免费
缺点- 长期存储需要额外工具(如 Thanos) - 学习曲线较陡- 配置复杂 - 不适合大规模动态环境- 功能较为基础 - 不适合现代云原生环境- 仅提供可视化功能,需依赖其他监控工具

总结

  1. Prometheus:适合云原生和动态环境,具有高效的时间序列数据库和强大的多维数据模型,但长期存储需要额外工具。
  2. Zabbix:功能全面,适合传统基础设施监控,但配置复杂,不适合大规模动态环境。
  3. Nagios:简单易用,适合基础监控需求,但功能较为单一,不适合现代云原生环境。
  4. Grafana:专注于数据可视化,支持多种数据源,通常与其他监控工具(如 Prometheus)结合使用。

考虑到实际的开发成本和容器化的趋势,Prometheus 将是一个更好的选择。

核心组件

Prometheus Server

Prometheus Server是Prometheus组件中的核心部分,负责实现对监控数据的获取,存储以及查询。 Prometheus Server可以通过静态配置管理监控目标,也可以配合使用Service Discovery的方式动态管理监控目标,并从这些监控目标中获取数据。其次Prometheus Server需要对采集到的监控数据进行存储,Prometheus Server本身就是一个时序数据库,将采集到的监控数据按照时间序列的方式存储在本地磁盘当中。最后Prometheus Server对外提供了自定义的PromQL语言,实现对数据的查询以及分析。

Prometheus Server内置的Express Browser UI,通过这个UI可以直接通过PromQL实现数据的查询以及可视化。

Prometheus Server的联邦集群能力可以使其从其他的Prometheus Server实例中获取数据,因此在大规模监控的情况下,可以通过联邦集群以及功能分区的方式对Prometheus Server进行扩展。

Exporters

Exporter将监控数据采集的端点通过HTTP服务的形式暴露给Prometheus Server,Prometheus Server通过访问该Exporter提供的Endpoint端点,即可获取到需要采集的监控数据。

AlertManager
在Prometheus Server中支持基于PromQL创建告警规则,如果满足PromQL定义的规则,则会产生一条告警,而告警的后续处理流程则由AlertManager进行管理。在AlertManager中我们可以与邮件,Slack等等内置的通知方式进行集成,也可以通过Webhook自定义告警处理方式。AlertManager即Prometheus体系中的告警处理中心。

PushGateway

由于Prometheus数据采集基于Pull模型进行设计,因此在网络环境的配置上必须要让Prometheus Server能够直接与Exporter进行通信。 当这种网络需求无法直接满足时,就可以利用PushGateway来进行中转。可以通过PushGateway将内部网络的监控数据主动Push到Gateway当中。而Prometheus Server则可以采用同样Pull的方式从PushGateway中获取到监控数据。

指标类型

所有采集的监控数据均以指标(metric)的形式保存在内置的时间序列数据库当中(TSDB)。所有的样本除了基本的指标名称以外,还包含一组用于描述该样本特征的标签。

如下所示:

http_request_status{code='200',content_path='/api/path', environment='produment'} => [value1@timestamp1,value2@timestamp2...]

http_request_status{code='200',content_path='/api/path2', environment='produment'} => [value1@timestamp1,value2@timestamp2...]

每一条时间序列由指标名称(Metrics Name)以及一组标签(Labels)唯一标识。每条时间序列按照时间的先后顺序存储一系列的样本值。

表示维度的标签可能来源于你的监控对象的状态,比如code=404或者content_path=/api/path。也可能来源于的你的环境定义,比如environment=produment。基于这些Labels我们可以方便地对监控数据进行聚合,过滤,裁剪。

Prometheus定义了4中不同的指标类型(metric type):Counter(计数器)、Gauge(仪表盘)、Histogram(直方图)、Summary(摘要)。

指标说明

Prometheus通过指标名称(metrics name)以及对应的一组标签(labelset)唯一定义一条时间序列。指标名称反映了监控样本的基本标识,而label则在这个基本特征上为采集到的数据提供了多种特征维度。用户可以基于这些特征维度过滤,聚合,统计从而产生新的计算后的一条时间序列。

每个metrics数据都包含几个部分:指标名称标签采样数据。举个例子,需要监控应用的线程池指标,包括核心数量、最大数量、当前工作线程数量、阻塞队列长度等等。并且不同应用有同样的监控需求。最终生成的指标数据如下:

# HELP thread_pool_core_size help
# TYPEthread_pool_core_size gauge
thread_pool_core_size{application="app1",} 20.0

数据结构类似:thread_pool_core_size[ 指标名称 ]{application[ 标签名称 ]="app1[ 标签属性 ]",} 20.0[ 采样数据 ]

说明:

  1. 如果是同样的监控指标,可以使用同一个指标名称,方便后续的数据统计;
  2. 一个指标名称可以带有多个标签,数据结构如下:
    1. [ 指标名称 ]{label1[ 标签名称 ]="l1[ 标签属性 ]",label2[ 标签名称 ]="l2[ 标签属性 ]"} 20.0[ 采样数据 ]
  1. 由于很多指标都是公共的,所以最佳实践是对于同一类型的指标,可以设置同样的指标名称,根据标签区分不同的应用;

监控接入

以上内容是普米的一些基础,从官网也都能查得到,我们本次主要是做普米接入端的开发,普米服务端由公司其他团队维护。

先整体梳理下现在的系统架构,前面说到现在系统存在多架构并行的情况,所以在实际接入之前,先拆解下问题:

  1. 代码需要遵循不同架构的设计规范;
  2. 监控指标都是类似的,不同系统可能有自己的定制化监控;
  3. 需要考虑代码复用性,尽量降低接入方的对接成本;

PS:这活不是应该让架构团队来做更合适吗?但是不得不说,相比之前待过的大厂,非互联网企业的基建真的差,这里就不过多吐槽了,有机会的话后续专门出个文章来做下对比。

代码设计

  1. 架构上新增一个指标加入组件,接入方不需要编写代码,引入组件即可实现普米监控接入;
  2. 代码设计上定义了一个公共的指标接口,抽象类实现基本的指标数据封装,详细的指标数据由各实现类完成,同时添加条件注解,允许接入方根据需要,注入对应的实现类。

接入步骤

以下是详细的代码设计

指标公共包

使用 Spring Boot starter 特性,新增模块prometheus-spring-boot-starter,用于其他服务依赖。

步骤一 依赖引入
<dependency>
  <groupId>io.prometheus</groupId>
  <artifactId>simpleclient</artifactId>
  <version>0.16.0</version>
</dependency>

<dependency>
  <groupId>io.prometheus</groupId>
  <artifactId>simpleclient_httpserver</artifactId>
  <version>0.16.0</version>
</dependency>

这两个依赖是最基础的包,simpleclient 提供核心的指标监控类,simpleclient_httpserver 对外暴露 http 端口,如果需要更多的监控,如 JVM 等等,需要引入对应的包。

步骤二 指标类开发
  1. 新增一个获取指标的公共接口:IMetric
public interface IMetric {

    /**
     * 业务域标签名称
     */
    String BUSINESS_DOMAIN_LABEL = "label";

    /**
     * 标签key
     */
    String APPLICATION_LABEL = "application";

    /**
     * 业务域标签值
     */
    String BUSINESS_DOMAIN = "domain";

    /**
     * 标签value
     */
    String APPLICATION_NAME = "";

    String HELP = "help";

    /**
     * 指标数据
     *
     * @return
     */
    List<Collector.MetricFamilySamples> getMetrics();

    /**
     * 标签组key
     */
    List<String> getLabelNames();

    /**
     * 标签组value
     */
    List<String> getLabelValues();
}

2. 抽象类继承接口:

public abstract class AbstractMetric implements IMetric {

    @Autowired
    private Environment env;

    @Override
    public List<String> getLabelNames() {
        return Arrays.asList(BUSINESS_DOMAIN_LABEL, APPLICATION_LABEL);
    }

    @Override
    public List<String> getLabelValues() {
        String applicationName = Objects.requireNonNull(env.getProperty("prometheus.metrics.labels.application")) ;
        return Arrays.asList(BUSINESS_DOMAIN, applicationName);
    }

    /**
     * 添加指标
     *
     * @param metricFamilySamples
     * @param name
     * @param value
     */
    protected void addMetric(List<Collector.MetricFamilySamples> metricFamilySamples, String name, double value) {
        Collector.MetricFamilySamples.Sample sample = MetricUtil.getMultiLabelMetricSample(name, getLabelNames(), getLabelValues(), value);
        metricFamilySamples.add(new Collector.MetricFamilySamples(name, Collector.Type.GAUGE, HELP, Arrays.asList(sample)));
    }

}

3. 监控类实现抽象类,返回指标集合List<Collector.MetricFamilySamples>,这里以 Tomcat 线程池为例:

@ConditionalOnProperty(name = "prometheus.metrics.threadpool.http.enable", havingValue = "true")
public class TomcatMetric extends AbstractMetric {

    private static final String CORE_POOL_SIZE = "tomcat_thread_pool_core_size";
    private static final String MAX_POOL_SIZE = "tomcat_thread_pool_max_size";
    private static final String ACTIVE_COUNT = "tomcat_thread_pool_active_count";
    private static final String QUEUE_SIZE = "tomcat_thread_pool_queue_size";

    @Autowired
    private WebServerApplicationContext webServerApplicationContext;

    @Override
    public List<Collector.MetricFamilySamples> getMetrics() {

        List<Collector.MetricFamilySamples> metricFamilySamples = new ArrayList<>();
        ThreadMetricIndex httpThreadPool = getRpcThreadPool();

        addMetric(metricFamilySamples, CORE_POOL_SIZE, httpThreadPool.getCorePoolSize());
        addMetric(metricFamilySamples, MAX_POOL_SIZE, httpThreadPool.getMaxPoolSize());
        addMetric(metricFamilySamples, ACTIVE_COUNT, httpThreadPool.getActiveCount());
        addMetric(metricFamilySamples, QUEUE_SIZE, httpThreadPool.getQueueSize());
        return metricFamilySamples;
    }

    /**
     * 获取http连接池指标
     *
     * @return
     */
    private ThreadMetricIndex getRpcThreadPool() {
        ThreadMetricIndex threadMetricIndex = new ThreadMetricIndex();

        Tomcat tomcat = ((TomcatWebServer) webServerApplicationContext.getWebServer()).getTomcat();
        if (tomcat == null || tomcat.getService().findConnectors().length == 0) {
            return threadMetricIndex;
        }
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) tomcat
                .getConnector()
                .getProtocolHandler()
                .getExecutor();
        threadMetricIndex.setCorePoolSize(threadPoolExecutor.getCorePoolSize());
        threadMetricIndex.setMaxPoolSize(threadPoolExecutor.getMaximumPoolSize());
        threadMetricIndex.setActiveCount(threadPoolExecutor.getActiveCount());
        threadMetricIndex.setQueueSize(threadPoolExecutor.getQueue().size());

        return threadMetricIndex;
    }
}

4. 创建收集器,继承 Collector:

public class CustomerCollector extends Collector {

    private static final Logger logger = LoggerFactory.getLogger(CustomerCollector.class);

    @Override
    public List<MetricFamilySamples> collect() {

        List<MetricFamilySamples> mfs = new ArrayList<>();
        try {
            Map<String, IMetric> beansOfType = ContextUtil.getBeansOfType(IMetric.class);
            beansOfType.forEach((s, iMetric) -> mfs.addAll(iMetric.getMetrics()));
        } catch (Exception e) {
            logger.error("监控接口发生异常,异常原因:", e);
        }
        return mfs;
    }

}

5. 对外暴露接口:

@ConditionalOnProperty(name = "prometheus.http.server.port")
public class PrometheusConfig {

    @Autowired
    private Environment env;

    @Primary
    @Bean
    public CustomerCollector customerCollector() throws IOException {
        CustomerCollector customerCollector = new CustomerCollector();
        customerCollector.register();

        int servPort = Integer.parseInt(Objects.requireNonNull(env.getProperty("prometheus.http.server.port"))) ;
        new HTTPServer(servPort);
        return customerCollector;

    }
}
步骤三 条件注入

完成以上步骤,我们发现对于不同应用,有不同的接入需求,所以我们根据条件注解,来选择性注入监控类。

  1. 在 META-INF 下创建 spring.factories 文件,加入 Spring 容器:

  1. 在配置文件添加配置,允许接入方自定义监控接入:
prometheus.http.server.port=1234
prometheus.metrics.labels.application=application-name
prometheus.metrics.datasource.enable=false
prometheus.metrics.threadpool.rpc.enable=false
prometheus.metrics.threadpool.http.enable=true

如上配置,代表接入方对外暴露的端口是 1234,并且只需要 http 连接池的监控,其他监控不生效。

监控接入方

以 app1 为例,其他应用类似。

步骤一 引入依赖

步骤二 添加核心配置
prometheus.http.server.port=1234
prometheus.metrics.labels.application=app1
prometheus.metrics.datasource.enable=true
prometheus.metrics.threadpool.rpc.enable=true
prometheus.metrics.threadpool.http.enable=true

配置说明:

  1. prometheus.http.server.port
对外暴露的 http 端口;
  1. prometheus.metrics.labels.application
应用名称,用于配置指标标签;
  1. prometheus.metrics.datasource.enable
是否开启数据库监控
  1. prometheus.metrics.threadpool.rpc.enable
是否开启 rpc 连接池监控
  1. prometheus.metrics.threadpool.http.enable
是否开启 http 连接池监控

监控页

最终结合 Grafana,效果图如下:

总结

根据普米官方的说法,我们甚至可以在不使用Prometheus的情况下,采用Prometheus的client library来让你的应用程序支持监控数据采集,具备很强的开放性。

好了,以上就是应用接入普米监控的基本步骤,接入并不难,重点是在接入过程中,思考哪种接入方案对系统是最有效才是最重要的。如果不考虑系统架构,每个需要接入监控的应用各自完成普米接入,也能实现效果,但是技术债就是这么堆起来的。从“程序员”到“工程师”的蜕变,本质是从“解决问题”到“定义问题” 的思维升级。真正的匠心,在于能在业务压力与技术理想之间找到平衡,用简洁的代码和合理的架构为系统赋予长期生命力。保持对技术的好奇心,同时脚踏实地地优化每一行代码,这便是工程师的修行之路。

我是欧达克,祝你幸福。