Prometheus 统计月度流量

19次阅读
没有评论

共计 6683 个字符,预计需要花费 17 分钟才能阅读完成。

背景

当前手头上有许多服务器,部分服务器是固定流量的套餐,例如每月200GB流量。

在现有基建中,使用 node_exporter 采集服务器流量指标,VictoriaMetrics 存储指标,于 Grafana 中绘图,但是发现想要绘制指定自然周期内的流量数据很困难,只能绘制一个滚动的时间窗口内的流量数据。

原因如下:

1. Prometheus 的时间序列数据模型

Prometheus 采用的是时间序列数据库(TSDB)模型,其核心设计理念是存储和查询连续的时间序列数据点,而非聚合的时间段统计。它主要针对以下场景优化:

  • 实时监控:关注系统当前状态和近期趋势
  • 告警触发:基于最近数据点的阈值判断
  • 趋势分析:观察指标随时间的变化模式

2. 查询模型的时间窗口机制

Prometheus 的 PromQL 查询语言设计为滑动时间窗口模型(Sliding Window Model) ,而非固定时间边界模型(Fixed Time Boundary Model) 。这意味着:

  • 查询总是相对于查询执行时间点向后看一个时间窗口
  • 没有内置的”按日历日期聚合”功能
  • 函数如 rate()​, increase()​ 等都基于滑动窗口计算

3. Grafana 与 Prometheus 的交互机制

当 Grafana 查询 Prometheus 时,它使用的是 Prometheus HTTP API 中的两个主要端点:

  • /api/v1/query​ :在单一时间点执行查询
  • /api/v1/query_range​ :在时间范围内执行查询,返回一系列数据点

Grafana 传递的是相对时间范围参数,而非精确的日历边界。即使设置了精确的开始和结束时间,底层查询机制仍然是:

  1. 将时间范围分割成多个步长(step)
  2. 在每个步长上执行查询
  3. 返回时间序列数据点集合

4. 计数器指标的特性

网络流量指标如 node_network_receive_bytes_total​ 是单调递增计数器(Monotonically Increasing Counter) ,而非直接的速率或聚合值:

  • 需要通过差值计算才能获得特定时间段的流量
  • 计数器可能因服务重启而重置(Counter Reset)
  • 没有内置机制在每个自然日开始时自动”归零”

综上,用 node_exporter 直接采集的指标不能满足需要。这种情况下需要依赖外部脚本统计自然周期内流量,再将其输出到 Prometheus 中。

实践

数据采集有很多种方式,首先应该厘清我们所需要的数据模型。

由于服务器是按月统计流量,那么指标应当返回当前时刻在计费周期(月)内消耗的流量。

metrics存储则有两种方式:

vnStat 采集指标

vnStat 是一个轻量级的网络流量监控工具,它可以记录网络接口的流量并生成报告。

# Debian/Ubuntu
sudo apt install vnstat

# CentOS/RHEL
sudo yum install vnstat

命令示例:

# 启动服务
sudo systemctl start vnstat
sudo systemctl enable vnstat

# 查看所有接口的流量统计
vnstat

# 查看每日流量统计
vnstat -d

# 查看每月流量统计
vnstat -m

# 查看特定接口的流量
vnstat -i eth0

# 以json格式查看特定接口的流量
vnstat -i $INTERFACE --json

node_exporter 采集指标

使用计划任务定时运行 Shell 脚本即可,脚本如下:

#!/bin/bash

# 设置网卡名称
INTERFACE="ens3"
# 设置流量限制(单位:GB )
LIMIT=150

# 检查 vnstat 和 jq 是否已安装
if ! command -v vnstat &> /dev/null; then
echo "vnstat 未安装,请安装后重试。"
exit 1
fi

if ! command -v jq &> /dev/null; then
echo "jq 未安装,请安装后重试。"
exit 1
fi

# 检查 bc 是否已安装
if ! command -v bc &> /dev/null; then
echo "bc 未安装,请安装后重试。"
exit 1
fi

cat /dev/null > /var/lib/node_exporter/ecs_network_month_total_gb.prom.
cat /dev/null > /var/lib/node_exporter/ecs_network_month_total_gb.prom

