【Prometheus指标监控】基于VictoriaMetrics+Prometheus-Client+Nacos的指标采集方案

904 阅读6分钟

一、整体架构

我们需要在生成环境为业务应用提供一套 Prometheus 指标自助埋点的方案,经过评估,我们采用了下面的架构方案。

下面会逐个分析各个组件。

二、采集层

1.1 指标采集模式选择 pull 还是 push?

pullpush
开发成本低。官方SDK默认的方式,非常成熟稳定,可直接使用需要基于原有的SDK(simpleclient_pushgateway)做改造
target管理1.可以在控制台查看client(target),方便管理接入情况 2.由服务端控制拉取哪些client(target)的指标、拉取的频率 3.自动检查target的可用性,注册服务中未能拉取会记录为未收集状态,能发现哪些client有问题无法管理,不可控
服务发现1.依赖服务发现机制,需要搭建注册中心1.不需要注册中心,我们可以用vmagent替代pushgateway,接收client push的指标,然后存到vmstore
结论推荐选择该方式,可以管理target架构更简单。但是需要改造prometheus sdk,且无法管理client

我们在线上环境采用的是 pull 模式,因为对于监控平台而言,target 端的管理是非常重要的。

1.2 pull 模式

pull 模式需要用到注册中心

consul 或 nacos 是流行的 prometheus 注册中心,他们的对比如下:

nacosconsul
政策限制无限制背后公司跟中国政策有冲突,不建议使用
功能拉取服务时,没法指定各类传参进行过滤拉取服务时,支持多种传参进行过滤
官方文档docs.victoriametrics.com/sd_configs/… github.com/alibaba/nac…docs.victoriametrics.com/sd_configs/…
结论选择该方式,更可控,且符合政策要求

也可以参考这个方案:github 上已经有人开源了一个对应的 nacos 作为注册中心、提供 prometheus consul api 的 adapter,我们可以直接拿来使用,详见:github.com/weixiaohui-… adapter 的作用是将 Nacos 伪装成 Consul,让 Prometheus 能够自动发现 Nacos 中的服务,而不用每个微服务单独配置。用户需要引入一个 jar 包,并在 Prometheus 配置中使用 consul_sd_configs 指向 Nacos 的地址。但是对 spring cloud、spring boot 版本有要求,我们没有采用这个方案。

1.3 metric-client SDK

基于 prometheus-client 封装一个 metric-client SDK,通过该 SDK 可以自动注册 client 到 nacos。

1.4 VictoriaMetrics vmagent

我们采用 VictoriaMetrics 作为 prometheus 的集群方案。

vmagentVictoriaMetrics 提供的一个轻量级的 Prometheus 采集代理,用于从各种数据源(如 Prometheus、OpenTelemetry、Telegraf 等)采集监控数据,并将这些数据发送到 VictoriaMetrics 集群进行存储和查询。

vmagent相比 prometheus 采集器具有下面的优势:

  • 资源效率更高:CPU、内存、磁盘 IO 和网络带宽占用显著低于 Prometheus。
  • 数据缓冲与重试:在远程存储不可用时,vmagent 会将数据暂存本地磁盘,待恢复后重试,避免数据丢失。

三、nacos 注册中心

当选择 nacos 作为注册中心,选择版本什么版本?

版本2.0.42.4.2
协议不支持prometheus http sd协议已支持prometheus http sd协议,prometheus可以直接配置nacos http接口地址
接入成本通过开发nacos proxy服务,提供prometheus http sd协议的http接口给prometheus使用1.方案一、需要部署一套新版本的nacos困难:部署需要耗费时间、且后期多维护一套集群 2.方案二、现有nacos升级2.4.2版本困难:有一定风险,可能有版本不兼容的情况。
结论选择该方式,更可控。后期nacos升级版本后,可以去除sd http接口,直接对接新版本nacos

我们线上使用的 nacos 2.0.4 版本,但是短期内又不想升级到 2.4.2 版本 nacos,有一定风险,可能有版本不兼容的情况;也不想部署一套新的 2.4.2 版本的 nacos,增加机器和维护成本。

所以,我们还是使用现有的 2.0.4 版本集群,然后将现有的一个 Java 的监控应用 A,集成作为 nacos proxy 服务,提供 prometheus http sd 协议的 http 接口给 prometheus 使用。

下面是 nacos sd 协议 proxy 接口的示例代码:

@Slf4j
@RestController
@RequestMapping("api/prometheusSD")
public class PrometheusServiceDiscoveryController {
private NamingService namingService;

    @PostConstruct
    public void init() throws Exception {
        try {
            log.info("Starting to create Nacos NamingService client.");
            createNacosNamingServiceClient();
            log.info("Nacos NamingService client created successfully.");
        } catch (Exception e) {
            log.error("Error creating Nacos NamingService client.", e);
            // 如果不忽略报错则需要抛异常
            log.error("Nacos client creation error and will throw an exception.");
            throw e;
        }
    }
    
