• Go编程在Linux中如何获取CPU利用率
  • 发布于 2个月前
  • 222 热度
    0 评论
概述
Go 语言标准库没有提供获取 CPU 利用率的方法,如果业务开发中需要用到一些服务器性能指标数据,必须由开发者自己实现。本文主要介绍在 Linux 中如何获取 CPU 利用率,笔者的示例代码运行环境为 go1.20 linux/amd64。

命令行工具
Linux 中常见的命令如 top、htop、sar, 可以非常方便地获取和显示 CPU 利用率等数据,下面是 top 命令的结果输出。

我们可以很容易想到,利用 Go 语言标准库中的 exec.Command 方法结合 top 命令,获取 CPU 的利用率,下面是对应的代码。
package main

import (
 "bytes"
 "fmt"
 "log"
 "os/exec"
)
// 堆代码 duidaima.com
func main() {
 var output bytes.Buffer
 cmd := exec.Command("top", "-b", "-n", "1")
 cmd.Stdout = &output
 err := cmd.Run()
 if err != nil {
  log.Fatal(err)
 } else {
  fmt.Printf("top result: \n%v\n", output.String())
 }
}
执行命令:
$ go run main.go

top result:
top - 15:07:24 up 1 day,  1:13,  0 users,  load average: 0.00, 0.00, 0.00
Tasks:  24 total,   1 running,  23 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.1 us,  0.1 sy,  0.0 ni, 99.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  8056768 total,  5245460 free,   750332 used,  2060976 buff/cache
KiB Swap:  2097152 total,  2097152 free,        0 used.  7003500 avail Mem

  PID USER        PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    1 root        20   0    2324   1708   1600 S   0.0  0.0   0:01.13 init(Ubunt+
    4 root        20   0    2948    308     68 S   0.0  0.0   6:34.42 init

...

20997 someone     20   0   19680   9512   5420 S   0.0  0.1   0:00.30 zsh
29176 someone     20   0 1610576  21080   9272 S   0.0  0.3   0:00.09 go
29268 someone     20   0  711488   3008    872 S   0.0  0.0   0:00.00 main
29273 someone     20   0   29444   3656   3228 R   0.0  0.0   0:00.00 top
从输出的结果中可以看到,虽然上面的方法可以获取到 CPU 相关数据,但是输出结果仅仅只是便于人眼阅读,如果我们希望将相关数据值单独取出来在程序中使用, 就需要基于结果字符串进行解析操作,这个过程就会非常麻烦而且容易出错,所以我们需要一个更好的方案。

CPU 数据相关文件
在 Linux 一切皆文件[1] 一文中提到,Linux 将资源抽象为文件表示,那么和 CPU 相关的数据是否也会被抽象为文件,进而保存在某个文件中呢?通过查找 Linux 开发在线文档,可以发现和 CPU 相关的数据主要分布于 /proc 目录下的几个文件中:

/proc/stat
提供了内核统计数据,当然也包括了 CPU 的数据。
/proc/cpuinfo
提供了有关 CPU 的详细数据,包括 CPU 型号、核心数量等。
/proc/<PID>/stat

和 /proc/stat 提供的数据类似,但是数据对应的是单个进程。


/proc/stat
因为我们希望看到系统全局 CPU 利用率,所以这里选择基于 /proc/stat 文件中的内容进行解析来获取数据。
首先来看下 /proc/stat 的文件内容:
$ cat /proc/stat

# 笔者的测试机器输出如下
cpu  40493 6954 65486 76990150 9113 0 25483 0 0 0
cpu0 5181 995 9564 9615416 1201 0 20111 0 0 0

...

cpu7 4382 609 7231 9627991 626 0 203 0 0 0
intr 11137544 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1310 0 181 1 1 10 0 3 0 3 0 77853 0 1408 0 1408 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 37107675
btime 1694670818
processes 276043
procs_running 1
procs_blocked 0
softirq 21005941 0 2478101 7 16554 0 0 3790140 6921044 0 7800095
然后再对照看一下 /proc/stat 对应的文档:


/proc/stat 文档描述
结合上面的文档描述,/proc/stat 文件的输内容表示如下:
.第一行输出系统全局 CPU 使用情况
.从第二行开始,依次输出单个逻辑 CPU 的使用情况
CPU 使用情况数据按照空格划分,一共有 10 列 (也就是图中画红线的部分),每一列表示的含义如下。
列序号 名称 描述
1 user 用户态 CPU 时间
2 nice 低优先级用户态 CPU 时间 (进程的 nice 值被调整为 1-19 之间)
3 system 内核态 CPU 时间
4 idle CPU 空闲时间 (不包括 IO 等待时间)
5 iowait 等待 I/O 的 CPU 时间
6 irq 处理硬中断的 CPU 时间
7 softirq 处理软中断的 CPU 时间
8 steal 当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间
9 guest 通过虚拟化运行其他操作系统的时间
10 guest_nice 低优先级运行虚拟机的时间
文件内容剩下的字段本文暂时用不到,不过这里还是简单提一下。
字段 作用
intr 系统中断相关数据
ctxt 系统上下文切换次数
btime 系统启动以来的时间
processes 创建的进程数量
procs_running 运行进程数量
procs_blocked 阻塞进程数量
softirq 不同类型软中断的处理次数

计算利用率
CPU 利用率 = CPU 使用时间 / (CPU 闲置时间 + CPU 使用时间)
CPU 使用时间 = user + nice + system + irq + softirq + steal
CPU 闲置时间 = idle + iowait

在上面的公式中,我们将 iowait 字段算作 CPU 空闲时间,这一点可能存在争议 (因为 CPU 在等待 IO 时可能会去执行其他进程任务),这里我们暂且跳过这个争议, 先实现一个最小版本 (避免在细节上面浪费过多时间),然后找一个成熟的开源组件,对比一下计算方法即可。

代码实现
有了上面的理论基础之后,现在可以写代码来实现功能,核心的思路是: 通过读取 /proc/stat 文件内容解析出对应的 CPU 指标数据完成采样,然后通过多次采样数据对比计算出 CPU 的利用率。
package main

import (
 "fmt"
 "log"
 "math"
 "math/rand"
 "os"
 "strconv"
 "strings"
 "time"
)

const (
 cpuStatFile = "/proc/stat"
)

// 采样结果对象
type result struct {
 used uint64 // CPU 使用时间
 idle uint64 // CPU 闲置时间
}

// CPU 指标采样函数
func sample() (*result, error) {
 data, err := os.ReadFile(cpuStatFile)
 if err != nil {
  return nil, err
 }

 res := &result{}

 lines := strings.Split(string(data), "\n")
 for _, line := range lines {
  fields := strings.Fields(line)
  // 为了简化演示
  // 这里只取所有 CPU 总的统计数据
  if len(fields) == 0 || fields[0] != "cpu" {
   continue
  }

  // 将第一行数据分割为数组
  n := len(fields)
  for i := 1; i < n; i++ {
   if i > 8 {
    continue
   }

   // 解析每一列的数值
   val, err := strconv.ParseUint(fields[i], 10, 64)
   if err != nil {
    return nil, err
   }

   // 第 4 列表示 CPU 空闲时间
   // 第 5 列表示 等待 I/O 的 CPU 时间
   if i == 4 || i == 5 {
    res.idle += val
   } else {
    res.used += val
   }
  }

  return res, nil
 }

 return res, nil
}

func main() {
 // 获取第一次采样结果
 first, err := sample()
 if err != nil {
  log.Fatal(err)
 }

 // 模拟一些 CPU 密集型任务
 rand.Seed(time.Now().UnixNano())
 for i := 0; i < 10000; i++ {
  _ = math.Sqrt(rand.Float64())
 }

 // 获取第二次采样结果
 second, err := sample()
 if err != nil {
  log.Fatal(err)
 }

 // 计算两次采样期间 CPU 的空闲时间
 idle := float64(second.idle - first.idle)
 // 计算两次采样期间 CPU 的使用时间
 used := float64(second.used - first.used)
 // CPU 利用率 = CPU 使用时间 / (CPU 闲置时间 + CPU 使用时间)
 var usage float64
 if idle+used > 0 {
  usage = used / (idle + used) * 100
 }

 fmt.Printf("CPU usage is %f%%\n", usage)
}
运行上面的代码
$ go run main.go

CPU usage is 32.558140%
上面的代码演示了如何获取系统中所有 CPU 总的利用率,感兴趣的读者可以在这个代码基础上进行改进,实现获取单个 CPU 的利用率。

对比验证
现在找一个 Go 语言的开源组件,对比和验证一下刚才的实现代码是否存在问题,避免闭门造车,一叶障目。笔者选择的组件是 gopsutil[2],下面是使用该组件获取 CPU 利用率对应的代码。

组件实现代码
package main

import (
 "fmt"
 "github.com/shirou/gopsutil/v3/cpu"
 "log"
 "math"
 "math/rand"
 "time"
)

func main() {
 done := make(chan struct{})

 go func() {
  for i := 0; i < 5; i++ {
   // 获取 CPU 利用率 (每 100 毫秒获取一次)
   percent, err := cpu.Percent(100*time.Millisecond, false)
   if err != nil {
    log.Fatalf("get CPU usage: %v\n", err)
    return
   }

   for _, v := range percent {
    fmt.Printf("CPU usage is %.2f%%\n", v)
   }
  }

  // 模拟程序结束后通过 channel 发送通知
  done <- struct{}{}
 }()

 // 模拟一些 CPU 密集型任务
 rand.Seed(time.Now().UnixNano())
 for i := 0; i < 10000000; i++ {
  _ = math.Sqrt(rand.Float64())
 }

 <-done
 close(done)
}
运行上面的代码
$ go run main.go

CPU usage is 32.558140%
CPU usage is 12.35%
CPU usage is 5.00%
CPU usage is 0.00%
CPU usage is 0.00%
CPU usage is 0.00%
最后,我们追踪下 gopsutil 源代码的调用链路,学习一下内部的实现细节。

TimesStat 对象
TimesStat 表示 CPU 指标数据集合对象。
type TimesStat struct {
 CPU       string  `json:"cpu"`
 User      float64 `json:"user"`
 System    float64 `json:"system"`
 Idle      float64 `json:"idle"`
 Nice      float64 `json:"nice"`
 Iowait    float64 `json:"iowait"`
 Irq       float64 `json:"irq"`
 Softirq   float64 `json:"softirq"`
 Steal     float64 `json:"steal"`
 Guest     float64 `json:"guest"`
 GuestNice float64 `json:"guestNice"`
}
Percent 方法
// https://github.com/shirou/gopsutil/blob/2fabf15a16dca198f735a5de2722158576e986a9/cpu/cpu.go#L148
// Percent 方法计算 CPU 利用率
//   可以计算总的 CPU 利用率,也可以计算单个 CPU 的利用率 (取决于第二个参数)
//   在刚才的例子中,我们计算的是总的 CPU 利用率
func Percent(interval time.Duration, percpu bool) ([]float64, error) {
 return PercentWithContext(context.Background(), interval, percpu)
}

// Percent 方法的内部具体实现
func PercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) {
 ...

 // 第一次采样
 cpuTimes1, err := TimesWithContext(ctx, percpu)
 if err != nil {
  return nil, err
 }

 // 获取指标的间隔时间
 // 期间直接进入休眠
 if err := common.Sleep(ctx, interval); err != nil {
  return nil, err
 }

 // 第二次采样
 cpuTimes2, err := TimesWithContext(ctx, percpu)
 if err != nil {
  return nil, err
 }

 // 根据两次采样数据计算出 CPU 利用率
 return calculateAllBusy(cpuTimes1, cpuTimes2)
}
TimesWithContext
该方法主要用于获取 CPU 指标数据采样。
func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) {
 // 获取对应的指标数据文件名称,也就是 /proc/stat
 filename := common.HostProc("stat")
 lines := []string{}
 if percpu {
  // 获取单个 CPU 数据
  ...
 } else {
  // 获取总的 CPU 数据
  lines, _ = common.ReadLinesOffsetN(filename, 0, 1)
 }

 ret := make([]TimesStat, 0, len(lines))

 for _, line := range lines {
  // 将 /proc/stat 文件中的单行文本数据解析为 TimesStat 指标对象
  ct, err := parseStatLine(line)
  if err != nil {
   continue
  }
  ret = append(ret, *ct)

 }
 return ret, nil
}
calculateAllBusy
该方法根据两个 TimesStat 采样数据对象,计算出 CPU 利用率,具体的计算方法委托给 calculateBusy 方法。
func calculateAllBusy(t1, t2 []TimesStat) ([]float64, error) {
 ...

 ret := make([]float64, len(t1))
 for i, t := range t2 {
  ret[i] = calculateBusy(t1[i], t)
 }
 return ret, nil
}
// 堆代码 duidaima.com
func calculateBusy(t1, t2 TimesStat) float64 {
 t1All, t1Busy := getAllBusy(t1)
 t2All, t2Busy := getAllBusy(t2)

 ...

 return math.Min(100, math.Max(0, (t2Busy-t1Busy)/(t2All-t1All)*100))
}

