type
status
date
slug
summary
tags
category
icon
password
你是否也曾跟我一样,看了很多书、学了很多 Linux 性能工具,但在面对 Linux 性能问题时,还是束手无策?比如:流量高峰期,服务器 CPU 使用率过高报警,你登录服务器执行完 top 命令之后,却不知道怎么进一步定位,到底是系统 CPU 资源太少,还是程序并发写的有问题?系统并没有跑什么吃内存的程序,但是敲完 free 命令之后,却发现系统已经没有什么内存了,那到底是哪里占用了内存?这个时候该怎么办?
你可能会想,反正程序出了问题,上网查就是了,用别人的方法,囫囵吞枣地多试几次,有可能就解决了。于是,你懒得深究这些方法为啥有效,更不知道为什么,很多方法在别人的环境有效,到你这儿就不行了。其实很多工程师,在分析应用程序所使用的第三方组件的性能时,并不熟悉这些组件所用的编程语言,却依然可以分析出线上问题的根源,并能通过一些方法进行优化。所以只要你理解了应用程序和操作系统的基本原理,再进行大量的实战练习,建立起整体性能的全局观,大多数性能问题的优化就会水到渠成。
毫无疑问,性能优化是软件系统中最有挑战的工作之一,但是换个角度看,它也是最考验体现你综合能力的工作之一。我觉得主要是因为性能优化是个系统工程,总是牵一发而动全身。它涉及了从程序设计、算法分析、编程语言,再到系统、存储、网络等各种底层基础设施的方方面面。每一个组件都有可能出问题,而且很有可能多个组件同时出问题。
一个正确的选择胜过千百次的努力。千万不要把命令工具当成学习的全部。工具只是解决问题的手段,关键在于你的用法。只有真正理解了它们背后的原理,并且结合具体场景,融会贯通系统的不同组件,你才能真正掌握它们。其实,对于我们大多数人来说,最好的学习方式一定是带着问题学习,而不是先去啃那几本厚厚的原理书籍,这样很容易把自己的信心压垮。
平均负载
什么是平均负载
平均负载 = 运行中的进程数 + 等待运行的进程数 + 不可中断状态的进程数
平均负载是指单位时间内,系统处于
可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数,它和 CPU 使用率并没有直接关系。它不仅包括了正在使用 CPU 的进程,还包括等待 CPU 和等待 I/O 的进程。
可运行状态的进程是指正在使用 CPU 或者正在等待 CPU 的进程,也就是我们常用 ps 命令看到的,处于 R 状态(运行态Running 或 就绪态Runnable)的进程。
不可中断状态的进程则是正处于内核态关键流程中的进程,并且这些流程是不可打断的,比如最常见的是等待硬件设备的 I/O 响应,也就是我们在 ps 命令中看到的 D 状态(Uninterruptible Sleep,也称为 Disk Sleep)的进程。比如,当一个进程向磁盘读写数据时,为了保证数据的一致性,在得到磁盘回复前,它是不能被其他进程或者中断打断的,这个时候的进程就处于不可中断状态。如果此时的进程被打断了,就容易出现磁盘数据与进程数据不一致的问题。所以,不可中断状态实际上是系统对进程和硬件设备的一种保护机制。而 CPU 使用率,是单位时间内 CPU 繁忙情况的统计,跟平均负载并不一定完全对应。比如:CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高
平均负载为多少时合理
每次发现系统变慢时,我们通常做的第一件事,就是执行
top 或者 uptime 命令,来了解系统的负载情况。比如像下面这样,我在命令行里输入了 uptime 命令,系统也随即给出了结果。在 uptime 命令的结果里,那三个时间段的平均负载数,多大的时候能说明系统负载高?或是多小的时候就能说明系统负载很低呢?
平均负载最理想的情况是等于 CPU 个数。所以在评判平均负载时,首先你要知道系统有几个 CPU,有了 CPU 个数,我们就可以判断出,当平均负载比 CPU 个数还大的时候,系统已经出现了过载。这可以通过命令lscpu或者从文件 /proc/cpuinfo 中读取平均负载其实就是平均活跃进程数。平均活跃进程数,直观上的理解就是单位时间内的活跃进程数。既然平均的是活跃进程数,那么最理想的,就是每个 CPU 上都刚好运行着一个进程,这样每个 CPU 都得到了充分利用。我们知道平均负载有三个数值,到底该参考哪一个呢?实际上都要看。三个不同时间间隔的平均值,其实给我们提供了,分析系统负载趋势的数据来源,让我们能更全面、更立体地理解目前的负载状况。三个平均负载值结合分析:
平稳:1分钟≈5分钟≈15分钟
升高:1分钟 > 15分钟
降低:1分钟 < 15分钟
如果 1 分钟、5 分钟、15 分钟的三个值基本相同,或者相差不大,那就说明系统负载很平稳。如果 1 分钟的值远小于 15 分钟的值,就说明系统最近 1 分钟的负载在减少,而过去 15 分钟内却有很大的负载。反过来,如果 1 分钟的值远大于 15 分钟的值,就说明最近 1 分钟的负载在增加,这种增加有可能只是临时性的,也有可能还会持续增加下去,所以就需要持续观察。一旦 1 分钟的平均负载接近或超过了 CPU 的个数,就意味着系统正在发生过载的问题,这时就得分析调查是哪里导致的问题,并要想办法优化了在我看来,当平均负载高于 CPU 数量 70% 的时候,你就应该分析排查负载高的问题了。平均负载升高的几个原因:
CPU 密集型进程:使用大量 CPU 会导致平均负载升高
I/O 密集型进程:等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高
大量等待 CPU 的进程调度:频繁的上下文切换导致平均负载升高,此时的 CPU 使用率也会比较高假设我们在一个单 CPU 系统上看到平均负载为 1.73,0.60,7.98,那么说明在过去 1 分钟内,系统有 73% 的超载,而在 15 分钟内,有 698% 的超载,从整体趋势来看,系统的负载在降低。一旦负载过高,就可能导致进程响应变慢,进而影响服务的正常功能。例如当前系统中只有一个CPU,当平均负载为1.71,则说明有71%的超载,这时就需要注意了。比如当平均负载为 2 时,意味着什么呢?在只有 2 个 CPU 的系统上,意味着所有的 CPU 都刚好被完全占用。在 4 个 CPU 的系统上,意味着 CPU 有 50% 的空闲。而在只有 1 个 CPU 的系统中,则意味着有一半的进程竞争不到 CPU。
平均负载案例
预先安装
stress 和 sysstat 包
stress 是一个 Linux 系统压力测试工具,这里我们用作异常进程模拟平均负载升高的场景
sysstat 包含了常用的 Linux 性能工具,用来监控和分析系统的性能CPU 密集型进程
首先,我们在第一个终端运行 stress 命令,模拟一个 CPU 使用率 100% 的场景:
接着,在第二个终端运行
uptime 查看平均负载的变化情况:最后,在第三个终端运行
mpstat 查看 CPU 使用率的变化情况,mpstat主要用于显示多处理器系统上每个处理器的使用情况从终端二中可以看到,1 分钟的平均负载会慢慢增加到 1.00,而从终端三中还可以看到,正好有一个 CPU 的使用率为 100%,但它的 iowait 只有 0。这说明,平均负载的升高正是由于 CPU 使用率为 100% 。
那么,到底是哪个进程导致了 CPU 使用率为 100% 呢?你可以使用
pidstat 来查询或者top命令等工具从这里可以明显看到,stress 进程的 CPU 使用率为 100%。此时只有一个cpu使用率100%,由于系统只有2个cpu,意味着有一半的cpu空闲

