欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

linux 性能优化实战

程序员文章站 2022-05-16 22:38:23
...

CPU

平均负载

输入 topuptime 命令查看 linux 负载情况

18:22:00 up 63 days, 16:50,  1 user,  load average: 0.15, 0.14, 0.18

18:22:00 当前时间
up 63 days, 16:50 运行时间
1 user 正在登录用户数量
load average: 0.15, 0.14, 0.18 平均负载时间:1分钟、5分钟、15分钟

什么是平均负载?

平均负载是指单位时间内,系统处于 可运行状态不可中断状态 的平均进程数,也就是 平均活跃进程数

  • 可运行状态:正在使用 CPU 或者正在等待 CPU 的进程

  • 不可中断状态:正处于内核态关键流程中的进程

最理想的就是每个 CPU 上都刚好运行着一个进程,这样每个 CPU 都得到了充分利用

例:当平均负载为 2 时,2 颗逻辑核心,意味着刚好占完;4 颗时,意味着 CPU 一半空闲;单颗时,意味着一半的进程竞争不到

平均负载为多少时合理?

三个负载时间段

  • 如果 1 分钟、5 分钟、15 分钟的三个值基本相同,或者相差不大,那就说明系统负载很平稳。
  • 但如果 1 分钟的值远小于 15 分钟的值,就说明系统最近 1 分钟的负载在减少,而过去 15 分钟内却有很大的负载。
  • 反过来,如果 1 分钟的值远大于 15 分钟的值,就说明最近 1 分钟的负载在增加,这种增加有可能只是临时性的,也有可能还会持续增加下去,所以就需要持续观察。一旦 1 分钟的平均负载接近或超过了 CPU 的个数,就意味着系统正在发生过载的问题,这时就得分析调查是哪里导致的问题,并要想办法优化了。

当平均负载高于 CPU 数量 70% 的时候,就应该分析排查负载高的问题了

平均负载与 CPU 使用率

平均负载是指单位时间内,处于可运行状态和不可中断状态的进程数。

所以,它不仅包括了 正在使用 CPU 的进程,还包括 等待 CPU等待 I/O 的进程

而 CPU 使用率,是单位时间内 CPU 繁忙情况的统计。比如:

  • CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;
  • I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;
  • 大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高。

stress 场景模拟

stress 是一个 Linux 系统压力测试工具,这里我们用作异常进程模拟平均负载升高的场景。

sysstat 包含了常用的 Linux 性能工具,用来监控和分析系统的性能。

  • mpstat 是一个常用的多核 CPU 性能分析工具,用来实时查看每个 CPU 的性能指标,以及所有 CPU 的平均指标。
  • pidstat 是一个常用的进程性能分析工具,用来实时查看进程的 CPU、内存、I/O 以及上下文切换等性能指标。

场景一:CPU 密集型

# 模拟一个 CPU 使用率 100% 的场景
stress --cpu 1 --timeout 600
# 查看平均负载
uptime
# mpstat 查看 CPU 使用率的变化情况
mpstat -P ALL 5 
# pidstat 查询进程信息
pidstat -u 5 1

场景二:IO 密集型

# 模拟 I/O 压力,即不停地执行 sync
stress -i 1 --timeout 600
# 查看平均负载
uptime
# mpstat 查看 CPU 使用率的变化情况
mpstat -P ALL 5 
# pidstat 查询进程信息
pidstat -u 5

场景三:大量进程

# 模拟 8 个进程
stress -c 8 --timeout 600
# 查看平均负载
uptime
# mpstat 查看 CPU 使用率的变化情况
mpstat -P ALL 5 
# pidstat 查询进程信息
pidstat -u 5

CPU 上下文

Linux 支持远大于 CPU 数量的任务同时运行。这些任务实际上并不是真的在同时运行,而是因为系统在很短的时间内,将 CPU 轮流分配给它们

在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,需要系统事先帮它设置好 CPU 的 寄存器程序计数器,即 CPU 上下文

程序计数器也是寄存器的一种

什么是上下文切换?

先把前一个任务的 CPU 上下文保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

根据任务的不同,CPU 的上下文切换分为 进程上下文切换线程上下文切换 以及 中断上下文切换

Linux 特权等级

Linux 按照特权等级,把进程的运行空间分为 内核空间用户空间

  • 内核空间(Ring 0)具有最高权限,可以直接访问所有资源;
  • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源

linux 性能优化实战

进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。

特权模式切换

系统调用过程:一次系统调用的过程,发生了两次 CPU 上下文切换

  1. CPU 寄存器里原来用户态的指令位置,需要先保存起来。CPU 寄存器需要更新为内核态指令的新位置。跳转到内核态运行内核任务。

  2. 系统调用结束后,CPU 寄存器需要 恢复 原来保存的用户态,然后再切换到用户空间,继续运行进程

进程上下文切换

进程是由内核来管理和调度的,进程的切换只能发生在 内核态

因此,进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。

进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。

Linux 通过 TLB 来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。

其他进程什么时候被 CPU 运行?

  1. 时间片耗尽
  2. 资源不足
  3. 主动挂起,如 sleep
  4. 有优先级更高的进程运行时
  5. 硬件中断,执行中断服务程序

线程上下文切换

线程是调度的基本单位,进程则是资源拥有的基本单位

  • 当进程只有一个线程时,可以认为进程就等于线程。
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
  • 线程也有自己的私有数据,这些在上下文切换时也是需要保存的。

线程上下文切换分为两种情况

  1. 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
  2. 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,只需要切换线程的私有数据、寄存器等不共享的数据。