func getAllBusy(t TimesStat) (float64, float64) {
    busy := t.User + t.System + t.Nice + t.Iowait + t.Irq +
        t.Softirq + t.Steal
    return busy + t.Idle, busy
}
从上面的代码可以看到,calculateBusy 方法内部的计算公式为:
CPU 利用率 = CPU 使用时间 / (CPU 闲置时间 + CPU 使用时间)
CPU 使用时间 = user + nice + system + iowait + irq + softirq + steal
CPU 闲置时间 = idle

小结
本文主要介绍了在 Linux 系统中,如何使用 Go 语言获取 CPU 利用率,我们首先介绍了在 Linux 中获取 CPU 利用率时涉及到的文件和具体方法, 然后通过自己手动实现了一个简单的版本,最后通过开源组件 gopsutil[3] 的内部实现和自己手动实现进行对比和验证, 发现了计算细节的差异:
.自己手动实现的版本中,iowait 列的数据作为 CPU 闲置时间
.gopsutil 实现的版本中,iowait 列的数据作为 CPU 使用时间
此外,笔者查看了 htop 命令 对应的源代码,发现 htop 也是将 iowait 列的数据作为 CPU 闲置时间,下面是具体的代码链接和截图。

htop 命令源代码[4]

针对上面的差异情况,笔者 (不成熟的) 的建议如下:
1.如果技术栈为 Go 语言,直接使用 gopsutil 组件即可
2.如果技术栈为其他语言,使用该语言中对应的成熟组件
3.如果上述两种情况都不符合,或者必须自己实现获取 CPU 利用率的功能,可以根据业务场景来决定
  3.1 对于 CPU 密集型场景,将 iowait 列的数据作为 CPU 计算时间
  3.2 对于 IO 密集型场景,将 iowait 列的数据作为 CPU 闲置时间
用户评论