大量等待 CPU 的进程调度
模拟大量进程的场景,由于系统只有 2 个 CPU,明显比 8 个进程要少得多。当系统中运行进程超出 CPU 运行能力时,就会出现等待 CPU 的进程。因而,系统的 CPU 处于严重过载状态
可以看出,8 个进程在争抢 2 个 CPU,每个进程等待 CPU 的时间(也就是代码块中的 %wait 列)高达 75%。这些超出 CPU 计算能力的进程,最终导致 CPU 过载。
pidstat输出中没有%wait的问题,是因为CentOS默认的sysstat稍微有点老,源码或者RPM升级到11.5.5版本以后就可以看到了。而Ubuntu的包一般都比较新,没有这个问题。执行top可以看到cpu使用率100%,平均负载过高,大量进程
I/O 密集型进程
运行 stress 命令模拟 I/O 压力,即不停地执行 sync
iowait无法升高的问题,是因为案例中stress使用的是 sync() 系统调用,它的作用是刷新缓冲区内存到磁盘中。对于新安装的虚拟机,缓冲区可能比较小,无法产生大的IO压力,这样大部分就都是系统调用的消耗了。所以,你会看到只有系统CPU使用率升高。解决方法是使用stress的下一代stress-ng,它支持更丰富的选项,比如 stress-ng -i 1 --hdd 1 --timeout 600(--hdd表示读写临时文件)。在第二个终端运行 top 命令查看平均负载和cpu的变化情况

第三个终端运行 mpstat 查看 CPU 使用率的变化情况:
从这里可以看到,其中一个 CPU 的系统 CPU 使用率升高到了 23.87,而 iowait 高达 67.53%。这说明,平均负载的升高是由于 iowait 的升高。
这里模拟IO密集进程时,iowait没有变化,sys cpu使用情况突增。 这里查询到的解释:产生N个进程,每个进程循环调用sync将内存缓冲区内容写到磁盘上,产生IO压力。通过系统调用sync刷新内存缓冲区数据到磁盘中,以确保同步。如果缓冲区内数据较少,写到磁盘中的数据也较少,不会产生IO压力。在SSD磁盘环境中尤为明显,很可能iowait总是0,却因为大量调用系统调用sync,导致系统CPU使用率sys 升高。
导致 iowait 这么高呢?我们还是用 pidstat 来查询。可以发现,还是 stress 进程导致的。
如果无法模拟上述案例,可以使用以下脚本进行模拟。当脚本运行时,会产生大量的io操作,导致系统的负载逐渐升高,直到达到一定的阈值
脚本执行后,负载升高,但是cpu使用并不高,io很高
执行
pidstat可以看到sh和dd命令导致io密集型执行top命令如图