# 获取当前流量(单位:bytes )
VNSTAT_JSON=$(vnstat -i $INTERFACE --json)
#echo "vnstat JSON 输出: $VNSTAT_JSON"

# 使用 jq 解析 JSON 数据获取接收和发送的流量(单位:bytes )
RX=$(echo $VNSTAT_JSON | jq -r '.interfaces[0].traffic.total.rx')
TX=$(echo $VNSTAT_JSON | jq -r '.interfaces[0].traffic.total.tx')

# 输出解析结果
#echo "接收流量 (RX): $RX bytes"
#echo "发送流量 (TX): $TX bytes"

# 检查 RX 和 TX 是否为有效的数字
if ! [[ $RX =~ ^[0-9]+$ ]] || ! [[ $TX =~ ^[0-9]+$ ]]; then
echo "RX 或 TX 不是有效的数字。"
exit 1
fi

# 计算总流量(单位:GB )
# 判断 RX 和 TX 中较大的值,交由node_exporter采集
cat > /var/lib/node_exporter/ecs_network_month_total_gb.prom <<EOF
# HELP ecs_network_month_total_gb
# TYPE ecs_network_month_total_gb gauge
EOF

if [ "$RX" -gt "$TX" ]; then
    TOTAL=$(awk 'BEGIN{printf "%.3f\n", '"$RX"' / 1024 / 1024 / 1024}')
    echo ecs_network_month_total_gb $TOTAL >> /var/lib/node_exporter/ecs_network_month_total_gb.prom
else
    TOTAL=$(awk 'BEGIN{printf "%.3f\n", '"$TX"' / 1024 / 1024 / 1024}')
    echo ecs_network_month_total_gb $TOTAL >> /var/lib/node_exporter/ecs_network_month_total_gb.prom
fi

自建 exporter 采集指标

可以使用 Systemd 管理脚本持续运行,Python 脚本如下:

#!/usr/bin/env python3
import json
import subprocess
from http.server import HTTPServer, BaseHTTPRequestHandler
import logging

# 配置参数
INTERFACE = "ens3"
LIMIT = 150  # GB
PORT = 9088  # Prometheus抓取端口

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('vnstat-exporter')