虽然同为上下文切换,但同进程内的线程切换,要比多进程间的切换消耗更少的资源,而这,也正是多线程代替多进程的一个优势。

中断上下文切换

为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行

对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。

怎么查看系统的上下文切换情况

sysstat 包中的 vmstat 工具

procs -----------memory---------- ---swap--- ----io---- -sys-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
  • cs:是每秒上下文切换的次数。
  • in:则是每秒中断的次数。
  • r:是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
  • b:则是处于不可中断睡眠状态的进程数。

vmstat 只给出了系统总体的上下文切换情况,要想查看每个进程的详细情况,使用 pidstat -w

20时37分42秒   UID       PID   cswch/s nvcswch/s  Command
  • 每秒自愿上下文切换(cswch/s):进程无法获取资源导致的上下文切换
  • 每秒非自愿上下文切换(nvcswch/s):时间片已到,被系统强制调度的上下文切换

sysbench 场景模拟

sysbench 是一个多线程的基准测试工具,一般用来评估不同系统参数下的数据库负载情况。在此模拟上下文切换过多的问题

模拟多线程系统调度瓶颈

# 以 10 个线程运行 5 分钟的基准测试,模拟多线程切换的问题
sysbench --threads=10 --max-time=300 threads run
# 查看上下文切换情况,5 秒输出一次
vmstat 5
# mpstat 查看 CPU 使用率的变化情况
mpstat -P ALL 5 
# pidstat 查询进程信息
pidstat -wt
# 查看中断使用情况
watch -d cat /proc/interrupts

观察发现

  • 就绪队列从 0 变到 8,每秒中断次数从 2 位数变为 4 位数,每秒上下文切换从 3 位数变为 7 位数
  • 系统调用占用 80% 的 CPU
  • sysbench 该进程的上下文切换不多,但是他的子进程切换是 5 位数
  • 变化速度最快的是 重调度中断(RES),这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为 处理器间中断(Inter-Processor Interrupts,IPI)

每秒上下文切换多少次才算正常?

这个数值其实取决于系统本身的 CPU 性能。在我看来,如果系统的上下文切换次数比较稳定,那么从数百到一万以内,都应该算是正常的

  • 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;
  • 非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
  • 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。

CPU 使用率

节拍

为了维护 CPU 时间,Linux 通过事先定义的节拍率(内核中表示为 HZ),触发时间中断,并使用全局变量 Jiffies 记录了开机以来的节拍数。每发生一次时间中断,Jiffies 的值就加 1

# 查看节拍率
grep 'CONFIG_HZ=' /boot/config-$(uname -r)

节拍率 HZ 是内核选项,所以用户空间程序并不能直接访问。为了方便用户空间程序,内核还提供了一个用户空间节拍率 USER_HZ,它总是固定为 100

# 系统的 CPU 和任务统计信息
cat /proc/stat

每列数字依次代表

  • user(通常缩写为 us),代表用户态 CPU 时间。注意,它不包括下面的 nice 时间,但包括了 guest 时间。
  • nice(通常缩写为 ni),代表低优先级用户态 CPU 时间,也就是进程的 nice 值被调整为 1-19 之间时的 CPU 时间。这里注意,nice 可取值范围是 -20 到 19,数值越大,优先级反而越低。
  • system(通常缩写为 sys),代表内核态 CPU 时间。
  • idle(通常缩写为 id),代表空闲时间。注意,它不包括等待 I/O 的时间(iowait)。
  • iowait(通常缩写为 wa),代表等待 I/O 的 CPU 时间。
  • irq(通常缩写为 hi),代表处理硬中断的 CPU 时间。
  • softirq(通常缩写为 si),代表处理软中断的 CPU 时间。
  • steal(通常缩写为 st),代表当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间。
  • guest(通常缩写为 guest),代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。
  • guest_nice(通常缩写为 gnice),代表以低优先级运行虚拟机的时间。

统计工具,每隔一段时间,就会通过节拍差值计算 CPU 使用率

怎么查看 CPU 使用率

  • top 显示了系统总体的 CPU 和内存使用情况,以及各个进程的资源使用情况。
  • ps 则只显示了每个进程的资源使用情况。
  • pidstat 查看每个进程的详细情况
    • 用户态 CPU 使用率 (%usr)
    • 内核态 CPU 使用率(%system)
    • 运行虚拟机 CPU 使用率(%guest)
    • 等待 CPU 使用率(%wait)
    • 以及总的 CPU 使用率(%CPU)

perf 工具

perf 工具适合在第一时间分析进程的 CPU 问题

  1. perf top 能够实时显示占用 CPU 时钟最多的函数或者指令,因此可以用来查找热点函数
    1. 第一列 Overhead ,是该符号的性能事件在所有采样中的比例,用百分比来表示。
    2. 第二列 Shared ,是该函数或指令所在的动态共享对象(Dynamic Shared Object),如内核、进程名、动态链接库名、内核模块名等。
    3. 第三列 Object ,是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。
    4. 最后一列 Symbol 是符号名,也就是函数名。当函数名未知时,用十六进制的地址来表示。
  2. perf recordperf report,用于记录与解析记录

为什么找不到高 CPU 的应用?

碰到常规问题无法解释的 CPU 使用率情况时,首先要想到有可能是短时应用导致的问题,比如有可能是下面这两种情况。

  • 第一,应用里直接调用了其他二进制程序,这些程序通常运行时间比较短,通过 top 等工具也不容易发现
  • 第二,应用本身在不停地崩溃重启,而启动过程的资源初始化,很可能会占用相当多的 CPU