实验结束后,将进程kill掉
CPU 上下文切换
进程在竞争 CPU 的时候并没有真正运行,为什么还会导致系统的负载升高呢?CPU 上下文切换就是罪魁祸首。Linux 是一个多任务操作系统,它支持远大于 CPU 数量的任务同时运行。当然,这些任务实际上并不是真的在同时运行,而是因为系统在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。为了确保切换后每个任务都能够继续执行,系统需要保存当前任务的状态信息,这个状态信息就叫做
CPU 上下文。CPU 上下文切换就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。被保存的上下文信息会存在计算机系统的核心中,以便在任务重新执行时再次加载进来,这样就能保证任务看起来像是一直在运行。好比你正在写作业,突然间要切换去做其他事情,比如回答一条消息。在切换之前,你需要保存当前写作业的状态,比如你写到哪里了,用了哪些笔记,等等。然后去回答消息,回来继续写作业时,你会根据之前保存的状态信息继续写作业。在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好 CPU 寄存器和程序计数器。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此也被叫做 CPU 上下文。根据任务的不同,CPU 的上下文切换就可以分为几个不同的场景:
进程上下文切换、线程上下文切换以及中断上下文切换。
CPU 寄存器:CPU 寄存器是 CPU 内置的速度极快的内存
程序计数器:程序计数器会存储 CPU 正在执行的指令位置,或者即将执行的指令位置进程上下文切换
Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间。进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的
用户态,而陷入内核空间的时候,被称为进程的内核态。内核空间(Ring 0)具有最高权限,可以直接访问所有资源;用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源
进程从用户态到内核态的转变,需要通过
系统调用来完成。比如web服务的进程,一般是运行在用户态的,但是当需要访问内存、磁盘等硬件设备的时候需要先进入到内核态中,也就是从用户态到内核态的转变,而这种转变需要借助系统调用来实现。系统调用是内核向用户进程提供服务的唯一方法。再比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。系统调用的过程如下:
系统调用过程中,CPU 的上下文切换还是无法避免的,CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。而系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换(用户态→内核态→用户态)。好比你在做一份作业,忽然想查一下书上的内容。这就像你的程序在运行时需要访问一些特殊的计算机功能,比如读取文件。
保存现场: 在你开始查书前,你会记下你作业的进度和页数,以便回来时能继续。计算机也一样,当程序需要访问特殊功能时,需要保存当前进度,即用户态的指令位置。
进入图书馆: 为了查书,你需要到图书馆。计算机也需要进入图书馆,即内核态,执行一些特殊的任务。
查书: 在图书馆,你找到了需要的书,就像计算机执行了特殊任务,比如读取文件。
回到作业: 查完书后,你回到你的作业,就像计算机要切换回用户态,继续执行原来的程序。
系统调用就像你查书一样,需要先保存当前进度(用户态指令位置),然后进入图书馆执行特殊任务(内核态代码),完成后再回到你的作业(用户态),这就是两次上下文切换。
所以系统调用过程中,不涉及虚拟内存等进程用户态的资源,也不会切换进程,也就是系统调用过程中一直是同一个进程在运行。系统调用过程也通常被称为特权模式切换。而
进程上下文切换是指从一个进程切换到另一个进程运行。进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。因此进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。所以在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是导致平均负载升高的一个重要因素。如下图所示,保存上下文和恢复上下文的过程并不是免费的,需要内核在 CPU 上运行才能完成。