class VnstatCollector:
    def __init__(self, interface):
        self.interface = interface

    def check_dependencies(self):
        """检查依赖项是否安装"""
        dependencies = ['vnstat']
        for dep in dependencies:
            try:
                subprocess.run(['which', dep], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            except subprocess.CalledProcessError:
                logger.error(f"{dep} 未安装,请安装后重试")
                return False
        return True

    def collect_data(self):
        """收集vnstat数据"""
        try:
            # 获取vnstat JSON数据
            result = subprocess.run(
                ['vnstat', '-i', self.interface, '--json'],
                capture_output=True, text=True, check=True
            )
            vnstat_json = json.loads(result.stdout)

            # 解析数据
            rx = vnstat_json['interfaces'][0]['traffic']['total']['rx']
            tx = vnstat_json['interfaces'][0]['traffic']['total']['tx']

            # 转换为GB
            rx_gb = rx / (1024 * 1024 * 1024)
            tx_gb = tx / (1024 * 1024 * 1024)

            # 取较大值
            total_gb = max(rx_gb, tx_gb)

            data = {
                'rx_bytes': rx,
                'tx_bytes': tx,
                'rx_gb': rx_gb,
                'tx_gb': tx_gb,
                'total_gb': total_gb
            }

            logger.debug(f"数据已收集: RX={rx_gb:.3f}GB, TX={tx_gb:.3f}GB, Total={total_gb:.3f}GB")
            return data

        except Exception as e:
            logger.error(f"收集数据时出错: {str(e)}")
            return None

    def get_metrics(self):
        """生成Prometheus格式的指标"""
        data = self.collect_data()

        if not data:
            return "# 无法获取数据\n"

        metrics = []

        # 添加帮助和类型信息
        metrics.append("# HELP ecs_network_month_total_gb 本月网络流量总计(GB)")
        metrics.append("# TYPE ecs_network_month_total_gb gauge")
        metrics.append(f"ecs_network_month_total_gb {data['total_gb']:.3f}")

        # 添加更详细的指标
        metrics.append("# HELP ecs_network_rx_bytes 接收的总字节数")
        metrics.append("# TYPE ecs_network_rx_bytes gauge")
        metrics.append(f"ecs_network_rx_bytes {data['rx_bytes']}")

        metrics.append("# HELP ecs_network_tx_bytes 发送的总字节数")
        metrics.append("# TYPE ecs_network_tx_bytes gauge")
        metrics.append(f"ecs_network_tx_bytes {data['tx_bytes']}")

        metrics.append("# HELP ecs_network_rx_gb 接收的总GB数")
        metrics.append("# TYPE ecs_network_rx_gb gauge")
        metrics.append(f"ecs_network_rx_gb {data['rx_gb']:.3f}")

        metrics.append("# HELP ecs_network_tx_gb 发送的总GB数")
        metrics.append("# TYPE ecs_network_tx_gb gauge")
        metrics.append(f"ecs_network_tx_gb {data['tx_gb']:.3f}")

        # 添加限制相关指标
        metrics.append("# HELP ecs_network_limit_gb 流量限制(GB)")
        metrics.append("# TYPE ecs_network_limit_gb gauge")
        metrics.append(f"ecs_network_limit_gb {LIMIT}")

        metrics.append("# HELP ecs_network_usage_percent 流量使用百分比")
        metrics.append("# TYPE ecs_network_usage_percent gauge")
        usage_percent = (data['total_gb'] / LIMIT) * 100
        metrics.append(f"ecs_network_usage_percent {usage_percent:.2f}")

        return "\n".join(metrics) + "\n"

class PrometheusHandler(BaseHTTPRequestHandler):
    def __init__(self, *args, collector=None, **kwargs):
        self.collector = collector
        super().__init__(*args, **kwargs)

    def do_GET(self):
        if self.path == '/metrics':
            # 每次请求时收集最新数据
            self.send_response(200)
            self.send_header('Content-Type', 'text/plain')
            self.end_headers()
            self.wfile.write(self.collector.get_metrics().encode())
        else:
            self.send_response(404)
            self.end_headers()
            self.wfile.write(b'Not Found')

    def log_message(self, format, *args):
        # 可以启用以下行来记录HTTP请求
        # logger.debug(f"HTTP {format%args}")
        pass

def main():
    collector = VnstatCollector(INTERFACE)

    # 检查依赖
    if not collector.check_dependencies():
        return

    # 启动HTTP服务器
    server = HTTPServer(('0.0.0.0', PORT), lambda *args: PrometheusHandler(*args, collector=collector))

    logger.info(f"启动Prometheus指标服务器在端口 {PORT}")
    logger.info(f"指标可通过 http://localhost:{PORT}/metrics 访问")

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        logger.info("服务器已停止")

if __name__ == "__main__":
    main()

定时清空历史数据

云服务商往往不会按照自然月统计,而是按照账单日统计月流量,需要在计划任务中配置指定日期,清理 vnStat 数据。

Shell 脚本:

# cat /data/scripts/reset_network.sh
#!/bin/bash

# 停止 vnStat 服务
systemctl stop vnstat # 如果使用 systemd 管理服务

# 删除 vnStat 数据库文件(根据需要修改网络接口名称)
rm -f /var/lib/vnstat/* # 删除所有 vnstat 数据库文件

# 重新启动 vnStat 服务
systemctl start vnstat # 如果使用 systemd 管理服务

echo "vnStat 流量统计数据已重置。"

计划任务:

# 每月1日0时0分清空数据
0 0 1 * * /bin/bash /data/scripts/reset_network.sh

Grafana 绘图效果

Prometheus 统计月度流量

本文属于专题:Prometheus Exporter

引用链接

正文完
 
pengyinwei
版权声明:本站原创文章,由 pengyinwei 2025-04-01发表,共计6683字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处:https://www.opshub.cn
评论(没有评论)