一、项目背景
IDC带宽计费普遍采用95百分位计费法。原理很简单:一个月内每5分钟采集一次带宽数据,按值从大到小排序,去掉最高的5%,剩余数据中的最大值即为计费带宽。
这种计费方式对客户有利——偶尔的流量突发不会被计入费用。但对运维来说,每月手动统计、制表、发邮件是重复劳动,以前每月我都需要拿出1天时间,来导出zabbix的监控数据,并计算95值,然后做报表,发邮件。开发这个系统的目的就是把这些事情自动化,提高效率节省时间。
二、系统介绍
首页

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

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

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

流量数据存储在Zabbix的MySQL数据库中。系统定时从Zabbix同步数据到本地表,避免直接查询生产库造成压力。
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 | 轻量、性能好、社区活跃 |
| ORM | GORM | 功能完善、开发效率高 |
| 前端框架 | 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七、总结
这个系统解决了一个具体的运维痛点:每月的带宽统计和报告发送。开发过程中几个关键点:
- 数据源选择:直接读Zabbix数据库比调API效率高,但要注意不要影响Zabbix自身性能,所以加了同步机制。
- 进制问题:带宽单位换算有1000和1024两种标准,必须和监控平台保持一致,否则数据对不上。
- 多端口聚合:一个客户多条线路是常见情况,需要按时间点累加后再计算95值。
- 报告自动化:最大的价值在于节省人工,一键生成报告并发送邮件。
系统功能相对简单,但覆盖了完整的业务闭环:数据采集 → 计算 → 报告 → 通知。后续可以根据需求扩展,比如增加历史趋势分析、异常告警等功能。
评论 (0)