Linux 通过
TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。进程切换时才需要切换上下文,换句话说,只有在进程调度的时候,才需要切换上下文。Linux 为每个 CPU 都维护了一个就绪队列,将活跃进程(即正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。进程切换的场景有:
进程时间片耗尽:为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行;
进程在系统资源不足:比如内存不足时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
进程挂起:进程通过睡眠函数 sleep 主动把自己挂起,CPU会重新调度;
进程抢占:当有CPU发现优先级更高的进程运行时,为了去运行高优先级进程,当前进程会被挂起;
中断:发生硬中断,CPU 上的进程会被挂起,然后去执行内核中的中断服务进程。线程上下文切换
线程与进程最大的区别在于,线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程。而进程只是给线程提供了虚拟内存、全局变量等资源。所以对于线程和进程,我们可以这么理解:当进程只有一个线程时,可以认为进程就等于线程;当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的。另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。这么一来,线程的上下文切换其实就可以分为两种情况:
第一种:前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
第二种:前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
虽然同为上下文切换,但同进程内的线程切换,要比多进程间的切换消耗更少的资源,而这,也正是多线程代替多进程的一个优势。
中断上下文切换
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。
案例
sysbench 是一个多线程的基准测试工具,一般用来评估不同系统参数下的数据库负载情况。在这次案例中,我们使用 sysbench 来模拟系统多线程调度切换的情况,来演示上下文切换过多的问题。机器配置:4 CPU,8GB 内存安装 sysbench 和 sysstat
如果无法使用yum安装,可以使用源码包下载:https://github.com/akopytov/sysbench/archive/1.0.15.tar.gz
首先,在第一个终端里运行
sysbench ,模拟系统多线程调度的瓶颈。以10个线程运行5分钟的基准测试,模拟多线程切换的问题接着,在第二个终端运行
vmstat ,观察上下文切换情况你应该可以发现,cs 列的上下文切换次数从之前的 107 骤然上升到了 1122906。同时观察其他几个指标:
r 列:就绪队列的长度已经到了 9,远远超过了系统 CPU 的个数 2,所以肯定会有大量的 CPU 竞争。
us(user)和 sy(system)列:这两列的 CPU 使用率加起来上升到了 73%,其中系统 CPU 使用率,也就是 sy 列高达 49%,说明 CPU 主要是被内核占用了。
in 列:中断次数也上升到了 1 万左右,说明中断处理也是个潜在的问题。综合这几个指标,我们可以知道,系统的就绪队列过长,也就是正在运行和等待 CPU 的进程数过多,导致了大量的上下文切换,而上下文切换又导致了系统 CPU 的占用率升高。然后再在第三个终端再用
pidstat 来看一下, CPU 和进程上下文切换的情况:从 pidstat 的输出你可以发现,CPU 使用率的升高果然是 sysbench 导致的,它的 CPU 使用率已经达到了 100%。虽然 sysbench 进程(也就是主线程)的上下文切换次数看起来并不多,但它的子线程的上下文切换次数却有很多。看来上下文切换罪魁祸首,还是过多的 sysbench 线程。
前面在观察系统指标时,除了上下文切换频率骤然升高,还有一个指标也有很大的变化:中断次数。中断次数也上升到了 1 万,但到底是什么类型的中断上升了?我们知道中断它只发生在内核态,而 pidstat 只是一个进程的性能分析工具,并不提供任何关于中断的详细信息,我们可以从
/proc/interrupts 这个只读文件中读取中断发生的类型。/proc 实际上是 Linux 的一个虚拟文件系统,用于内核空间与用户空间之间的通信。/proc/interrupts 就是这种通信机制的一部分,提供了一个只读的中断使用情况。通过一段时间的观察可以发现,变化最频繁的中断类型是
重调度中断(RES)。这种中断的作用是唤醒处于空闲状态的 CPU,使其重新参与调度并运行新的任务。在多核处理器系统中,调度器通过这种中断机制,将任务分散到多个 CPU 上执行,从而实现负载均衡。这个过程也被称为 处理器间中断。简单来说,RES 中断就是一个打电话叫别的 CPU 来帮忙干活的机制。那么每秒上下文切换多少次才算正常呢?这个数值其实取决于系统本身的 CPU 性能。在我看来,如果系统的上下文切换次数比较稳定,那么从数百到一万以内,都应该算是正常的。但当上下文切换次数超过一万次,或者切换次数出现数量级的增长时,就很可能已经出现了性能问题。这时,你还需要根据上下文切换的类型,再做具体分析。比方说:自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看
/proc/interrupts 文件来分析具体的中断类型。CPU使用率
CPU 使用率是单位时间内 CPU 使用情况的统计,以百分比的方式展示。那么,作为最常用也是最熟悉的 CPU 指标,你能说出 CPU 使用率到底是怎么算出来的吗?再有,诸如
top、ps 之类的性能工具展示的 %user、%nice、 %system、%iowait 、%steal 等等,你又能弄清楚它们之间的不同吗?进程CPU使用率
最直接的方法,就是从源头开始寻找答案。我们可以去看一下top命令的源代码: https://gitlab.com/procps-ng/procps。在代码中你会看到对于每个进程,top都会从proc文件系统中每个进程对应的stat文件中读取2个数值。这个stat文件就是
/proc/[pid]/stat ,[pid] 就是替换成具体一个进程的PID值。比如PID值为1的进程,这个文件就是/proc/1/stat。stat文件实时输出了进程的状态信息,这里我们重点关注stat文件中的utime和stime这两项数值。utime是表示进程的用户态部分在Linux调度中获得CPU的tick,stime是表示进程的内核态部分在Linux调度中获得CPU的tick。这个tick 是 Linux 操作系统中的最小时间单位。Linux 内核会通过一个周期性时钟中断来进行时间推进和调度,比如:系统设置为每秒产生 100 次时钟中断(即 HZ=100),那么每个 tick 表示的时间就是 1/100 秒,也就是 10 毫秒。假设某个进程的 utime 为 130 tick,那么它在用户态总共消耗的时间为:130 tick × (1 / 100 秒) = 1.3 秒,也就是说,从进程启动以来,它在用户态运行了 1.3 秒钟。
utime和stime都是一个累计值,也就是说从进程启动开始,这两个值就是一直在累积增长的。那么我们怎么计算某一进程在用户态和内核态中,分别获得了多少CPU的ticks呢?首先,我们可以假设这个瞬时是1秒钟,这1秒是T1时刻到T2时刻之间的,那么这样我们就能获得 T1 时刻的utime_1 和stime_1,同时获得T2时刻的utime_2 和 stime_2。在这1秒的瞬时,进程用户态获得的CPU ticks就是 (utime_2 – utime_1), 进程内核态获得的CPU ticks就是 (stime_2 – stime_1)。那么我们可以推导出,进程CPU总的开销就是用户态加上内核态,也就是在1秒瞬时进程总的CPU ticks等于 (utime_2 – utime_1) + (stime_2 – stime_1)。得到了进程以ticks为单位的CPU开销,接下来还要做个转化,把这个值转化成我们熟悉的百分比值:
进程的CPU使用率=((utime_2 – utime_1) + (stime_2 – stime_1)) * 100.0 / (HZ * et * 1 ):
((utime_2 – utime_1) + (stime_2 – stime_1))是瞬时进程总的CPU ticks,乘以100.0的目的是产生百分比数值。我们先来看一下什么是节拍率(HZ):
Linux 是一个多任务操作系统,它会将每个 CPU 的运行时间划分为非常短的小段,称为时间片(time slice),再通过调度器将这些时间片轮流分配给不同的任务,从而实现多个任务看起来同时运行的效果。为了精确控制时间片的划分,Linux 内核会使用一个固定频率的时钟中断来维持节奏。这个中断的频率就是我们说的
节拍率(HZ)。在内核中,每当发生一次时钟中断,内核就会将一个名为 jiffies 的全局变量加一,表示系统已经过去了一个tick(节拍)。这个 jiffies 变量可以看作是系统运行的时间计数器,它记录了从系统启动以来发生的时钟中断次数。节拍率 HZ 是一个内核配置项,常见的值有:100(表示每秒 100 次中断,每个 tick 是 10ms)、250(每秒 250 次中断,每个 tick 是 4ms)、1000(每秒 1000 次中断,每个 tick 是 1ms)。不同的系统可能设置不同数值,你可以通过查看系统内核配置文件
/boot/config-*来查询你当前系统的 HZ 值。例如,在我的系统中,配置的是 CONFIG_HZ=1000,也就是说内核每秒会触发 1000 次时间中断,即每个 tick 的长度为 1 毫秒。由于节拍率 HZ 是一个内核级别的配置参数,普通的用户空间程序无法直接获取这个值。为了解决这个问题,Linux 内核提供了一个统一的接口——用户节拍率(USER_HZ),它的值始终固定为 100,也就是每一个 tick 等于 1/100 秒。这样一来,无论底层内核的实际 HZ 值是 100、250 还是 1000,用户空间程序看到的始终是统一的时间单位(1 tick = 0.01 秒)。这使得用户态的程序不需要关心内核是如何配置的,也能准确地使用如 utime、stime 等时间统计值。
USER_HZ 的值在几乎所有的 Linux 系统上都固定为 100,这是内核编译时约定好的,不会随系统设置而改变。虽然它不是一个运行时可变参数,但你可以通过 getconf 命令进行验证,getconf 命令会返回当前系统的用户节拍率(USER_HZ):
下面我们再来看一下什么是et:et 表示我们前面提到的采样时间间隔,也就是获取 utime_1 和 utime_2 这两个时间点之间的时间差。它代表的是一个很短的瞬时时间段,用于计算该时间段内的 CPU 使用情况。最后乘1, 就更容易理解了,就是1个CPU。那么这三个值相乘,你是不是也知道了它的意思呢?就是在这瞬时的时间(et)里,1个CPU所包含的ticks数目。
接下来,我们验证一下这个公式对不对,启动一个消耗CPU的程序,然后读取一下进程对应的
/proc/[pid]/stat中的utime和stime,然后用这个方法来计算一下进程使用率这个百分比值,并且和top的输出对比一下,看看是否一致。首先启动一个消耗100%的程序执行top命令,可以看到它的PID是4434,CPU使用率是100%

然后,我们查看这个进程对应的stat文件
/proc/4434/stat,间隔1秒钟输出第二次,因为stat文件内容很多,我们知道utime和stime第14和15项,所以我们这里只截取了前15项的输出。这里可以看到,utime_1 = 4547,stime_1=14,utime_2=4648,stime_2=14。
根据前面的公式,我们来计算一下进程threads-cpu的CPU使用率:((4648 – 4547) + (14 – 14)) * 100.0 / (100 * 1 * 1) =101,也就是101%。你会发现这个值和我们运行top里的值是一样的。同时,我们也就验证了这个公式是没问题的。
CPU使用率
Linux 通过
/proc 虚拟文件系统,向用户空间提供了系统内部状态的信息,而 /proc/stat 提供的就是系统的 CPU 和任务统计信息。在 /proc/stat 文件的 cpu 这行有10列数据,同样我们可以在proc文件系统的Linux programmer’s manual:https://man7.org/linux/man-pages/man5/proc.5.html里,找到每一列数据的定义,而前8列数据正好对应top输出中 %Cpu(s) 那一行里的8项数据user/system/nice/idle/iowait/irq/softirq/steal
而在
/proc/stat里的每一项的数值,就是系统自启动开始的ticks。其中,第一列表示的是 CPU 编号,如 cpu0、cpu1 ,而第一行没有编号的 cpu ,表示的是所有 CPU 的累加。其他列则表示不同场景下 CPU 的累加节拍数,它的单位是 USER_HZ,也就是 10 ms(1/100 秒),所以这其实就是不同场景下的 CPU 时间那么要计算出瞬时的CPU使用率,首先就要算出这个瞬时的ticks,比如1秒钟的瞬时,我们可以记录开始时刻T1的ticks, 然后再记录1秒钟后T2时刻的ticks,再把这两者相减,就可以得到这1秒钟的ticks了

在这1秒钟里每个CPU使用率的ticks统计表如下:

我们想要计算每种 CPU 使用率的百分比,其实也很简单。只需要在一个固定时间间隔,例如 1 秒 内,统计各个 CPU 状态(如 user、nice、system、idle 等)所消耗的时间片tick,然后将所有状态的 tick 加总,得到这一秒内 CPU 的总工作时间。用某一状态的 tick 数值除以总 tick 值,即可计算该状态的占比。
比如,要计算 idle(空闲)时间占比,公式:idle_percent = idle_ticks / total_ticks
总 tick 数为:total = 0 + 0 + 0 + 1203 + 0 + 0 + 0 + 0 = 1203
则空闲占比为:idle_percent = 1203 /( 0 + 0 + 0 + 1203 + 0 + 0 + 0 + 0)=100%
那么 CPU 使用率就是除了空闲时间外的其他时间占总 CPU 时间的百分比,用公式来表示就是:

为了计算 CPU 使用率,性能工具一般都会取间隔一段时间,比如 3 秒的两次值,作差后,再计算出这段时间内的平均 CPU 使用率,这个公式,就是我们用各种性能工具所看到的 CPU 使用率的实际计算方法

容器CPU使用率
在容器中运行
top命令,%Cpu(s)那一行中显示的数值,并不是这个容器的CPU整体使用率,而是容器所在宿主机系统整体的CPU使用率。比如我们在一个12个CPU的宿主机上,启动一个容器,然后在容器里运行top命令
我们可以看到,容器里有两个进程
threads-cpu总共消耗了200%的CPU(2 CPU Usage),而%Cpu(s)那一行的us cpu是58.5%。对于12CPU的系统来说,12 * 58.5%=7.02,也就是说这里显示总共消耗了7个CPU,远远大于容器中2个CPU的消耗。所以top这个工具虽然在物理机或者虚拟机上看得到系统CPU开销,但是如果是放在容器环境下,运行top就无法得到容器中总的CPU使用率。对于系统总的CPU使用率,需要读取/proc/stat文件,但是这个文件中的各项CPU ticks是反映整个节点的,并且这个
/proc/stat文件也不包含在任意一个Namespace里。那么我们还有没有办法得到整个容器的CPU使用率呢?每个容器都会有一个CPU Cgroup的控制组。在这个控制组目录下面有很多参数文件,有的参数可以决定这个控制组里最大的CPU可使用率外,除了它们之外,目录下面还有一个可读项cpuacct.stat。这里包含了两个统计值,这两个值分别是这个控制组里所有进程的内核态ticks和用户态的ticks,那么我们就可以计算整个容器的CPU使用率:CPU使用率=((utime_2 – utime_1) + (stime_2 – stime_1)) * 100.0 / (HZ * et * 1 )就像下图显示的这样,整个容器的CPU使用率的百分比就是 ( (174021 - 173820) + (4 – 4)) * 100.0 / (100 * 1 * 1) = 201, 也就是201%。所以,我们从每个容器的CPU Cgroup控制组里的cpuacct.stat的统计值中,可以比较快地得到整个容器的CPU使用率。

中断
在计算机中,中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应设备的请求。进程的不可中断状态是系统的一种保护机制,可以保证硬件的交互过程不被意外打断。所以,短时间的不可中断状态是很正常的。但是,当进程长时间都处于不可中断状态时,你就得当心了。这时,你可以使用
dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,进而排查相关的进程和磁盘设备。这样的解释可能过于学术了,容易云里雾里。我就举个生活中取外卖的例子让你感受一下中断的魅力。比如说你订了一份外卖,但是不确定外卖什么时候送到,也没有别的方法了解外卖的进度,但是配送员送外卖是不等人的,到了你这儿没人取的话,就直接走人了。所以你只能去门口苦苦等着,而不能干其他事情。如果在订外卖的时候,你就跟配送员约定好,让他送到后给你打个电话,那你就不用苦苦等待了,就可以去忙别的事情,直到电话一响,接电话、取外卖就可以了。这里的打电话,其实就是一个中断。没接到电话的时候,你可以做其他的事情;只有接到了电话(也就是发生中断),你才要进行另一个动作——取外卖。
通过这个例子可以发现,中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力。由于中断处理程序会打断其他进程的运行,所以,为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行。如果中断本身要做的事情不多,那么处理起来也不会有太大问题;但如果中断要处理的事情很多,中断服务程序就有可能要运行很长时间。特别是,中断处理程序在响应中断时,可能还会临时关闭中断。这意味着,当前中断处理程序没有执行完之前,系统中其他的中断请求都无法被响应,也就是说中断有可能会丢失,所以中断处理程序要短且快。
那么还是以取外卖为例,假如你订了 2 份外卖,一份主食和一份饮料,并且是由 2 个不同的配送员来配送。这次你不用时时等待着,两份外卖都约定了电话取外卖的方式。当第一份外卖送到时,配送员给你打了个长长的电话,商量发票的处理方式。与此同时,第二个配送员也到了,也想给你打电话。很明显,因为电话占线(也就是关闭了中断响应),第二个配送员的电话是打不通的。所以,第二个配送员很可能试几次后就走掉了(也就是丢失了一次中断)。
为了解决中断处理程序执行过长和中断丢失的问题,Linux 将中断处理过程分成了两个阶段,也就是上半部和下半部:上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。比如说前面取外卖的例子,上半部就是你接听电话,告诉配送员你已经知道了,其他事儿见面再说,然后电话就可以挂断了;下半部才是取外卖的动作,以及见面后商量发票处理的动作。这样,第一个配送员不会占用你太多时间,当第二个配送员过来时,照样能正常打通你的电话。
除了取外卖,我再举个最常见的网卡接收数据包的例子,让你更好地理解。在网络数据包到达时,网卡会通过
硬件中断的方式通知内核有新数据需要处理。内核响应这个中断后,会进入中断处理程序,这一阶段被称为中断的上半部。对上半部来说,既然是快速处理,其实就是要把网卡的数据读到内存中,然后更新一下硬件寄存器的状态(表示数据已经读好了),最后再发送一个软中断信号触发软中断,通知下半部做进一步的处理。而下半部被软中断信号唤醒后,需要从内存中取出之前接收到的数据包,再按照网络协议栈,对数据进行逐层解析和处理,最后将数据传递给用户空间的应用程序。因为网卡接收到数据会放到一个缓冲区中,然后触发中断,通知内核来处理数据,这是一个异步的过程,因为有缓冲区的参与,就涉及到一个生产者和消费者的典型问题,生产者消息生产速度过快,而消费者处理消息速度过慢,就必然引起消息积压和消息处理延时的问题。这套机制就像一个延迟处理的消息队列:上半部先把任务放入队列,比如数据读入内存。下半部再根据队列中的数据触发后续处理逻辑,协议栈解析、传递给应用。所以中断处理程序的上部分和下半部可以理解为:
上半部直接处理硬件请求,也就是我们常说的硬中断,特点是快速执行;
而下半部则是由内核触发,也就是我们常说的软中断,特点是延迟执行。
实际上,上半部会打断 CPU 正在执行的任务,然后立即执行中断处理程序。而下半部以内核线程的方式执行,并且每个 CPU 都对应一个软中断内核线程,名字为
ksoftirqd/CPU 编号,比如说, 0 号 CPU 对应的软中断内核线程的名字就是 ksoftirqd/0。不过要注意的是,软中断不只包括了刚刚所讲的硬件设备中断处理程序的下半部,一些内核自定义的事件也属于软中断,比如内核调度和 RCU 锁(Read-Copy Update 的缩写,RCU 是 Linux 内核中最常用的锁之一)等。我个人理解比较简单粗暴,硬中断是由外部硬件设备发出的中断信号,比如:键盘输入、鼠标点击、网络接收到数据、硬盘读写完成等。软中断是由内核代码自行触发,比如:定时器中断、网络协议栈处理数据包、RCU 回调函数的延迟处理、网络收发数据中下半部的处理。那要怎么知道你的系统里有哪些软中断呢?在 Linux 系统里,我们可以通过查看
/proc/softirqs 的内容来知晓软中断的运行情况,查看/proc/interrupts的内容来知晓硬中断的运行情况。软中断
通过查看
/proc/softirqs 的内容来知晓软中断的运行情况。运行下面的命令,查看各种类型软中断在不同 CPU 上的累积运行次数在软中断统计信息中,我们可以看到第一列列出了 10 个不同类别的软中断,每一类对应着特定的内核工作类型,例如:
NET_TX 表示网络发送中断、NET_RX 表示网络接收中断、 TIMER 表示定时中断、 RCU 表示 RCU 锁中断、 SCHED 表示内核调度中断。通常情况下,同一种类型的软中断在多个 CPU 核心上的累计次数应该大致相当。如果发现差距较大,可能意味着负载分布不均,存在调度偏斜的问题。其中
TASKLET 是最常用的软中断实现机制,每个 TASKLET 只运行一次就会结束,并且只在调用它的函数所在的 CPU 上运行。因此,使用 TASKLET 特别简便,当然也会存在一些问题,比如说由于只在一个 CPU 上运行导致的调度不均衡,再比如因为不能在多个 CPU 上并行运行带来了性能限制。软中断实际上是以内核线程的方式运行的,每个 CPU 都对应一个软中断内核线程,这个软中断内核线程就叫做
ksoftirqd/CPU 编号。我们可以用 ps 命令查看这些线程的运行状况可以看到有 2 个 ksoftirqd 内核线程,这是因为我这台服务器的 CPU 是 2 核心的,每个 CPU 核心都对应着一个内核线程。这些线程的名字外面都有中括号,这说明 ps 无法获取它们的命令行参数。一般来说,ps 的输出中,名字括在中括号里的,一般都是内核线程。当软中断事件的频率过高时,内核线程也会因为 CPU 使用率过高而导致软中断处理不及时,进而引发网络收发延迟、调度缓慢等性能问题。大量的网络小包会导致频繁的硬中断和软中断,导致性能问题。
火焰图
火焰图简介
Linux火焰图是分析CPU性能瓶颈的可视化工具,通过将CPU执行路径转换为垂直条形图来展示各个函数调用所占的CPU时间比例。通过收集和转换系统CPU调用栈信息,生成SVG格式的火焰图,可以快速定位性能问题。实例涵盖内核函数、用户空间应用、库函数、线程和上下文切换以及并行和并发分析,有助于开发者优化代码和提高系统效率。

火焰图(Flame Graph) 是一种可视化工具,用于分析程序在运行过程中
CPU 使用情况 或 性能瓶颈。它通常以 SVG 格式 展示,支持在浏览器中直接打开,并且具有良好的交互性 —— 用户可以通过点击任意区域查看函数名、源码行号、调用路径等详细信息。通常火焰图常用于分析函数执行的频繁程度、分析哪些函数经常阻塞、分析哪些函数频繁分别配内存。火焰图的构成:
X 轴(横轴):表示所有采样到的调用栈按函数分类的总量。每一个矩形的宽度代表该函数及其子函数在采样周期内累计占用 CPU 时间的比例,即越宽的火焰图代表函数及其子函数消耗的CPU时间越多。直观的展现了哪些函数占用了大部分CPU计算资源。
Y 轴(纵轴):表示函数调用栈的层级关系。最底部是最初调用的函数,越往上是被调用的函数。顶部的函数是 CPU 当前正在执行的函数,下方依次是它的调用者。栈越深,火焰就越高。
火焰图类型
常见的火焰图包括on-cpu火焰图、off-cpu火焰图、内存火焰图、Hot/Cold火焰图、红蓝分叉火焰图。
on-cpu火焰图:分析cpu占用时间,找出cpu占用高的函数,分析代码热路径,通常是固定频率采样cpu调用栈来获取采用数据;
off-cpu火焰图:分析cpu阻塞时间,找出i/o、网络等阻塞、锁竞争、死锁等导致性能下降问题;通常是固定频率采用组的时间调用栈来获取采用数据;
内存火焰图:分析内存申请释放韩式调用次数,可以找出内存泄露问题,内存占用高的对象申请内存多的函数;
Hot/Cold火焰图:on-cpu和off-cpu火焰图综合展示;
红蓝分叉火焰图:红色表示上升、蓝色表示下降;处理不同版本性能回退问题,一般是对比两个on-cpu火焰图。绘制火焰图
绘制火焰图需要root权限,一般绘制火焰图有三个步骤:事件采集—>堆栈折叠—>火焰图绘制
perf
perf 是 Linux 系统中的一个强大的性能分析工具,用于收集系统的各种性能数据。它以性能事件采样为基础,不仅可以分析系统的各种事件和内核性能,还可以用来分析指定应用程序的性能问题。perf 的使用方法也很丰富,不过不用担心,目前你只要会用
perf record 和 perf report 就够了。要使用 perf,确保你的 Linux 发行版已安装了正确的软件包,例如在 Centos 上,你可以运行以下命令来安装它:
常见用法是
perf top,类似于 top,它能够实时显示占用 CPU 时钟最多的函数或者指令,因此可以用来查找热点函数,使用界面如下所示
输出结果中,第一行包含三个数据,分别是采样数(Samples)、事件类型(event)和事件总数量(Event count)。比如这个例子中,perf 总共采集了 7K 个 CPU 时钟事件,而总事件数则为 1810500000。另外,采样数需要我们特别注意,如果采样数过少(比如只有十几个),那下面的排序和百分比就没什么实际参考价值了。
再往下看是一个表格式样的数据,每一行包含四列,分别是:
第一列 Overhead ,是该符号的性能事件在所有采样中的比例,用百分比来表示。
第二列 Shared ,是该函数或指令所在的动态共享对象(Dynamic Shared Object),如内核、进程名、动态链接库名、内核模块名等。
第三列 Object ,是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。
最后一列 Symbol 是符号名,也就是函数名。当函数名未知时,用十六进制的地址来表示。
还是以上面的输出为例,我们可以看到,占用 CPU 时钟最多的是 perf 工具自身,它的比例也只有 9.89%,说明系统并没有 CPU 性能问题。
perf top 虽然实时展示了系统的性能信息,但它的缺点是并不保存数据,也就无法用于离线或者后续的分析。而 perf record 则提供了保存数据的功能,保存后的数据,需要你用 perf report 解析展示。perf常见的命令有以下几个:
perf top:实时查看当前系统进程的性能统计信息
perf stat:分析指定的程序性能情况
perf list:查看当前软硬件支持的采用性能事件
perf record:记录一段时间内系统或进程的性能事件
perf report:读取perf record生成的perf.data文件明显分析数据参数:
-p 执行线程或进程pid,可指定多个,使用逗号隔开
-F 每个cpu每秒的采用次数
-g 开启call-graph记录,及利用函数调用栈中的信息来追踪程序执行的路径和调用关系
-o 指定输出文件,默认输出文件为perf.data
-a 收集所有cpu的系统数据
示例
火焰图绘制工具
Flame Graph 工程实现了一套生成火焰图的脚本,可以直接clone下来使用,GitHub地址:https://github.com/brendangregg/FlameGraph
使用perf script工具对采用数据perf.data进行解析
使用 stackcollaps-perf.pl 将 perf 解析的 perf.unfold 中的符号进行折叠
最后使用 flamegraph.pl 工具生成火焰图
可以将上数命令整合程一条指令
将生成的火焰图导出,使用浏览器打开即可
- 链接:www.qianshuai.cn/article/2477d4cc-a92a-8004-a77a-c3d0fba07a08
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。







