95计费系统开发过程总结

行云流水
2026-03-29 / 0 评论 / 2 阅读 / 正在检测是否收录...

一、项目背景

IDC带宽计费普遍采用95百分位计费法。原理很简单:一个月内每5分钟采集一次带宽数据,按值从大到小排序,去掉最高的5%,剩余数据中的最大值即为计费带宽。

这种计费方式对客户有利——偶尔的流量突发不会被计入费用。但对运维来说,每月手动统计、制表、发邮件是重复劳动,以前每月我都需要拿出1天时间,来导出zabbix的监控数据,并计算95值,然后做报表,发邮件。开发这个系统的目的就是把这些事情自动化,提高效率节省时间。

二、系统介绍

首页

首页

首页展示系统概况,包括项目数量、报告统计、快捷入口等。

项目管理

项目列表

项目是系统的核心概念,每个项目对应一个计费主体(比如一个客户或一条专线)。项目下挂载多个端口,端口信息从Zabbix同步过来。

新建项目

新建项目时配置基本信息:趋势方向(UP/DOWN,决定取流入还是流出)、数据源、Grafana Dashboard等。

数据同步

数据同步

流量数据存储在Zabbix的MySQL数据库中。系统定时从Zabbix同步数据到本地表,避免直接查询生产库造成压力。

95计费结果

95计费结果

选择时间范围后,系统自动计算各项目的95值。支持一键复制为Markdown表格,方便粘贴到汇报文档。

报告管理

新建报告

报告是月度统计的产出物,包含95值、峰值、Grafana截图、CSV数据文件。

报告列表

报告支持批量创建、状态管理、一键发送邮件。

数据管理

数据管理

支持项目配置的导入导出、流量数据的导出,便于备份和迁移。

系统设置

系统设置

配置通过环境变量管理,包括数据库连接、Grafana地址、SMTP信息等。

邮件通知

收到的邮件

报告发送后,收件人会收到包含统计结果、图表附件的邮件。

三、架构设计

整体架构

┌─────────────────────────────────────────────────────────────┐
│                        前端 (Vue 3)                          │
│  Element Plus + Vue Router + Axios                          │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      后端 (Go + Gin)                         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │ Handler  │──│ Service  │──│Repository│──│  Model   │   │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
        ┌──────────┐   ┌──────────┐   ┌──────────┐
        │  MySQL   │   │  Zabbix  │   │ Grafana  │
        │ (zreport)│   │  (只读)  │   │  (截图)  │
        └──────────┘   └──────────┘   └──────────┘

目录结构

aireport/
├── cmd/server/main.go          # 入口
├── internal/
│   ├── config/                  # 配置加载
│   ├── models/                  # 数据模型
│   ├── repository/              # 数据访问层
│   ├── services/                # 业务逻辑层
│   ├── handlers/                # HTTP处理器
│   └── router/                  # 路由配置
├── pkg/
│   ├── utils/                   # 工具函数(95计算)
│   └── response/                # 统一响应格式
├── web/                         # 前端项目
│   └── src/
│       ├── api/                 # 接口封装
│       ├── views/               # 页面组件
│       └── router/              # 前端路由
└── exports/                     # 导出文件存储

技术选型

层面技术选择理由
后端框架Gin轻量、性能好、社区活跃
ORMGORM功能完善、开发效率高
前端框架Vue 3组合式API、响应式设计方便
UI组件库Element Plus组件丰富、文档完善
数据库MySQL业务数据量小、关系型足够

四、核心代码

95计费算法

95计费的核心是排序取百分位,实现并不复杂:

// Calculate95thPercentile 计算95计费
// 排序后取第5%位置的值
func Calculate95thPercentile(data []DataPoint, unitBase int) BillingResult {
    if len(data) == 0 {
        return BillingResult{}
    }

    // 确定进制,影响Gbps转换
    divisor := float64(1e9) // 1000进制
    if unitBase == 1024 {
        divisor = 1073741824 // 1024进制
    }

    // 按值降序排序
    sort.Slice(data, func(i, j int) bool {
        return data[i].Value > data[j].Value
    })

    // 计算要去掉的数据条数(5%)
    totalCount := len(data)
    removeCount := int(math.Round(float64(totalCount) / 20))

    // 95值就是第removeCount位置的那个值
    index95 := removeCount
    if index95 >= totalCount {
        index95 = totalCount - 1
    }

    return BillingResult{
        TotalCount:   totalCount,
        RemoveCount:  removeCount,
        Result95:     data[index95].Value,
        Result95Gbps: float64(data[index95].Value) / divisor,
        PeakValue:    data[0].Value,
        PeakGbps:     float64(data[0].Value) / divisor,
    }
}

这里有个细节:进制选择。Grafana默认用1000进制(1Gbps = 10^9 bps),但有些场景用1024进制。系统支持在项目级别配置,保证计算结果和监控图表一致。

多端口数据合并

一个项目可能有多个端口,需要把所有端口的流量按时间点累加:

