CPU使用率

CPU 使用率是最常见的监控指标之一,用于衡量 CPU繁忙程度

一方面,如果某个系统 CPU 使用率越高,意味着应用进程得不到调度的概率越大,应用响应速度将受负面影响;另一方面,某个应用 CPU 使用率过高,意味着其消耗太多 CPU 资源,很可能存在优化的空间。

指标

大多数系统管理员或开发人员对 CPU 使用率或多或少有所了解,但未必准确。

那么,操作系统如何衡量 CPU 的繁忙程度呢?

最简单的方式是,统计 CPU 在执行任务的时间以及空闲的时间,并计算这两部分时间的占比。如果执行任务时间占比很高,则说明 CPU 非常繁忙;反之亦然。因此, CPU 使用率是一个 百分比 也就非常好理解了。

通常,操作系统对 CPU 时间的统计更为细化。以 Linux 为例,内核进一步将执行时间和空闲时间进行分类,形成更为细致指标:

CPU使用率指标(%)
指标名含义
user用户态
nice低优先级用户态
system内核态
idle空闲
iowaitIO等待
irq中断处理
softirq软中断处理
steal被其他虚拟化系统占用
guest运行客户机系统
guest_nice运行低优先级客户机系统

user

user 表示 CPU 运行在 用户态 的时间占比。

应用进程执行分为 用户态 以及 内核态CPU 在用户态执行应用进程自身的代码逻辑,通常是一些 逻辑数值计算CPU 在内核态执行进程发起的 系统调用 ,通常是响应进程对资源的请求。

如果应用为 计算密集型 (包含大量计算很少系统调用),则 CPU__user 状态使用率很高。

nice

nice 表示 CPU 运行在 低优先级用户态 的时间占比,低优先级意味着进程 nice 值小于 0

system

user 表示 CPU 运行在 内核态 的时间占比。

一般而言, 内核态CPU 使用率不应过高,除非应用进程发起大量系统调用。如果该值较高,需要着手分析原因,重新审视程序设计是否存在缺陷。

idle

idle 表示 CPU 在空闲状态的时间占比,该状态下 CPU 没有任何任务可执行。

iowait

iowait 表示“等待I/O”的时间。大部分人对此有误解,认为 CPU 此时不能工作。

这是不正确的, CPU 在等待 I/O 时,可以切换到其他就绪任务执行,只是当时刚好没有就绪任务可以运行。准确讲, iowaitCPU 空闲并且系统有 I/O 请求未完成的时间。

另一个误解是: iowait 升高时便认为系统存在 I/O 瓶颈。同种 I/O 条件下,如果系统还有其他计算密集型任务, iowait 将明显降低。

因此, iowait 是一个非常模糊的指标,并不足以说明问题。大部分情况下,还需要检查 I/O 量,等待队列等更加明确的指标。如果只是 iowait 升高,其他指标没有明显变化,便无需担心。

注解

idleiowait 都说明 CPU 很空闲, iowait 还说明系统有未完成的 I/O 请求。

irq

irq 表示 CPU 处理 硬件中断 的时间占比。

网卡中断 是一个典型的例子:网卡接到数据包后,通过硬件中断通知 CPU 进行处理。如果系统网络流量非常大,则可观察到 irq 使用率明显升高。

通常,网卡中断只由一个 CPU 来响应。如果网络处理上不去并观察到单个 CPU__irq 指标较高,则可以考虑通过 irqbalance 将中断处理平衡到更多 CPU 上。

softirq

对应地, softirq 表示 CPU 处理 软件中断 的时间占比。

steal

steal 是指在虚拟化环境中,被其他系统占用的时间。这体现为物理 CPU 没有办法为当前系统服务,通常正在为另一个系统服务。在虚拟机超卖比较严重的场景,这个数值非常明显。这部分时间显然不是当前系统所用,而是被其他系统占用了。

total

CPU 时间片数是各种状态时间的和,计算公式如下:

[total = user + nice + system + idle + iowait + irq + softirq + steal]

注意到, guest 以及 guest_nice 不参与求和计算,因为这两种时间分别作为 user 以及 nice 的一部分统计在其中了。

utilized

CPU 用于执行任务的时间将是 6 种执行状态时间的总和:

[utilized = user + nice + system + irq + softirq + steal]

除此之外,还有另外一种计算方法,只包含 5 种执行状态:

[utilized = user + nice + system + irq + softirq]

两种计算方式区别只在于: steal 状态占用的时间是否参与计算。前者反应了系统的 实际负载steal 虽不是本系统占用,但也制约了系统对 CPU 资源的进一步使用;后者则反映了系统的 真实负载 ,也就是系统的实际开销。

算法

Linux 内核为 CPU 各个核心维护了 自系统启动 以来各种状态的时间,并暴露在在 proc 伪文件系统中,路径为 /proc/stat 。通过以下命令可以窥探一二:

  1. $ cat /proc/stat

