CPU使用率

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

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

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

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

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

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

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

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

    nice

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

    system

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

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

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

    iowait

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

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

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

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

    注解

    irq

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

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

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

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

    steal

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

    total

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

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

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

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

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

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

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

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

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

    注解

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

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

    [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. records = []
    23. # open /proc/stat to read
    24. with open('/proc/stat') as f:
    25. for line in f.readlines():
    26. if not line.startswith('cpu'):
    27. continue
    28. # split to fields
    29. fields = line.strip().split()
    30. # cpu name
    31. name = fields[0]
    32. # convert all counters to int
    33. counters = tuple(map(int, fields[1:]))
    34. # calculate total cpu time
    35. total = sum(counters[:8])
    36. records.append((name, counters, total))
    37. return records
    38. def sample_forever():
    39. last_records = None
    40. while True:
    41. # sample cpu counters
    42. records = cpu_counters()
    43. if last_records:
    44. table_data = []
    45. # iterate counters for every cpu core
    46. for (device, last_counters, last_total), (_, counters, total) in \
    47. zip(last_records, records):
    48. delta = total - last_total
    49. percents = list(map(
    50. lambda pair: 100. * (pair[0]-pair[1]) / delta,
    51. ))
    52. utilized_percent = sum(percents[:3] + percents[5:8])
    53. table_data.append([device, utilized_percent] + percents)
    54. # make table
    55. table_data = tabulate(
    56. table_data,
    57. TABLE_HEADER,
    58. tablefmt='simple',
    59. floatfmt='6.2f',
    60. )
    61. # print table
    62. print(table_data)
    63. print()
    64. last_records = records
    65. time.sleep(INTERVAL)
    66. def main():
    67. try:
    68. sample_forever()
    69. except KeyboardInterrupt:
    70. pass
    71. 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 每种状态执行时间的占比(百分比);

    订阅更新,获取更多学习资料,请关注我们的 :