Go 语言如何获取 CPU 利用率

概述

Go 语言标准库没有提供获取 CPU 利用率的方法,如果业务开发中需要用到一些服务器性能指标数据,必须由开发者自己实现。

本文主要介绍在 Linux 中如何获取 CPU 利用率,笔者的示例代码运行环境为 go1.20 linux/amd64

命令行工具

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

007bf5b4f7bb53896b66ee62eaf1294e.png

top 命令输出

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

package main

import (
 "bytes"
 "fmt"
 "log"
 "os/exec"
)

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 对应的文档:

d171e943e668a71a88738ee5ccb5065c.png

/proc/stat 文档描述

结合上面的文档描述,/proc/stat 文件的输内容表示如下:

  • 第一行输出系统全局 CPU 使用情况

  • 从第二行开始,依次输出单个逻辑 CPU 的使用情况

CPU 使用情况数据按照空格划分,一共有 10 列 (也就是图中画红线的部分),每一列表示的含义如下。

列序号名称描述
1user用户态 CPU 时间
2nice低优先级用户态 CPU 时间 (进程的 nice 值被调整为 1-19 之间)
3system内核态 CPU 时间
4idleCPU 空闲时间 (不包括 IO 等待时间)
5iowait等待 I/O 的 CPU 时间
6irq处理硬中断的 CPU 时间
7softirq处理软中断的 CPU 时间
8steal当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间
9guest通过虚拟化运行其他操作系统的时间
10guest_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
}

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]

b127a6ed54fb20a623ecdf12fdbf0262.png

htop 命令 iowait 计算方式

针对上面的差异情况,笔者 (不成熟的) 的建议如下:

  • 如果技术栈为 Go 语言,直接使用 gopsutil 组件即可

  • 如果技术栈为其他语言,使用该语言中对应的成熟组件

  • 如果上述两种情况都不符合,或者必须自己实现获取 CPU 利用率的功能,可以根据业务场景来决定

  1. 对于 CPU 密集型场景,将 iowait 列的数据作为 CPU 计算时间

  2. 对于 IO 密集型场景,将 iowait 列的数据作为 CPU 闲置时间

扩展阅读

  • proc(5) — Linux manual page[5]

  • How to Check CPU Utilization in Linux?[6]

  • How to Check Linux CPU Usage or Utilization[7]

  • gopsutil[8]

  • Linux CPU utilization[9]

链接

[1]

Linux 一切皆文件: https://dbwu.tech/posts/linux_everything_is_a_file/

[2]

gopsutil: https://github.com/shirou/gopsutil

[3]

gopsutil: https://github.com/shirou/gopsutil

[4]

htop 命令源代码: https://github.com/htop-dev/htop/blob/e79788c250c23e0c7c9f3e29fdca578ce9e9bca0/linux/LinuxMachine.c#L469C1-L469C1

[5]

proc(5) — Linux manual page: https://man7.org/linux/man-pages/man5/procfs.5.html

[6]

How to Check CPU Utilization in Linux?: https://www.scaler.com/topics/how-to-check-cpu-utilization-in-linux/

[7]

How to Check Linux CPU Usage or Utilization: https://www.atlantic.net/vps-hosting/how-to-check-linux-cpu-usage-or-utilization/

[8]

gopsutil: https://github.com/shirou/gopsutil

[9]

Linux CPU utilization: https://www.rosettacode.org/wiki/Linux_CPU_utilization

想要了解Go更多内容,欢迎扫描下方👇关注公众号,回复关键词 [实战群]  ,就有机会进群和我们进行交流


fd0242aa7ce7f3123144c0a21514d2a9.png

分享、在看与点赞Go 59a93d841fdfe352a87c6fbd0aebc5d0.gif

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值