对于这类进程,我们可以用 pstree 或者 execsnoop 找到它们的父进程,再从父进程所在的应用入手,排查问题的根源。

不可中断进程与僵尸进程

Linux 进程状态

  • R:可运行或运行状态,进程在 CPU 的就绪队列中,正在运行或者正在等待运行

  • D:不可中断状态睡眠,表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断

  • Z:僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源

  • S:可中断状态睡眠,进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态

  • I:空闲状态,用在不可中断睡眠的内核线程上

    D 状态的进程会导致平均负载升高, I 状态的进程却不会

  • T:暂停或者跟踪状态

向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,进程又会恢复运行

而当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行
  • X:死亡状态,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它

不可中断进程问题

不可中断状态,这其实是为了保证进程数据与硬件状态一致,并且正常情况下,不可中断状态在很短时间内就会结束。所以,短时的不可中断状态进程,我们一般可以忽略。

但如果系统或硬件发生了故障,进程可能会在不可中断状态保持很久,甚至导致系统中出现大量不可中断进程。这时,你就得注意下,系统是不是出现了 I/O 等性能问题。

解决方案找到不可中断进程分析问题

僵尸进程问题

当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源。

而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。

如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程。

一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建。

解决方案找到僵尸进程的父进程分析问题

进程组与会话

  • 进程组:一组相互关联的进程,如每个子进程都是父进程所在组的成员
  • 会话:共享同一个控制终端的一个或多个进程组

通过 SSH 登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话。

我们在终端中运行的命令以及它们的子进程,就构成了一个个的进程组。在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。

Linux 软中断

中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力

由于中断处理程序会打断其他进程的运行,所以,为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行。如果中断本身要做的事情不多,那么处理起来也不会有太大问题;但如果中断要处理的事情很多,中断服务程序就有可能要运行很长时间。

特别是,中断处理程序在响应中断时,还会临时关闭中断。这就会导致上一次中断处理完成之前,其他中断都不能响应,也就是说中断有可能会丢失。

Linux 中断处理

Linux 将中断处理过程分成了两个阶段,也就是 上半部和下半部

  • 上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。(硬中断)
  • 下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。(软中断)

上半部会打断 CPU 正在执行的任务,然后立即执行中断处理程序。而下半部以内核线程的方式执行,并且每个 CPU 都对应一个软中断内核线程(ksoftirqd/CPU 编号)

软中断不只包括了刚刚所讲的硬件设备中断处理程序的下半部,一些内核自定义的事件也属于软中断,比如内核调度和 RCU 锁

查看软中断和内核线程

  • /proc/softirqs 提供了软中断的运行情况
  • /proc/interrupts 提供了硬中断的运行情况

第一,要注意软中断的类型,软中断包括了 10 个类别,分别对应不同的工作类型

第二,要注意同一种软中断在不同 CPU 上的分布情况。正常情况下,同一种中断在不同 CPU 上的累积次数应该差不多

TASKLET 在不同 CPU 上的分布并不均匀。TASKLET 是最常用的软中断实现机制,每个 TASKLET 只运行一次就会结束 ,并且只在调用它的函数所在的 CPU 上运行。

因此,使用 TASKLET 特别简便,当然也会存在一些问题,比如说由于只在一个 CPU 上运行导致的调度不均衡,再比如因为不能在多个 CPU 上并行运行带来了性能限制

SYN Flood 攻击

SYN Flood 攻击正是利用了TCP连接的三次握手。

假设一个用户向服务器发送了 SYN 报文(第一次握手)后突然死机或掉线,那么服务器在发出 SYN+ACK 应答报文(第二次握手)后是无法收到客户端的 ACK 报文的(第三次握手无法完成),这种情况下服务器端一般会重试(再次发送 SYN+ACK 给客户端)并等待一段时间后丢弃这个未完成的连接。这段时间的长度我们称为 SYN Timeout,一般来说这个时间是分钟的数量级(大约为30秒-2分钟)

一个用户出现异常导致服务器的一个线程等待1分钟并不会对服务器端造成什么大的影响,但如果有大量的等待丢失的情况发生,服务器端将为了维护一个非常大的半连接请求而消耗非常多的资源。我们可以想象大量的保存并遍历也会消耗非常多的 CPU 时间和内存,再加上服务器端不断对列表中的 IP 进行SYN+ACK 的重试,服务器的负载将会变得非常巨大。如果服务器的 TCP/IP 栈不够强大,最后的结果往往是堆栈溢出崩溃。相对于攻击数据流,正常的用户请求就显得十分渺小,服务器疲于处理攻击者伪造的TCP连接请求而无暇理睬客户的正常请求,此时从正常客户会表现为打开页面缓慢或服务器无响应

解决方案

  1. 降低SYN timeout时间,使得主机尽快释放半连接的占用
  2. 采用 SYN cookie 设置,如果短时间内连续收到某个IP的重复 SYN 请求,则认为受到了该 IP 的攻击,丢弃来自该IP的后续请求报文
  3. 采用防火墙等外部网络安全设施也可缓解 SYN 泛洪攻击

CPU 性能优化

千万避免过早优化

怎么评估性能优化效果

  1. 确定性能的量化指标。
  2. 测试优化前的性能指标。
  3. 测试优化后的性能指标。

不要局限在单一维度的指标上,你至少要从应用程序和系统资源这两个维度,分别选择不同的指标。比如,以 Web 应用为例:

  • 应用程序的维度,我们可以用吞吐量和请求延迟来评估应用程序的性能。
  • 系统资源的维度,我们可以用 CPU 使用率来评估系统的 CPU 使用情况。

