原因总结
产生的原因一句话总结就是:等待磁盘 I / O 完成的进程过多,导致进程队列长度过大,但是 cpu 运行的进程却很少,这样就体现到负载过大了,cpu 使用率低。
下面内容是具体的原理分析:
在分析负载为什么高之前先介绍下什么是负载、多任务操作系统、进程调度等相关概念。
什么是负载
什么是负载:负载就是 cpu 在一段时间内正在处理以及等待 cpu 处理的进程数之和的统计信息,也就是 cpu 使用队列的长度统计信息,这个数字越小越好(如果超过 CPU 核心 *0.7 就是不正常)
负载分为两大部分:CPU 负载、IO 负载
例如,假设有一个进行大规模科学计算的程序,虽然该程序不会频繁地从磁盘输入输出,但是处理完成需要相当长的时间。因为该程序主要被用来做计算、逻辑判断等处理,所以程序的处理速度主要依赖于 cpu 的计算速度。此类 cpu 负载的程序称为“计算密集型程序”。
还有一类程序,主要从磁盘保存的大量数据中搜索找出任意文件。这个搜索程序的处理速度并不依赖于 cpu,而是依赖于磁盘的读取速度,也就是输入输出(input/output,I/O). 磁盘越快,检索花费的时间就越短。此类 I / O 负载的程序,称为“I/ O 密集型程序”。
什么是多任务操作系统
Linux 操作系统能够同时处理几个不同名称的任务。但是同时运行多个任务的过程中,cpu 和磁盘这些有限的硬件资源就需要被这些任务程序共享。即便很短的时间间隔内,需要一边在这些任务之间进行切换到一边进行处理,这就是多任务。
运行中的任务较少的情况下,系统并不是等待此类切换动作的发生。但是当任务增加时,例如任务 A 正在 CPU 上执行计算,接下来如果任务 B 和 C 也想进行计算,那么就需要等待 CPU 空闲。也就是说,即便是运行处理某任务,也要等到轮到他时才能运行,此类等待状态就表现为程序运行延迟。
uptime 输出中包含“load average”的数字
[root@localhost ~]# uptime
11:16:38 up 2:06, 4 users, load average: 0.00, 0.02, 0.05
Load average 从左边起依次是过去 1 分钟、5 分钟、15 分钟内,单位时间的等待任务数,也就是表示平均有多少任务正处于等待状态。在 load average 较高的情况下,这就说明等待运行的任务较多,因此轮到该任务运行的等待时间就会出现较大的延迟,即反映了此时负载较高。
进程调度
什么是进程调度:
进程调度也被一些人称为 cpu 上下文切换意思是:CPU 切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、中断)状态,另一个被选定的就绪任务成为当前任务。进程调度包括保存当前任务的运行环境,恢复将要运行任务的运行环境。
在 linux 内核中,每一个进程都存在一个名为“进程描述符”的管理表。该进程描述符会调整为按照优先级降序排序,已按合理的顺序运行进程(任务)。这个调整即为进程调度器的工作。
调度器划分并管理进程的状态,如:
- 等待分配 cpu 资源的状态。
- 等待磁盘输入输出完毕的状态。
下面在说一下进程的状态区别:
下面举例来说明进程状态转变:
这里有三个进程 A、B、C 同时运行。首先,每个进程在生成后都是可运行状态,也就是 running 状态的开始,而不是现在运行状态,由于在 linux 内核中无法区别正在运行的状态和可运行的等待状态,下面将可运行状态和正在运行状态都称为 running 状态。
- 进程 A:running
- 进程 B:running
- 进程 C:running
running 的三个进程立即成为调度对象。此时,假设调度器给进程 A 分配了 CPU 的运行权限。
- 进程 A:running(正在运行)
- 进程 B:running
- 进程 C:running
进程 A 分配了 CPU,所以进程 A 开始处理。进程 B 和 C 则在此等待进程 A 迁出 CPU。假设进程 A 进行若干计算之后,需要从磁盘读取数据。那么在 A 发出读取磁盘数据的请求之后,到请求数据到达之前,将不进行任何工作。此状态称为“因等待 I / O 操作结束而被阻塞”。在 I / O 完成处理前,进程 A 就一直处于等待中,就会转为不可中断睡眠状态(uninterruptible),并不使用 CPU。于是调度器查看进程 B 和进程 C 的优先级计算结果,将 CPU 运行权限交给优先级较高的一方。这里假设进程 B 的优先级高于进程 C。
- 进程 A:uninterruptible(等待磁盘输入输出 / 不可中断状态)
- 进程 B:running(正在运行)
- 进程 C:running
进程 B 刚开始运行,就需要等待用户的键盘输入。于是 B 进入等待用户键盘输入状态,同样被阻塞。结果就变成了进程 A 和进程 B 都是等待输出,运行进程 C。这时进程 A 和进程 B 都是等待状态,但是等待磁盘输入输出和等待键盘输入为不同的状态。等待键盘输入是无限期的事件等待,而读取磁盘则是必须短时间内完成的事件等待,这是两种不同的等待状态。各进程状态如下所示:
进程 A:uninterruptible(等待磁盘输入输出 / 不可中断状态)
进程 B:interruptible(等待键盘输入输出 / 可中断状态)
进程 C:running(正在运行)
这次假设进程 C 在运行的过程中,进程 A 请求的数据从磁盘到达了缓冲装置。紧接着硬盘对内核发起中断信号,内核知道磁盘读取完成,将进程 A 恢复为可运行状态。
- 进程 A:running(正在运行)
- 进程 B:interruptible(等待键盘输入输出 / 可中断状态)
- 进程 C:running(正在运行)
此后进程 C 也会变为某种等待状态。如 CPU 的占用时间超出了上限、任务结束、进入 I / O 等待。一旦满足这些条件,调度器就可以完成从进程 C 到进程 A 的进程状态切换。
负载的意义
负载表示的是“等待进程的平均数”。在上面的进程状态变换过程中,除了 running 状态,其他都是等待状态,那么其他状态都会加入到负载等待进程中吗?
事实证明,只有进程处于运行态(running)和不可中断状态(interruptible)才会被加入到负载等待进程中,也就是下面这两种情况的进程才会表现为负载的值。
- 即便需要立即使用 CPU,也还需等待其他进程用完 CPU
- 即便需要继续处理,也必须等待磁盘输入输出完成才能进行
下面描述一种直观感受的场景说明为什么只有运行态(running)和可中断状态(interruptible)才会被加入负载。
如:在很占用 CPU 资源的处理中,例如在进行动画编码的过程中,虽然想进行其他相同类型的处理,结果系统反映却变得很慢,还有从磁盘读取大量数据时,系统的反映也同样会变的很慢。但是另一方面,无论有多少等待键盘输入输出操作的进程,也不会让系统响应变慢。
什么场景会造成 CPU 低而负载确很高呢?
通过上面的具体分析负载的意义就很明显了,负载总结为一句话就是:需要运行处理但又必须等待队列前的进程处理完成的进程个数。具体来说,也就是如下两种情况:
- 等待被授权予 CPU 运行权限的进程
- 等待磁盘 I / O 完成的进程
cpu 低而负载高也就是说等待磁盘 I / O 完成的进程过多,就会导致队列长度过大,这样就体现到负载过大了,但实际是此时 cpu 被分配去执行别的任务或空闲,具体场景有如下几种。
场景一:磁盘读写请求过多就会导致大量 I / O 等待
上面说过,cpu 的工作效率要高于磁盘,而进程在 cpu 上面运行需要访问磁盘文件,这个时候 cpu 会向内核发起调用文件的请求,让内核去磁盘取文件,这个时候会切换到其他进程或者空闲,这个任务就会转换为不可中断睡眠状态。当这种读写请求过多就会导致不可中断睡眠状态的进程过多,从而导致负载高,cpu 低的情况。
场景二:MySQL 中存在没有索引的语句或存在死锁等情况
我们都知道 MySQL 的数据是存储在硬盘中,如果需要进行 sql 查询,需要先把数据从磁盘加载到内存中。当在数据特别大的时候,如果执行的 sql 语句没有索引,就会造成扫描表的行数过大导致 I / O 阻塞,或者是语句中存在死锁,也会造成 I / O 阻塞,从而导致不可中断睡眠进程过多,导致负载过大。
具体解决方法可以在 MySQL 中运行 show full processlist 命令查看线程等待情况,把其中的语句拿出来进行优化。
场景三:外接硬盘故障,常见有挂了 NFS,但是 NFS server 故障
比如我们的系统挂载了外接硬盘如 NFS 共享存储,经常会有大量的读写请求去访问 NFS 存储的文件,如果这个时候 NFS Server 故障,那么就会导致进程读写请求一直获取不到资源,从而进程一直是不可中断状态,造成负载很高。
结束语:大概内容就是这样,如果有朋友遇到其他场景,欢迎留言补充。
作者:西门飞冰,一名 90 后 it 男,一直在北京工作,热爱运动,热爱冒险,热爱旅行。
关注 民工哥技术之路 微信公众号对话框回复关键字:1024 可以获取一份最新整理的技术干货。