// combineByTime 按时间点合并数据
func (s *AnalysisService) combineByTime(data []utils.DataPoint) []utils.DataPoint {
    timeMap := make(map[int]uint64)

    for _, d := range data {
        timeMap[d.Clock] += d.Value
    }

    result := make([]utils.DataPoint, 0, len(timeMap))
    for clock, value := range timeMap {
        result = append(result, utils.DataPoint{
            Clock: clock,
            Value: value,
        })
    }

    return result
}

合并后再进行95计算,得到的是项目整体带宽。

Grafana截图

报告需要附带流量图表,直接调用Grafana的渲染接口:

func (s *ReportService) downloadGrafanaImage(project *models.Project, startTs, endTs int) (string, error) {
    url := fmt.Sprintf("%s/render/d/%s?from=%d&to=%d&width=%d&height=%d",
        s.grafanaURL,
        project.GrafanaUID,
        startTs*1000,
        endTs*1000,
        project.ChartWidth,
        project.ChartHeight,
    )

    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("Authorization", s.grafanaAPIKey)

    client := &http.Client{Timeout: 30 * time.Second}
    resp, err := client.Do(req)
    // ... 保存图片
}

邮件发送

报告准备好后,调用SMTP发送:

func (s *EmailService) SendReportWithAttachment(
    report *models.Report,
    imageData []byte,
    csvData []byte,
    csvFilename string,
    recipients []string,
) error {
    m := gomail.NewMessage()
    m.SetHeader("From", s.from)
    m.SetHeader("To", recipients...)
    m.SetHeader("Subject", report.Title)

    // HTML正文
    body := fmt.Sprintf(`
        <h3>%s</h3>
        <p>月份: %s</p>
        <p>95计费值: %.2f Gbps</p>
        <p>峰值: %.2f Gbps</p>
    `, report.Title, report.Month, report.Result95, report.PeakValue)
    m.SetBody("text/html", body)

    // 附件
    if imageData != nil {
        m.Attach("chart.png", gomail.SetCopyFunc(func(w io.Writer) error {
            _, err := w.Write(imageData)
            return err
        }))
    }

    // 发送
    d := gomail.NewDialer(s.host, s.port, s.user, s.password)
    return d.DialAndSend(m)
}

五、数据模型

主要表结构

// Project 项目
type Project struct {
    ID          uint   `gorm:"primaryKey"`
    Name        string `gorm:"uniqueIndex"`
    Host        string // 交换机主机
    Trend       string // UP/DOWN
    Source      string // 数据源表名
    UnitBase    int    // 进制: 1000 或 1024
    GrafanaUID  string // Grafana Dashboard ID
    Ports       []Port `gorm:"foreignKey:ProjectID"`
}

// Port 端口
type Port struct {
    ID        uint   `gorm:"primaryKey"`
    ProjectID uint
    Name      string
    ItemIDIn  uint64 // Zabbix流入监控项ID
    ItemIDOut uint64 // Zabbix流出监控项ID
}

// Report 报告
type Report struct {
    ID        uint
    ProjectID uint
    Month     string  // 2026-02
    Result95  float64 // Gbps
    PeakValue float64 // Gbps
    Status    string  // draft/sent/failed
}

Zabbix数据表

系统直接读取Zabbix的history_uint表,结构如下:

-- Zabbix原始表
CREATE TABLE history_uint (
    itemid    BIGINT UNSIGNED,
    clock     INT,          -- Unix时间戳
    value     BIGINT UNSIGNED,
    ns        INT           -- 纳秒
);

每个itemid对应一个监控项(比如某端口的入流量)。我们关心的是itemid、clock、value三个字段。

六、部署

系统采用单机部署,前后端打包在一起:

# 构建前端
cd web && npm run build

# 构建后端
go build -o aireport ./cmd/server

# 运行
./aireport

配置通过.env文件管理:

# 数据库
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=xxx
DB_NAME=zreport

# Zabbix数据库(只读)
ZABBIX_DB_HOST=127.0.0.1
ZABBIX_DB_NAME=zabbix

# Grafana
GRAFANA_URL=http://localhost:3000
GRAFANA_API_KEY=xxx

# 邮件
SMTP_HOST=smtp.163.com
SMTP_PORT=465
SMTP_USER=xxx@163.com
SMTP_PASSWORD=xxx
RECIPIENTS=xxx@qq.com,yyy@163.com

七、总结

这个系统解决了一个具体的运维痛点:每月的带宽统计和报告发送。开发过程中几个关键点:

  1. 数据源选择:直接读Zabbix数据库比调API效率高,但要注意不要影响Zabbix自身性能,所以加了同步机制。
  2. 进制问题:带宽单位换算有1000和1024两种标准,必须和监控平台保持一致,否则数据对不上。
  3. 多端口聚合:一个客户多条线路是常见情况,需要按时间点累加后再计算95值。
  4. 报告自动化:最大的价值在于节省人工,一键生成报告并发送邮件。

系统功能相对简单,但覆盖了完整的业务闭环:数据采集 → 计算 → 报告 → 通知。后续可以根据需求扩展,比如增加历史趋势分析、异常告警等功能。

评论 (0)

取消
只有登录/注册用户才可评论