- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
139 lines
3.6 KiB
Go
139 lines
3.6 KiB
Go
//go:build !windows
|
|
|
|
// Package collector — 磁盘 IO 指标采集器
|
|
// 通过读取 /proc/diskstats 获取磁盘读写 BPS、IOPS 和 util(磁盘繁忙程度)。
|
|
//
|
|
// 署名 Hermes
|
|
package collector
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"ezops/agent/model"
|
|
)
|
|
|
|
// diskIOSample 保存上一次采样,用于计算速率
|
|
type diskIOSample struct {
|
|
time time.Time
|
|
readSects uint64
|
|
writeSects uint64
|
|
readOps uint64
|
|
writeOps uint64
|
|
ioTicks uint64 // io_ticks (field 12) — 磁盘忙碌的毫秒累计
|
|
}
|
|
|
|
// DiskIOCollector 磁盘 IO 采集器
|
|
type DiskIOCollector struct {
|
|
mu sync.Mutex
|
|
prev *diskIOSample
|
|
}
|
|
|
|
// NewDiskIOCollector 创建磁盘 IO 采集器
|
|
func NewDiskIOCollector() *DiskIOCollector {
|
|
return &DiskIOCollector{}
|
|
}
|
|
|
|
// Collect 采集磁盘 IO 指标
|
|
func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
|
|
stat := model.DiskIOStat{}
|
|
|
|
rSects, wSects, rOps, wOps, ioTicks, err := readDiskStats()
|
|
if err != nil {
|
|
return stat, fmt.Errorf("读取磁盘 IO 失败: %w", err)
|
|
}
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
cur := &diskIOSample{
|
|
time: now,
|
|
readSects: rSects,
|
|
writeSects: wSects,
|
|
readOps: rOps,
|
|
writeOps: wOps,
|
|
ioTicks: ioTicks,
|
|
}
|
|
|
|
if c.prev != nil && now.After(c.prev.time) {
|
|
elapsed := now.Sub(c.prev.time).Seconds()
|
|
if elapsed > 0 {
|
|
sectorSize := uint64(512) // 标准扇区 512 bytes
|
|
stat.ReadBPS = float64((cur.readSects-c.prev.readSects)*sectorSize) / elapsed
|
|
stat.WriteBPS = float64((cur.writeSects-c.prev.writeSects)*sectorSize) / elapsed
|
|
stat.ReadIOPS = float64(cur.readOps-c.prev.readOps) / elapsed
|
|
stat.WriteIOPS = float64(cur.writeOps-c.prev.writeOps) / elapsed
|
|
// util% = io_ticks_delta_ms / (elapsed_ms) * 100
|
|
ioDelta := cur.ioTicks - c.prev.ioTicks
|
|
stat.Util = float64(ioDelta) / (elapsed * 1000.0) * 100.0
|
|
if stat.Util > 100 {
|
|
stat.Util = 100
|
|
}
|
|
if stat.Util < 0 {
|
|
stat.Util = 0
|
|
}
|
|
}
|
|
}
|
|
c.prev = cur
|
|
|
|
return stat, nil
|
|
}
|
|
|
|
// readDiskStats 读取 /proc/diskstats,汇总所有物理磁盘的读写数据
|
|
// fields[2]=设备名, fields[3]=读完成次数, fields[5]=读扇区数, fields[7]=写完成次数, fields[9]=写扇区数, fields[12]=io_ticks
|
|
func readDiskStats() (rSects, wSects, rOps, wOps, ioTicks uint64, err error) {
|
|
f, err := os.Open("/proc/diskstats")
|
|
if err != nil {
|
|
return 0, 0, 0, 0, 0, err
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 14 {
|
|
continue
|
|
}
|
|
|
|
dev := fields[2]
|
|
// 只采集物理磁盘 (sdX, nvmeXn1, vdX),跳过分区和 loop 设备
|
|
if strings.HasPrefix(dev, "loop") ||
|
|
(strings.HasPrefix(dev, "sd") && len(dev) > 3) || // sda1, sda2 等分区
|
|
(strings.HasPrefix(dev, "nvme") && strings.Contains(dev, "p")) || // nvme0n1p1
|
|
(strings.HasPrefix(dev, "ram")) ||
|
|
strings.HasPrefix(dev, "dm-") {
|
|
continue
|
|
}
|
|
|
|
// field 3 = 读完成次数 (rd_ios)
|
|
// field 5 = 读扇区数 (rd_sectors)
|
|
// field 7 = 写完成次数 (wr_ios)
|
|
// field 9 = 写扇区数 (wr_sectors)
|
|
// field 12 = io_ticks (milliseconds spent doing I/O)
|
|
if v, e := strconv.ParseUint(fields[3], 10, 64); e == nil {
|
|
rOps += v
|
|
}
|
|
if v, e := strconv.ParseUint(fields[5], 10, 64); e == nil {
|
|
rSects += v
|
|
}
|
|
if v, e := strconv.ParseUint(fields[7], 10, 64); e == nil {
|
|
wOps += v
|
|
}
|
|
if v, e := strconv.ParseUint(fields[9], 10, 64); e == nil {
|
|
wSects += v
|
|
}
|
|
if v, e := strconv.ParseUint(fields[12], 10, 64); e == nil {
|
|
ioTicks += v
|
|
}
|
|
}
|
|
|
|
return rSects, wSects, rOps, wOps, ioTicks, scanner.Err()
|
|
}
|