在进行性能测试时,有两个特别重要的地方你需要注意下。

  1. 要避免性能测试工具干扰应用程序的性能

  2. 避免外部环境的变化影响性能指标的评估

多个性能问题同时存在要怎么选择?

“二八原则”,也就是说 80% 的问题都是由 20% 的代码导致的。并不是所有的性能问题都值得优化

  1. 如果发现是系统资源达到了瓶颈,比如 CPU 使用率达到了 100%,那么首先优化的一定是系统资源使用问题。完成系统资源瓶颈的优化后,我们才要考虑其他问题。

  2. 针对不同类型的指标,首先去优化那些由瓶颈导致的,性能指标变化幅度最大的问题。比如产生瓶颈后,用户 CPU 使用率升高了 10%,而系统 CPU 使用率却升高了 50%,这个时候就应该首先优化系统 CPU 的使用。

有多种优化方法时要如何选择?

性能优化并非没有成本。性能优化通常会带来复杂度的提升,降低程序的可维护性,还可能在优化一个指标时,引发其他指标的异常。

例:DPDK 是一种优化网络处理速度的方法,它通过绕开内核网络协议栈的方法,提升网络的处理能力。

不过它有一个很典型的要求,就是要独占一个 CPU 以及一定数量的内存大页,并且总是以 100% 的 CPU 使用率运行。所以,如果你的 CPU 核数很少,就有点得不偿失了

应用程序优化

  • 编译器优化:很多编译器都会提供优化选项,适当开启它们,在编译阶段你就可以获得编译器的帮助,来提升性能
  • 算法优化:使用复杂度更低的算法,可以显著加快处理速度
  • 异步处理:使用异步处理,可以避免程序因为等待某个资源而一直阻塞,从而提升程序的并发处理能力。
  • 多线程代替多进程:前面讲过,相对于进程的上下文切换,线程的上下文切换并不切换进程地址空间,因此可以降低上下文切换的成本。
  • 善用缓存:经常访问的数据或者计算过程中的步骤,可以放到内存中缓存起来,这样在下次用时就能直接从内存中获取,加快程序的处理速度。

系统优化

优化 CPU 的运行,一方面要充分利用 CPU 缓存的本地性,加速缓存访问;另一方面,就是要控制进程的 CPU 使用情况,减少进程间的相互影响。

  • CPU 绑定:把进程绑定到一个或者多个 CPU 上,可以提高 CPU 缓存的命中率,减少跨 CPU 调度带来的上下文切换问题。
  • CPU 独占:跟 CPU 绑定类似,进一步将 CPU 分组,并通过 CPU 亲和性机制为其分配进程。这样,这些 CPU 就由指定的进程独占,换句话说,不允许其他进程再来使用这些 CPU。
  • 优先级调整:使用 nice 调整进程的优先级,正值调低优先级,负值调高优先级。优先级的数值含义前面我们提到过,忘了的话及时复习一下。在这里,适当降低非核心应用的优先级,增高核心应用的优先级,可以确保核心应用得到优先处理。
  • 为进程设置资源限制:使用 Linux cgroups 来设置进程的 CPU 使用上限,可以防止由于某个应用自身的问题,而耗尽系统资源。
  • NUMA(Non-Uniform Memory Access)优化:支持 NUMA 的处理器会被划分为多个 node,每个 node 都有自己的本地内存空间。NUMA 优化,其实就是让 CPU 尽可能只访问本地内存。
  • 中断负载均衡:无论是软中断还是硬中断,它们的中断处理程序都可能会耗费大量的 CPU。开启 irqbalance 服务或者配置 smp_affinity,就可以把中断处理过程自动负载均衡到多个 CPU 上。

Memory

LInux 内存

虚拟内存

大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。

Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。进程就可以很方便地访问内存,更确切地说是访问虚拟内存。

虚拟地址空间的内部又被分为 内核空间用户空间 两部分

进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

内存映射

并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过 **内存映射 **来管理的

页表实际上存储在 CPU 的内存管理单元 MMU 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。

而当进程访问的虚拟地址在页表中查不到时,系统会产生一个 缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

TLB 其实就是 MMU 中页表的高速缓存。由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能。

不过要注意,MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是 4 KB 大小。这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间。

多级页表与大页

多级页表:内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。

Linux 分为五部分,前四个用于选择页,最后一个表示页内偏移

linux 性能优化实战

大页:比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页常用在使用大量内存的进程上,如 Oracle、DPDK 等

虚拟内存空间分布

linux 性能优化实战

  1. 只读段,包括代码和常量等。
  2. 数据段,包括全局变量等。
  3. 堆,包括动态分配的内存,从低地址开始向上增长。
  4. 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。
  5. 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。

堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap()

内存分配

C 语言的 malloc() 函数对应系统调用有两种实现方式 brk()mmap()

  1. 小块内存(小于 128K),使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。可以减少缺页异常的发生,提高内存访问效率。频繁的内存分配和释放会造成内存碎片。释放时并不立即归还系统,而是缓存起来重复利用
  2. 大块内存(大于 128K),使用内存映射 mmap() 来分配,是在文件映射段找一块空闲内存分配出去。会在释放时直接归还系统,每次 mmap 都会发生缺页异常。频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大

这两种调用发生后,其实并没有真正分配内存。只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存

缺页异常有以下两种

  • 可以直接从物理内存中分配时,被称为次缺页异常。
  • 需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常。

内存回收