    private void createNacosNamingServiceClient() throws Exception {
        // 初始化 Nacos 配置
        Properties properties = new Properties();
    
        // 服务地址
        properties.put("serverAddr", MetricClientConfig.getNacosServerAddr());
        // 指定命名空间
        properties.put("namespace", MetricClientConfig.getNacosNamespace());
        // Nacos 用户名
        properties.put("username", MetricClientConfig.getNacosUsername());
        // Nacos 密码
        properties.put("password", MetricClientConfig.getNacosDecryptPassword());
    
        this.namingService = NacosFactory.createNamingService(properties);
    }
    
    /**
     * Prometheus Http SD(服务发现) 接口
     * 参考:
     * https://docs.victoriametrics.com/sd_configs/#http_sd_configs
     * https://prometheus.io/docs/prometheus/latest/http_sd/
     * @param namespace
     * @return 注意:返回结果跟Nacos官方sd实现有差异
     */
    @GetMapping("/namespace/{namespace}")
    public ResponseEntity<?> prometheusSdNamespace(@PathVariable String namespace) {
        // 如果应用启动时namingService创建失败,namingService可能是null
        if (namingService == null) {
            String status = "error";
            String message = "nacos namingService is null.";
            return getPrometheusSdErrorResponseEntity(status, message);
        }
    
        List<Map<String, Object>> serviceResult = Lists.newArrayList();
        int pageSize = 100;
        int maxPageNo = 1000;
        try {
            // 分页获取服务名称
            // 使用for循环进行分页处理
            for (int pageNo = 1; pageNo <= maxPageNo ; pageNo++) {
                ListView<String> serviceList = namingService.getServicesOfServer(pageNo, pageSize);
                // 检查是否有服务数据,没有则退出
                if (serviceList.getData().isEmpty()) {
                    break;
                }
    
                for (String serviceName : serviceList.getData()) {
                    // 获取当前页面的实例列表
                    List<Instance> instances = namingService.getAllInstances(serviceName, false);
    
                    // 将实例转化为 Prometheus SD 格式
                    List<String> targets = Lists.newArrayList();
                    Map<String, Object> serviceResultItem = Maps.newLinkedHashMap();
                    Map<String, String> labels = Maps.newLinkedHashMap();
                    labels.put("_service", serviceName);
                    labels.put("_namespace", namespace);
    
                    for (Instance instance : instances) {
                        targets.add(instance.getIp() + ":" + instance.getPort());
                    }
                    serviceResultItem.put("targets", targets);
                    serviceResultItem.put("labels", labels);
                    serviceResult.add(serviceResultItem);
                }
    
                // 如果一页的数据小于分页大小,说明后面没有数据了,可以退出循环,这样可以少调用一下获取service的接口
                if (serviceList.getData().size() < pageSize) {
                    break;
                }
            }
    
            return ResponseEntity.ok(serviceResult);
        } catch (Exception e) {
            String status = "error";
            String message = "Failed to fetch services: " + e.getMessage();
    
            return getPrometheusSdErrorResponseEntity(status, message);
        }
    }
    
    @NotNull
    private ResponseEntity<Map<String, String>> getPrometheusSdErrorResponseEntity(String status, String message) {
        Map<String, String> errorResp = Maps.newLinkedHashMap();
        // 捕获异常并设置错误的响应结果,返回 HTTP 500,不要返回200,
        errorResp.put("status", status);
        errorResp.put("message", message);
    
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResp);
    }
}

四、存储层

我们选用 VictoriaMetrics 的 vminsert、vmstorage 和 vmselect 组件,构建 Prometheus 集群的存储解决方案。

组件说明:

  1. vminsert:

    1. 功能:负责数据插入,接收来自外部的数据并进行写入操作。
    2. 特点:使用一致性哈希算法将数据分散到多个 vmstorage 节点上,确保数据均匀分布。
    3. 扩展性:支持水平扩展,通过增加节点来提升写入能力。
  2. vmstorage:

    1. 功能:负责数据存储和代理功能,提供数据持久化存储。
    2. 特点:支持数据压缩,减少存储空间需求;支持多租户(命名空间)环境,确保每个租户的数据隔离。
    3. 扩展性:支持横向扩展,通过增加节点来提升存储能力。
  3. vmselect:

    1. 功能:负责数据查询,根据输入的查询条件从 vmstorage 中获取数据。

    2. 特点:支持全局查询视图,可以处理来自多个 vmstorage 实例的数据,提供统一的查询接口。

    3. 扩展性:支持水平扩展,通过增加节点来提升查询能力。

VictoriaMetrics 相关内容后面再发文再写,如需要了解,可以参考这些文章:

五、展示层

使用 grafana 展示指标,生产环境的效果图不方便展示。下面展示一下,VictoriaMetrics cluster 自监控的指标看板。