注解

/proc/stat 中, CPU 时间单位为 jiffy ,即 USERHZ 分之一秒。其中, USER_HZ 是内核计时时钟的频率,表示时钟每秒产生多少次中断。时钟每中断一次,内核 _jiffies 自增 1

很显然, CPU 使用率可以由内核提供的计数器( counters )计算而来。

首先,在 t1 时间点采集一次 /proc/stat 数据并计算总 CPU 时间:

[total{t1} = user{t1} + nice{t1} + system{t1} + idle{t1} + iowait{t1} + irq{t1} + softirq{t1} + steal_{t1}]

t2 时间点再采集一次,同样计算总 CPU 时间:

[total{t2} = user{t2} + nice{t2} + system{t2} + idle{t2} + iowait{t2} + irq{t2} + softirq{t2} + steal_{t2}]

那么,从 t1t2CPU 时间为:

[delta{t1,t2} = total{t2} - total_{t1}]

其中,用户态时间占比为:

[user_percent = {\frac{user{t2} - user{t1}}{total{t2} - total{t1}}} \times 100\%]

这便是用户态 CPU 使用率,其他状态使用率计算方式以此类推。

很显然,所有状态 CPU 使用率加起来刚好就是 100% (同样不包括 guest 系列):

[user_percent + nice_percent + \cdots + steal_percent = 100\%]

采集

接下,看看如何读取 /proc/stat 文件并计算 CPU 使用率。直接上代码:

cpu_usage.py

  1. import time
  2. from tabulate import (
  3. tabulate,
  4. )
  5. # sample interval
  6. INTERVAL = 1
  7. # table header
  8. TABLE_HEADER = (
  9. 'device',
  10. 'utilized',
  11. 'user',
  12. 'nice',
  13. 'system',
  14. 'idle',
  15. 'iowait',
  16. 'irq',
  17. 'softirq',
  18. 'steal',
  19. 'guest',
  20. 'guset_nice',
  21. )
  22. def cpu_counters():
  23. records = []
  24. # open /proc/stat to read
  25. with open('/proc/stat') as f:
  26. # iterate all lines
  27. for line in f.readlines():
  28. if not line.startswith('cpu'):
  29. continue
  30. # split to fields
  31. fields = line.strip().split()
  32. # cpu name
  33. name = fields[0]
  34. # convert all counters to int
  35. counters = tuple(map(int, fields[1:]))
  36. # calculate total cpu time
  37. total = sum(counters[:8])
  38. records.append((name, counters, total))
  39. return records
  40. def sample_forever():
  41. last_records = None
  42. while True:
  43. # sample cpu counters
  44. records = cpu_counters()
  45. if last_records:
  46. table_data = []
  47. # iterate counters for every cpu core
  48. for (device, last_counters, last_total), (_, counters, total) in \
  49. zip(last_records, records):
  50. # calculate cpu usage
  51. delta = total - last_total
  52. percents = list(map(
  53. lambda pair: 100. * (pair[0]-pair[1]) / delta,
  54. zip(counters, last_counters),
  55. ))
  56. utilized_percent = sum(percents[:3] + percents[5:8])
  57. table_data.append([device, utilized_percent] + percents)
  58. # make table
  59. table_data = tabulate(
  60. table_data,
  61. TABLE_HEADER,
  62. tablefmt='simple',
  63. floatfmt='6.2f',
  64. )
  65. # print table
  66. print(table_data)
  67. print()
  68. last_records = records
  69. time.sleep(INTERVAL)
  70. def main():
  71. try:
  72. sample_forever()
  73. except KeyboardInterrupt:
  74. pass
  75. if __name__ == '__main__':
  76. main()

cpu_counters 函数负责读取 /proc/stat 文件并解析所有 CPU 时间计数器:

  • 31 行,打开 /proc/stat 文件;
  • 33 行,读取所有文本行;
  • 34-35 行,跳过所有非 cpu 开头的行;
  • 38 行,切分字段;
  • 41 行,取出 CPU 名字段;
  • 43 行,将所有 CPU 时间计数器转换成整数类型;
  • 45 行,累加总 CPU 时间;
  • 47 行,记录解析结果;sample_forever 函数不断采集并计算 CPU 使用率,计算部分逻辑如下:

  • 62-63 行,遍历每个 CPU 设备,分别取出 CPU 名、计数器以及总 CPU 时间;

  • 66 行,计算两次采集间的总 CPU 时间;
  • 67-70 行,计算两次采集间 CPU 每种状态执行时间的占比(百分比);
  • 71 行,计算 CPU 繁忙时间占比,包括 steal 部分;注意到,例子程序使用 tabulate 模块格式化表格,不再赘述。

下一步

订阅更新,获取更多学习资料,请关注我们的 微信公众号

../../../_images/wechat-mp-qrcode.png小菜学编程

参考文献