// Package collector 提供服务器各项系统指标的采集能力。 // 本文件实现 CPU 使用率与负载采集,主要通过解析 Linux /proc/stat 与 /proc/loadavg 实现。 // // 署名 Logan.Li package collector import ( "bufio" "fmt" "os" "runtime" "strconv" "strings" "sync" "ezops/agent/model" ) // cpuSample 保存某一时刻 CPU 的累计 jiffies,用于两次采样计算差值 type cpuSample struct { total uint64 // 自系统启动以来 CPU 所有状态的累计 jiffies idle uint64 // idle + iowait 的累计 jiffies iowait uint64 // iowait 单独累计 jiffies(用于计算 iowait 占比) } // CPUCollector CPU 指标采集器。 // 通过在两次采集之间保存上一次的采样数据来计算真实使用率。 type CPUCollector struct { mu sync.Mutex prev *cpuSample } // NewCPUCollector 创建 CPU 采集器实例 func NewCPUCollector() *CPUCollector { return &CPUCollector{} } // Collect 采集当前 CPU 指标。 // 注意:首次调用因缺少上次采样数据,返回的使用率为 0;后续调用返回两次采集之间的平均使用率。 func (c *CPUCollector) Collect() (model.CPUStat, error) { stat := model.CPUStat{CoreCount: runtime.NumCPU()} c.mu.Lock() defer c.mu.Unlock() cur, err := readCPUSample() if err != nil { return stat, fmt.Errorf("读取 CPU 采样失败: %w", err) } if c.prev != nil { totalDelta := cur.total - c.prev.total idleDelta := cur.idle - c.prev.idle iowaitDelta := cur.iowait - c.prev.iowait if totalDelta > 0 { // 使用率 = (总增量 - 空闲增量) / 总增量 * 100 stat.UsagePercent = float64(totalDelta-idleDelta) / float64(totalDelta) * 100.0 if stat.UsagePercent < 0 { stat.UsagePercent = 0 } // iowait = iowait增量 / 总增量 * 100 stat.IOWait = float64(iowaitDelta) / float64(totalDelta) * 100.0 if stat.IOWait < 0 { stat.IOWait = 0 } } } c.prev = &cur // 负载平均值采集失败不影响 CPU 使用率上报 if l1, l5, l15, err := readLoadAvg(); err == nil { stat.Load1, stat.Load5, stat.Load15 = l1, l5, l15 } return stat, nil } // readCPUSample 读取 /proc/stat 第一行(聚合所有 CPU)并计算 total 与 idle func readCPUSample() (cpuSample, error) { var sample cpuSample f, err := os.Open("/proc/stat") if err != nil { return sample, err } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "cpu ") { continue } // 形如: cpu user nice system idle iowait irq softirq steal guest guest_nice fields := strings.Fields(line) if len(fields) < 5 { return sample, fmt.Errorf("/proc/stat 数据格式异常") } // fields[0] == "cpu",之后依次为各类 jiffies for i, v := range fields[1:] { x, perr := strconv.ParseUint(v, 10, 64) if perr != nil { continue } sample.total += x // idle(索引3) 与 iowait(索引4) 计入空闲 if i == 3 || i == 4 { sample.idle += x } // iowait(索引4) 单独记录 if i == 4 { sample.iowait += x } } return sample, nil } if err := scanner.Err(); err != nil { return sample, err } return sample, fmt.Errorf("/proc/stat 中未找到聚合 CPU 行") } // readLoadAvg 读取 /proc/loadavg,返回 1/5/15 分钟平均负载 func readLoadAvg() (load1, load5, load15 float64, err error) { f, err := os.Open("/proc/loadavg") if err != nil { return 0, 0, 0, err } defer f.Close() _, err = fmt.Fscanf(f, "%f %f %f", &load1, &load5, &load15) return load1, load5, load15, err }