系统也不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存

  • 回收缓存,回收最近使用最少的内存页面
  • 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中(Swap
  • 杀死进程,内存紧张时系统通过 OOM(Out of Memory),直接杀掉进程。(oom_score 评分

交换分区(Swap):把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。

oom_score: 为每个进程的内存使用情况进行评分

  1. 消耗内存越大,评分越低
  2. 占用 CPU 越多,评分越高

内存泄漏

堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数 free() 来释放它们。如果应用程序没有正确释放堆内存,就会造成内存泄漏。

  • 只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。
  • 数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。
  • 最后一个内存映射段,包括动态链接库和共享内存,其*享内存由程序动态分配和管理。所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。

内存泄漏的危害非常大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。内存泄漏不断累积,甚至会耗尽系统内存。

虽然,系统最终可以通过 OOM (Out of Memory)机制杀死进程,但进程在 OOM 前,可能已经引发了一连串的反应,导致严重的性能问题。

比如,其他需要内存的进程,可能无法分配新的内存;内存不足,又会触发系统的缓存回收以及 SWAP 机制,从而进一步导致 I/O 的性能问题等等。

内存查看

free 工具

              total        used        free      shared  buff/cache   available
Mem:        1882192      571676       81268         656     1229248     1122384
Swap:             0           0           0
              总内存      使用内存     未用内存     共享内存         缓存    可分配内存

available 不仅包含未使用内存,还包括了可回收的缓存

top 工具按下 M

top - 13:59:07 up 64 days, 12:27,  1 user,  load average: 0.00, 0.02, 0.07
Tasks:  83 total,   1 running,  82 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.7 us,  1.0 sy,  0.0 ni, 98.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1882192 total,    82456 free,   569760 used,  1229976 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  1124308 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                   
 3849 mongod    20   0 1558120  78368   3952 S  1.0  4.2 182:32.33 mongod                    
 6487 root      20   0  158192   9076   1760 S  0.3  0.5  17:39.50 barad_agent               
 6488 root      20   0  611236  13484   1980 S  0.3  0.7  91:57.06 barad_agent               
26147 root      20   0  945688  24956   6728 S  0.3  1.3  14:53.01 YDService
  • VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存
  • RES 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 Swap 和共享内存
  • SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段
  • %MEM 是进程使用物理内存占系统总内存的百分比

Buffer 与 Cache

  • Buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。
  • Cache(Page Cache) 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与 SReclaimable 之和。

Buffers、Cached、Sreclaimable 是什么?

  • Buffers 是对原始磁盘块的临时存储,也就是用来 缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。
  • Cached 是从磁盘读取文件的页缓存,也就是用来 缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。
  • SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。

cache 只会用来缓存文件读取的数据吗?写入的数据会缓存吗?

buffer 只会用来缓存磁盘写入的数据吗?读取的数据会缓存吗?

看以下实验

磁盘写与文件写案例

# 减少缓存对实验的影响,每次实验前都线清除缓存
echo 3 > /proc/sys/vm/drop_caches

场景 1:文件写案例

# 注意观察 buffer cache bi bo
vmstat
# 读取随机设备,生成一个 500MB 大小的文件
dd if=/dev/urandom of=/tmp/file bs=1M count=500
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
0  0      0 1143080  6764 152028    0    0    40   195  324  591  1  1 98  0  0
1  0      0 948236   6836 344144    0    0   119 15814  774  835  2 28 70  0  0
0  0      0 628052   6884 666488    0    0    46 64845  952  657  1 50 47  3  0
0  0      0 627284   6900 666512    0    0     3 22119  414  771  1  2 96  1  0
0  0      0 626952   6908 666520    0    0     0    29  351  707  1  1 98  0  0

观察发现文件写入时 cache 也在增长,当文件写入结束后 cache 也停止了增长

场景 2:磁盘写案例

需要你的系统配置多块磁盘,并且磁盘分区 /dev/sdb1 还要处于未使用状态

# 然后运行 dd 命令向磁盘分区 /dev/sdb1 写入 2G 数据
dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache    si   so    bi    bo   in   cs us sy id wa st
1  0      0 7093352 631800 110520    0    0     0     0   23  223  0 50 50  0  0
1  1      0 6930056 790520 114980    0    0     0 12804   23  168  0 50 42  9  0
1  0      0 6757204 949240 119396    0    0     0 183804  24  191  0 53 26 21  0
1  1      0 6591516 1107960 123840   0    0     0 77316   22  232  0 52 16 33  0

观察发现磁盘写入时 buff 在增长,当文件写入结束后 buff 停止了增长

磁盘读与文件读案例

场景 3:文件读案例

# 读取文件
dd if=/tmp/file of=/dev/null
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----
r  b   swpd   free   buff  cache    si   so    bi    bo   in   cs us sy id wa st
0  0      0 1156448   944 147188    0    0      0     0  229  458  2  0 98  0  0
0  1      0 1139368   952 163440    0    0  16292     0  316  557  2  4 86  8  0
0  1      0 1028904   952 274068    0    0 110592     0  764  394  9 26  0 65  0
0  1      0 920376    960 384660    0    0 110592   216 1285 1421  8 29  0 63  0
0  1      0 809652    960 495232    0    0 110592    20 1056 1041  9 25  0 66  0
1  0      0 703136    960 601760    0    0 106496     0  808  450 10 30  0 60  0
0  0      0 645640    964 659472    0    0  57712     0  519  433  5 16 47 31  0

观察发现文件读时 cache 在增长,当文件读结束后 cache 停止了增长

场景 4:磁盘读案例

# 从磁盘分区 /dev/sda1 中读取数据,写入空设备
dd if=/dev/sda1 of=/dev/null bs=1M count=1024
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free    buff  cache   si   so    bi    bo   in  cs  us sy id wa st
0  0      0 7225880   2716 608184    0    0    0      0   48  159  0  0 99  0  0
0  1      0 7199420  28644 608228    0    0 25928     0   60  252  0  1 65 35  0
0  1      0 7167092  60900 608312    0    0 32256     0   54  269  0  1 50 49  0
0  1      0 7134416  93572 608376    0    0 32672     0   53  253  0  0 51 49  0
0  1      0 7101484 126320 608480    0    0 32748     0   80  414  0  1 50 49  0

观察发现磁盘读时 buff 也在增长,当磁盘读结束后 buff 也停止了增长

文件、磁盘读写案例总结

  1. 与文件相关的读写 cache 都会使用
  2. 与磁盘相关的读写 buffer 都会使用

磁盘是一个块设备,可以划分为不同的分区;在分区之上再创建文件系统,挂载到某个目录,之后才可以在这个目录中读写文件。

这里提到的 “文件” 是普通文件,磁盘是块设备文件

在读写普通文件时,会经过文件系统,由文件系统负责与磁盘交互;而读写磁盘或者分区时,就会跳过文件系统,也就是所谓的“裸I/O“。这两种读写方式所使用的缓存是不同的,也就是文中所讲的 Cache 和 Buffer 区别。

缓存命中率

所谓缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。

命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。

  • cachestat 提供了整个操作系统缓存的读写命中情况。
  • cachetop 提供了每个进程的缓存命中情况。
sudo yum update
sudo yum install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
export PATH=$PATH:/usr/share/bcc/tools

cachestat 参数

HITS   MISSES  DIRTIES HITRATIO   BUFFERS_MB  CACHED_MB
命中     未命中  新增脏页    命中率    buffer大小  cache大小

cachetop 参数

HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
命中      未命中    新增脏页    读命中率     写命中率

Swap

Swap 机制

缓存缓冲区、通过内存映射获取的 文件映射页,通常被叫做 **文件页(**File-backed Page)。

大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。

这些脏页,一般可以通过两种方式写入磁盘。

  • 可以在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中;
  • 也可以交给系统,由内核线程 pdflush 负责这些脏页的刷新。

除了文件页,应用程序动态分配的堆内存(匿名页),这些内存在分配后很少被访问,也是一种资源浪费。可以把它们暂时先存在磁盘里,释放内存给其他更需要的进程(Swap 机制

Swap 就是把一块磁盘空间或者一个本地文件,当成内存来使用

  • 换出:把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。
  • 换入:进程再次访问这些内存的时候,把它们从磁盘读到内存中来。

kswapd0

有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存这个过程通常被称为 直接内存回收

除了直接内存回收,还有一个专门的内核线程用来定期回收内存,也就是 kswapd0

linux 性能优化实战

  • 剩余内存小于 页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。
  • 剩余内存落在 页最小阈值页低阈值 中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。
  • 剩余内存落在页低阈值页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。
  • 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力

NUMA 与 Swap

你明明发现了 Swap 升高,可系统剩余内存还多着呢。为什么剩余内存很多的情况下,也会发生 Swap 呢?

在 NUMA 架构下,多个处理器被划分到不同 Node 上,且 每个 Node 都拥有自己的本地内存空间

而同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等

你可以通过 numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使用情况

内存阈值可以通过 /proc/zoneinfo 查看

某个 Node 内存不足时,有以下四种模式

  • 默认的 0 ,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。
  • 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。

swappiness

  • 对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。
  • 而对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存。

Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度。

swappiness 的范围是 0-100,数值越大,越积极使用 Swap

这并不是内存的百分比,而是调整 Swap 积极程度的权重,即使你把它设置成 0 还是会发生 Swap

分配 Swap

使用 free 查看

              total        used        free      shared  buff/cache   available
Mem:        1882192      569600       78440         708     1234152     1138252
Swap:             0           0           0

Swap total = 0 意味着,没有配置 Swap

Linux 本身支持两种类型的 Swap,即 Swap 分区Swap 文件

# 创建 Swap 文件
fallocate -l 1G /mnt/swapfile
# 修改权限只有根用户可以访问
chmod 600 /mnt/swapfile
# 配置 Swap 文件
mkswap /mnt/swapfile
# 开启 Swap
swapon /mnt/swapfile

再次 free 查看

              total        used        free      shared  buff/cache   available
Mem:        1882192      576092       67196         708     1238904     1131280
Swap:       8388604           0     8388604

关闭 Swap 后再重新打开,也是一种常用的 Swap 空间清理方法

swapoff -a # 关闭
swapon -a # 开启
  • 禁止 Swap,现在服务器的内存足够大,所以除非有必要,禁用 Swap 就可以了。随着云计算的普及,大部分云平台中的虚拟机都默认禁止 Swap。
  • 如果实在需要用到 Swap,可以尝试降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
  • 响应延迟敏感的应用,如果它们可能在开启 Swap 的服务器中运行,你还可以用库函数 mlock() 或者 mlockall() 锁定内存,阻止它们的内存换出。

IO

Linux 文件系统

在 Linux 中一切皆文件。不仅普通的文件和目录,就连块设备、套接字、管道等,也都要通过统一的文件系统来管理。

索引节点和目录项

Linux 文件系统为每个文件都分配两个数据结构,索引节点(index node)和 目录项(directory

  • 索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到 磁盘 中。所以索引节点同样占用磁盘空间。
  • 目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不同于索引节点,目录项是由 内核 维护的一个内存数据结构,所以通常也被叫做目录项缓存。
  1. 目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据
  2. 磁盘在执行文件系统格式化时,会被分成三个存储区域
    1. 超级块,存储整个文件系统的状态
    2. 索引节点区,用来存储索引节点
    3. 数据块区,则用来存储文件数据

索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,理解为一个文件可以有多个别名

磁盘读写的最小单位是扇区,然而扇区只有 512B 大小,如果每次都读写这么小的单位,效率一定很低。所以,文件系统又把连续的扇区组成了逻辑块,然后每次都以逻辑块为最小单元,来管理数据。常见的逻辑块大小为 4KB,也就是由连续的 8 个扇区组成。

超级块,用来记录文件系统整体的状态

虚拟文件系统

Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是 虚拟文件系统 VFS

VFS 定义了一组所有文件系统都支持的数据结构和标准接口。用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互就可以

Linux 支持各种各样的文件系统。按照存储位置的不同,这些文件系统可以分为三类

  • 基于磁盘 的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。
  • 基于内存 的文件系统( 虚拟文件系统)。不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。
  • 网络 文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、iSCSI 等。

这些文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件

文件系统 IO

第一种,根据是否利用标准库缓存,可以把文件 I/O 分为缓冲 I/O 与非缓冲 I/O。

  • 缓冲 I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件。
  • 非缓冲 I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。

无论缓冲 I/O 还是非缓冲 I/O,它们最终还是要经过系统调用来访问文件系统调用后,还会通过页缓存,来减少磁盘的 I/O 操作。

第二,根据是否利用操作系统的页缓存,可以把文件 I/O 分为直接 I/O 与非直接 I/O。

  • 直接 I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。
  • 非直接 I/O 正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。

数据库等场景中,还会看到跳过文件系统读写磁盘的情况,也就是我们通常所说的裸 I/O。

第三,根据应用程序是否阻塞自身运行,可以把文件 I/O 分为阻塞 I/O 和非阻塞 I/O

  • 阻塞 I/O,是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程
  • 非阻塞 I/O,是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。

第四,根据是否等待响应结果,可以把文件 I/O 分为同步和异步 I/O:

  • 所谓同步 I/O,是指应用程序执行 I/O 操作后,要一直等到整个 I/O 完成后,才能获得 I/O 响应。
  • 所谓异步 I/O,是指应用程序执行 I/O 操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次 I/O 完成后,响应会用事件通知的方式,告诉应用程序。

查看文件系统、索引节点磁盘使用情况

df 命令,就能查看 文件系统 的磁盘空间使用情况

文件系统          容量   已用  可用  已用% 挂载点
devtmpfs        909M     0  909M    0% /dev
tmpfs           920M   24K  920M    1% /dev/shm
tmpfs           920M  684K  919M    1% /run
tmpfs           920M     0  920M    0% /sys/fs/cgroup
/dev/vda1        50G   23G   25G   49% /
tmpfs           184M     0  184M    0% /run/user/0
overlay          50G   23G   25G   49% /var/lib/docker/overlay2/ca0a209cb18b9b5c143b931c9073576d29819cd35abdf5e4615055541e1d25ee/merged

明明你碰到了空间不足的问题,可是用 df 查看磁盘空间后,却发现剩余空间还有很多。这是怎么回事呢?除了文件数据,索引节点也占用磁盘空间

df -i 查看 索引节点 的磁盘空间使用情况

文件系统         Inode  已用(I) 可用(I) 已用(I)% 挂载点
devtmpfs        228K     322    227K       1% /dev
tmpfs           230K       7    230K       1% /dev/shm
tmpfs           230K     510    230K       1% /run
tmpfs           230K      16    230K       1% /sys/fs/cgroup
/dev/vda1       3.2M    172K    3.0M       6% /
tmpfs           230K       1    230K       1% /run/user/0
overlay         3.2M    172K    3.0M       6% /var/lib/docker/overlay2/ca0a209cb18b9b5c143b931c9073576d29819cd35abdf5e4615055541e1d25ee/merged

查看文件系统中的目录项和索引节点缓存

内核使用 Slab 机制,管理目录项和索引节点的缓存。/proc/meminfo 只给出了 Slab 的整体大小,具体到每一种 Slab 缓存,还要查看 /proc/slabinfo 这个文件

或者直接使用 slabtop

 Active / Total Objects (% used)    : 316479 / 370948 (85.3%)
 Active / Total Slabs (% used)      : 14103 / 14103 (100.0%)
 Active / Total Caches (% used)     : 82 / 105 (78.1%)
 Active / Total Size (% used)       : 98230.03K / 104634.67K (93.9%)
 Minimum / Average / Maximum Object : 0.01K / 0.28K / 8.00K

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME                   
 94185  53385  56%    0.10K   2415	 39	 9660K buffer_head
 50127  48991  97%    0.19K   2387	 21	 9548K dentry

磁盘 I/O 是怎么工作的?

磁盘

机械磁盘和固态磁盘的顺序/随机读写性能

  • 对机械磁盘来说,由于随机 I/O 需要更多的 磁头寻道和盘片旋转,它的性能自然要比连续 I/O 慢。
  • 而对固态磁盘来说,存在 “先擦除再写入” 的限制。随机读写会导致大量的垃圾回收,所以相对应的,随机 I/O 的性能比起连续 I/O 来,也还是差了很多。
  • 连续 I/O 还可以通过 预读 的方式,来减少 I/O 请求的次数,这也是其性能优异的一个原因。很多性能优化的方案,也都会从这个角度出发,来优化 I/O 性能。

机械磁盘和固态磁盘最小的读写单位

  • 机械磁盘的最小读写单位是 扇区,一般大小为 512 字节
  • 固态磁盘的最小读写单位是 ,通常大小是 4KB、8KB 等
  • 文件系统会把连续的扇区或页,组成 逻辑块(block),然后以逻辑块作为最小单元来管理数据。常见的逻辑块的大小是 4KB

通用块层

通用块层,其实是处在文件系统和磁盘驱动中间的一个块设备抽象层。它主要有两个功能 。

  • 第一个功能跟虚拟文件系统的功能类似。向上,为文件系统和应用程序,提供访问块设备的标准接口;向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。
  • 第二个功能,通用块层还会给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。

Linux 内核支持四种 I/O 调度算法,分别是 NONE、NOOP、CFQ 以及 DeadLine

  1. NONE,此时磁盘 I/O 调度完全由物理机负责

  2. NOOP ,实际上是一个先入先出的队列,只做一些最基本的请求合并,常用于 SSD 磁盘。

  3. CFQ(Completely Fair Scheduler),也被称为完全公平调度器,是现在很多发行版的默认 I/O 调度器,它为每个进程维护了一个 I/O 调度队列,并按照时间片来均匀分布每个进程的 I/O 请求。CFQ 还支持进程 I/O 的优先级调度

  4. DeadLine 调度算法,分别为读、写请求创建了不同的 I/O 队列,可以提高机械磁盘的吞吐量,并确保达到最终期限(deadline)的请求被优先处理

IO 栈

Linux 存储系统的 I/O 栈,由上到下分为三个层次,分别是 文件系统层通用块层设备层

  • 文件系统层,包括虚拟文件系统和其他各种文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过通用块层,来存储和管理磁盘数据。
  • 通用块层,包括块设备 I/O 队列和 I/O 调度器。它会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层。
  • 设备层,包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作。

磁盘性能指标

  • 使用率,是指磁盘处理 I/O 的时间百分比。过高的使用率(比如超过 80%),通常意味着磁盘 I/O 存在性能瓶颈。
  • 饱和度,是指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求。
  • IOPS(Input/Output Per Second),是指每秒的 I/O 请求数。
  • 吞吐量,是指每秒的 I/O 请求大小。
  • 响应时间,是指 I/O 请求从发出到收到响应的间隔时间。

使用率只考虑有没有 I/O,而不考虑 I/O 的大小。换句话说,当使用率是 100% 的时候,磁盘依然有可能接受新的 I/O 请求。

随机读写比较多的场景中,IOPS 更能反映系统的整体性能;顺序读写较多的场景中,吞吐量才更能反映系统的整体性能。

IO 性能优化

应用程序优化

  1. 可以用追加写代替随机写,减少寻址开销,加快 I/O 写的速度。

  2. 可以借助缓存 I/O ,充分利用系统缓存,降低实际 I/O 的次数。

  3. 可以在应用程序内部构建自己的缓存,或者用 Redis 这类外部缓存系统。这样,一方面,能在应用程序内部,控制缓存的数据和生命周期;另一方面,也能降低其他应用程序使用缓存对自身的影响

  4. 频繁读写同一块磁盘空间时,可以用 mmap 代替 read/write,减少内存的拷贝次数

  5. 同步写的场景中,尽量将写请求合并,而不是让每个请求都同步写入磁盘

  6. 多个应用程序共享相同磁盘时,为了保证 I/O 不被某个应用完全占用,推荐你使用 cgroups 的 I/O 子系统,来限制进程 / 进程组的 IOPS 以及吞吐量

  7. 在使用 CFQ 调度器时,可以用 ionice 来调整进程的 I/O 调度优先级,特别是提高核心应用的 I/O 优先级。ionice 支持三个优先级类:Idle、Best-effort 和 Realtime。其中, Best-effort 和 Realtime 还分别支持 0-7 的级别,数值越小,则表示优先级别越高。

文件系统优化

  1. 据实际负载场景的不同,选择最适合的文件系统

  2. 选好文件系统后,还可以进一步优化文件系统的配置选项,包括文件系统的特性(如 ext_attr、dir_index)、日志模式(如 journal、ordered、writeback)、挂载选项(如 noatime)

  3. 可以优化文件系统的缓存。优化 pdflush 脏页的刷新频率以及脏页的限额;优化内核回收目录项缓存和索引节点缓存的倾向

  4. 不需要持久化时,可以用内存文件系统 tmpfs,以获得更好的 I/O 性能 。tmpfs 把数据直接保存在内存中,而不是磁盘中。

磁盘优化

  1. SSD 替换 HDD
  2. 使用 RAID ,把多块磁盘组合成一个逻辑磁盘
  3. 针对磁盘和应用程序 I/O 模式的特征,我们可以选择最适合的 I/O 调度算法。比方说,SSD 和虚拟机中的磁盘,通常用的是 noop 调度算法。而数据库应用,更推荐使用 deadline 算法
  4. 可以对应用程序的数据,进行磁盘级别的隔离
  5. 顺序读比较多的场景中,我们可以增大磁盘的预读数据
  6. 优化内核块设备 I/O 的选项。可以调整磁盘队列的长度 ,适当增大队列长度,可以提升磁盘的吞吐量(当然也会导致 I/O 延迟增大)
  7. 要注意,磁盘本身出现硬件错误,也会导致 I/O 性能急剧下降

参考资料

倪朋飞:Linux 性能优化实战
鸟哥:鸟哥的 Linux 私房菜

相关标签: Linux linux