0%

linux 性能优化实战学习笔记

《linux 性能优化实战》学习笔记

CPU 性能

平均负载

在遇到性能问题时,第一时间通过执行 top 或者 uptime 查看系统平均负载,例如:

root@913ecec6c8c3:/workdir# uptime
23:43:40 up  1:13,  0 users,  load average: 1.24, 0.35, 0.89

这几列的含义分别是:

23:43:40                        // 当前时间
up  1:13                        // 系统运行时间
0 users                         // 正在登录的用户数,这里不知道为什么是0
load average: 1.24, 0.35, 0.89  // 系统在过去1分钟,5分钟,15分钟内的平均负载

平均负载 是指在单位时间内,系统处于可运行状态不可中断状态的平均进程数,也就是 平均活跃进程数。可运行状态的进程是指正在使用 CPU 或者正在等待 CPU 的进程数,也就是我们常用 ps 命令看到的处于 R 状态的进程。不可中断状态的进程则是正处于内核态关键流程中的进程,并且这些流程是不可打断的,比如最常见的是等待硬件设备的 I/O 响应,也就是我们在 ps 命令中看到的 D 状态(Uninterruptible Sleep,也称为 Disk Sleep)的进程。

简单点,可以理解为就是单位时间内平均活跃的进程数,举个例子,如果平均负载是2,那么:

  • 当 CPU 数量是 1 的时候,表示过载 100%;
  • 当 CPU 数量是 2 的时候,表示 CPU 刚好被完全利用;
  • 当 CPU 数量是 4 的时候,表示每个 CPU 有 50% 的空闲;

所以说,平均负载是否合理,还要看 CPU 数量,举个简单的例子,如果在单核系统上看到的平均负载分别为:1.73,0.60,7.98,那么说明在过去 1 分钟内系统有 73% 的超载,在过去 15 分钟内,系统有 698% 的超载,总的来说,系统负载呈现下降趋势。

需要注意的是,平均负载与 CPU 是使用率的关系,从定义上来说,平均负载指的是单位时间内处于可运行状态和不可中断状态的进程数,所以不仅包括了正在使用 CPU 的进程,也包括了正在等待 CPU 和 I/O 进程。而 CPU 使用率指的是单位时间内 CPU 繁忙情况的一种统计,跟负载并不完全对应。比如:

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

接下里使用一些工具通过模拟不同场景导致平均负载升高。

CPU 密集型进程

使用 stress 或者 stress-ng 命令模拟一个 CPU 使用率百分百的场景:

root@913ecec6c8c3:/# stress --cpu 1 --timeout 600
stress: info: [124] dispatching hogs: 1 cpu, 0 io, 0 vm, 0 hdd

然后我们观测平均负载的变化:

root@913ecec6c8c3:/# watch -d uptime
...  load average: 1.00, 0.49, 0.20

这里我们会观测到一分钟内的平均负载会慢慢升到 1.00,我们再通过 mpstat 工具查看哪个 CPU 核心遭到了暴击:

root@913ecec6c8c3:/# mpstat -P ALL 5 1
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/26/20     _x86_64_    (2 CPU)
...
21:37:35     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
21:37:40     all   48.69    0.00    0.73    0.00    0.00    0.21    0.00    0.00    0.00   50.37
21:37:40       0    0.61    0.00    1.02    0.00    0.00    0.00    0.00    0.00    0.00   98.37
21:37:40       1   99.14    0.00    0.43    0.00    0.00    0.43    0.00    0.00    0.00    0.00

从上面的结果可以看到,一个 CPU 的使用率已经接近 100% 了,我们再通过 pidstat 命令来看看哪个进程导致的:

root@913ecec6c8c3:/# pidstat -u 5 1
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/26/20     _x86_64_    (2 CPU)

21:39:58      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
21:40:03        0       125  100.00    0.20    0.00    0.00  100.20     1  stress

一目了然,可以看出是我们的模拟工具 stress 进程导致的。

I/O 密集型进程

我们这次再使用 stress-ng 模拟 I/O 压力:

root@913ecec6c8c3:/# stress-ng -i 1 --hdd 1 --timeout 600
stress-ng: info:  [1448] dispatching hogs: 1 io, 1 hdd

然后我们观察平均负载的变化:

root@913ecec6c8c3:/# watch -d uptime
21:56:55 up 57 min,  0 users,  load average: 3.16, 2.02, 1.12

发现一分钟内的平均负载还是会到很高,我们还是通过 mpstat 命令先查看 CPU 使用率的情况:

root@913ecec6c8c3:/# mpstat -P ALL 5
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/26/20     _x86_64_    (2 CPU)

21:57:15     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
21:57:20     all    1.36    0.00   54.51   25.59    0.00   16.69    0.00    0.00    0.00    1.85
21:57:20       0    0.73    0.00   36.67   33.99    0.00   26.16    0.00    0.00    0.00    2.44
21:57:20       1    2.00    0.00   72.75   17.00    0.00    7.00    0.00    0.00    0.00    1.25

跟 CPU 密集型程序区别很大,用户进程占用的CPU比较少,大多都被系统消耗,而且 iowait 比较高,很显然这是有等待 I/O 导致平均负载升高,我们再来查看哪个进程导致的:

root@913ecec6c8c3:/# pidstat -u 5 1
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/26/20     _x86_64_    (2 CPU)

21:56:04      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
21:56:09        0       821    0.20    0.00    0.00    0.20    0.20     1  watch
21:56:09        0      1449    0.40    9.38    0.00    5.19    9.78     1  stress-ng-io
21:56:09        0      1450    1.40   83.03    0.00    5.39   84.43     1  stress-ng-hdd

果然还是我们的压力测试进程导致。

大量进程的场景

根据前面平均负载的定义,平均负载也包括正在等待 CPU 的进程,当系统中运行进程超出 CPU 能力时,就会出现等待 CPU 的进程。这次我们使用 stress 模拟 8 个进程的场景:

root@913ecec6c8c3:/# stress -c 8 --timeout 600
stress: info: [1963] dispatching hogs: 8 cpu, 0 io, 0 vm, 0 hdd

由于我的系统中只有 2 个 CPU,等待 CPU 的进程数明显超过了 CPU 的运行能力,所以系统的 CPU 处于严重的过载状态,平均负载会持续上升:

root@913ecec6c8c3:/# watch -d uptime
22:06:37 up  1:06,  0 users,  load average: 7.43, 3.74, 2.04

我们再去查看两个可怜的 CPU 使用率:

root@913ecec6c8c3:/# mpstat -P ALL 5 1
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/26/20     _x86_64_    (2 CPU)

22:06:11     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
22:06:16     all   99.58    0.00    0.42    0.00    0.00    0.00    0.00    0.00    0.00    0.00
22:06:16       0   99.58    0.00    0.42    0.00    0.00    0.00    0.00    0.00    0.00    0.00
22:06:16       1   99.58    0.00    0.42    0.00    0.00    0.00    0.00    0.00    0.00    0.00

已经被榨干了,毛都不剩,进一步查看罪魁祸首:

root@913ecec6c8c3:/# pidstat -u 5 1
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/26/20     _x86_64_    (2 CPU)

22:08:14      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
22:08:19        0      1964   24.80    0.00    0.00   75.00   24.80     1  stress
22:08:19        0      1965   24.80    0.00    0.00   75.40   24.80     0  stress
22:08:19        0      1966   24.80    0.00    0.00   75.40   24.80     0  stress
22:08:19        0      1967   24.80    0.00    0.00   75.20   24.80     1  stress
22:08:19        0      1968   25.00    0.00    0.00   75.40   25.00     1  stress
22:08:19        0      1969   24.80    0.20    0.00   75.20   25.00     0  stress
22:08:19        0      1970   24.80    0.00    0.00   75.40   24.80     0  stress
22:08:19        0      1971   25.00    0.00    0.00   75.00   25.00     1  stress

两个 CPU 被我们 8 个测试进程瓜分,各占 25%。所以当一旦出现平均负载高的时候,我们要查看各个 CPU 的使用率,避免出现一人干活多人围观的情况,也要分析出是由于等待 CPU 还是等待 I/O 造成的。

CPU 上下文切换

记得之前面试的时候,总跟别人说起多进程(线程)编程和异步编程的区别,总会提到一句:多进(线)程会引发 CPU 上下文切换,消耗过多的CPU资源,异步编程一般是单进程单线程,通过 EPOLL 等事件循环库监听不同的资源状态,不会引起大量的 CPU 切换。但是说实话,我从来没搞懂过,也没见到什么是 CPU 上下文切换。

回顾上节的概念平均负载,其中有一种情况是大量进程等待CPU也会导致负载上什,可是这个时候进程并没有在运行,它为什么还会导致负载上升呢?其实就是内核在做进程的上下文切换。

因为 Linux 是一个多任务的操作系统,它支持远大于 CPU 数量的进程同时运行,当然并不是真的同时在运行,只是因为系统在很短的时间内,将 CPU 轮流分配给他们,所以造成了任务被同时运行的错觉,因为大家都是平等的,所以 CPU 要做到雨露均沾。

CPU 在运行一个任务前,它要知道任务从哪里加载,又从哪里开始,也就是说,需要系统事先帮它设置好 CPU 寄存器程序计数器(Program Counter,PC)。CPU 寄存器是容量小但是速度极快的内存,用来存储程序运行期间产生的中间数据。而程序计数器则是用来存储 CPU 正在执行的指令位置、或者下一条指令执行的位置。它们都是 CPU 在运行任何任务前,必须依赖的信息,所以也叫 CPU 上下文

cpu 上下文切换

那么再来理解 CPU 上下文切换就比较容易了,无非就是操作系统把前一个任务的 CPU 上下文 保存起来,然后加载将要执行任务的上下文。被保存起来的上下文会被存在内核中,再次执行再把它加载进来就行了。

看样子上下文切换只是更新CPU寄存器的值,而寄存器又很快,为什么还是会影响性能,这就要看 CPU 上下文切换的是什么任务了

进程上下文切换

linux 中的进程是有不同特权的,进程的运行空间分为内核空间和用户空间。内核空间用于最高权限,可以直接访问所有资源。用户空间的进程只访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能被允许访问这些资源。

一个进程必可避免的要访问系统资源,比如读取文件,读取内存,所以说一个进程既可以在用户空间运行又可以在内核空间运行。进程在用户空间运行时叫做用户态,在内核空间运行时叫做内核态。从用户态到内核态的转变是需要 系统调用 来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:

  • 首先调用 open() 打开文件
  • 然后调用 read() 读取文件
  • 最后调用 close() 关闭文件

然而,系统调用是要发生上下文切换的,而且一次系统调用发生了两次上下文切换。当从用户态转向内核态时,需要先保存进程用来用户态的指令位置,寄存器信息,接着为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置,最后才是跳转到内核态执行内核任务。系统调用结束后,CPU 寄存器需要恢复到原来的用户态,然后切换回用户空间,继续运行进程。

不过,需要注意的是,系统调用是在同一个进程内执行的,不涉及虚拟内存等进程用户态的资源,也不会切换进程。所以系统调用也被叫做 特权模式切换,但其中的 CPU 上下文切换不可避免。

那么真正的上下文切换是什么样子的?众所周知,进程是由内核管理调度的,因此进程的切换只能发生在内核态,也正因为如此,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。So,进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。

进程上下文切换

据可靠情报,每次上下文切换都需要几十纳秒到数微秒的 CPU 时间,在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间,这也是导致平均负载升高的一个重要因素。

那么 CPU 在何时会切换运行另一个进程,只有在进程调度的时候,才需要切换上下文。Linux 为每个 CPU 都维护了一个就绪队列,将活跃进程(即正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。

一般来说,进程被调度到 CPU 上运行有以下几个原因:

  • 上一个进程执行完了,使用的 CPU 会被释放出来,这时就从就绪队列里面拿出一个运行;

  • 为了保证公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配到各个进程。所以当一个进程的时间片被用完了,就会被挂起,切换到另一个进程。

  • 系统资源不足事务,比如内存不足时,要等到资源满足才可以运行,这个时候进程也会被挂起,并由其他进程运行。

  • 当前进程主动通过 sleep 函数将自己挂起,自然也会重新调度

  • 当有更高优先级的进程需要运行时,当前进程也会被挂起。

  • 发生硬件中断时,CPU 上的进程被挂起,转而执行中断服务进程。

线程上下文切换

线程与进程最大的区别在于,线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程 ;而 进程只是给线程提供了虚拟内存、全局变量等资源。所以,对于线程和进程,我们可以这么理解:

  • 当进程只有一个线程时,可以认为进程就等于线程。

  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在线程上下文切换时是不需要修改的。

  • 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

照这么说,线程切换其实分为两种情况,通进程内和不同进程内:

  • 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。

  • 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

中断上下文切换

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

跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。

对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。

上下文切换分析

过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,缩短进程真正运行的时间,成了系统性能大幅下降的一个元凶。们可以使用 vmstat 这个工具,来查询系统的上下文切换情况。安装方式:

apt-get install sysbench sysstat

来看一个 vmstat 的例子

root@913ecec6c8c3:/workdir# vmstat 5 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free     buff  cache     si   so    bi    bo   in   cs us sy id   wa st
2  0      0   3091192  59644 582512    0    0     1     1    67   1  0   0 100  0  0

其中需要重点关注的四列内容:

  • cs(context switch):每秒钟上下文切换的次数
  • in(interrupt):每秒中断次数
  • r(Running or Runnable) 就绪队列的长度,正在运行和等待CPU的进程数
  • b(Blocked) 处于不可中断睡眠状态的进程数

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

root@913ecec6c8c3:/workdir# pidstat -w 5
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/28/20     _x86_64_    (4 CPU)

22:37:11      UID       PID   cswch/s nvcswch/s  Command
22:37:16        0      1366      0.20      0.00  pidstat

22:37:16      UID       PID   cswch/s nvcswch/s  Command
22:37:21        0      1366      0.20      0.00  pidstat

其中有两列比较有意思,一个是 cswch,表示 自愿上下文切换(voluntary context switches) 次数,另一个是 nvcswch,表示 非自愿上下文切换(non voluntary context switches) 次数。区别在于:

  • 自愿上下文切换,是指进程无法获取资源时,导致的上下文切换。比如,I/O,内存不足,就会发生资源上下文切换;

  • 非自愿上下文切换,则是值进程由于时间片已到,被系统强制调度,进而发生的上下文切换;

这两种切换意味不同的性能问题。

上下文切换模拟

我们将使用 sysbench 工具模拟系统多线程调度的瓶颈(以20个线程运行5分钟基准测试):

root@913ecec6c8c3:/workdir# sysbench --threads=20 --time=300 threads run
sysbench 1.0.18 (using system LuaJIT 2.1.0-beta3)

Running the test with following options:
Number of threads: 20
Initializing random number generator from current time


Initializing worker threads...

Threads started!

然后我们使用 vmstat 查看上下文切换情况:

root@913ecec6c8c3:/# vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r   b   swpd   free   buff  cache    si     so   bi    bo  in    cs  us  sy id  wa st
12  0      0 3088072  59676 583036    0    0     1     1   72    3    0  0  100 0  0
12  0      0 3088064  59676 583036    0    0     0     0 28414 471622 16 75  9  0  0
9   0      0 3088064  59676 583036    0    0     0     0 28676 492647 17 74  9  0  0
10  0      0 3088064  59676 583036    0    0     0     0 25577 507974 15 66 19  0  0
14  0      0 3088064  59676 583036    0    0     0     0 26545 463018 16 74 10  0  0
10  0      0 3088064  59676 583036    0    0     0     0 26747 484506 15 73 12  0  0
9   0      0 3088064  59676 583036    0    0     0     0 26250 454500 18 73  9  0  0
11  0      0 3088064  59676 583036    0    0     0     0 24254 423690 17 73 10  0  0
10  0      0 3086536  59676 583036    0    0     0     0 29882 485909 16 76  8  0  0
13  0      0 3085780  59676 583124    0    0     0     0 28158 460459 16 75  9  0  0
14  0      0 3085780  59676 583124    0    0     0     0 28533 475176 16 73 10  0  0
11  0      0 3085780  59676 583124    0    0     0     0 29070 477886 16 74 10  0  0
9   0      0 3085780  59676 583124    0    0     0     0 28956 471644 16 76  8  0  0
10  0      0 3087804  59684 583116    0    0     0    12 24487 437283 19 72  9  0  0
11  0      0 3087804  59684 583128    0    0     0     0 18452 388913 16 69 15  0  0
10  0      0 3087804  59684 583128    0    0     0     0 28702 478880 17 73  9  0  0

可以看到 cs 列从开始的 3 上升到了 50 多万,然后看到:

  • r 列,就绪队列的长度远远超过了系统 CPU 的个数 4,所以会产生大量的 CPU 竞争;

  • us (user) 和 sy (system) 列:这两列的 CPU 使用率加起来几乎达到 100%,而且系统占用的 CPU 高得多;

  • in 列:中断次数也达到了接近3万,说明中断处理也是一个潜在的问题;

从这可以得出:超越 CPU 运行能力的大量线程导致了 CPU 上下文切换过多,CPU 上下文切换过多导致系统的 CPU 的使用率上升;我们再来使用 pidsta 查看 CPU 和进程上下文切换的情况:

root@913ecec6c8c3:/# pidstat -w -u 1
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/28/20     _x86_64_    (4 CPU)

22:57:52      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
22:57:53        0      1440   65.00  284.00    0.00    0.00  349.00     1  sysbench

22:57:52      UID       PID   cswch/s nvcswch/s  Command
22:57:53        0      1461      1.00      1.00  pidstat

从这里可以很明显的看出,系统使用率的上升是由于 sysbench 导致的,而且大部分 CPU 都是被系统使用,用户态进程并没有占用多少 CPU 时间片。不过,看着输出,感觉上下文切换不对啊。冷静思考一下, Linux 调度的基本单位是线程,我们模拟的也是现成的调度问题,那我们应该查看线程的数据,所以如下:

root@913ecec6c8c3:/# pidstat -wt -u 1
Linux 4.19.76-linuxkit (913ecec6c8c3)     05/28/20     _x86_64_    (4 CPU)

....

22:59:23      UID      TGID       TID   cswch/s nvcswch/s  Command
22:59:24        0         -      1441   3714.56  22385.44  |__sysbench
....
22:59:24        0         -      1451   3168.93  17614.56  |__sysbench
22:59:24        0         -      1452   2867.96  20373.79  |__sysbench
22:59:24        0         -      1453   3141.75  22448.54  |__sysbench
22:59:24        0         -      1454   3378.64  19192.23  |__sysbench
22:59:24        0         -      1455   3065.05  16215.53  |__sysbench
22:59:24        0         -      1456   3720.39  20452.43  |__sysbench
22:59:24        0         -      1457   3349.51  20050.49  |__sysbench
22:59:24        0         -      1458   3919.42  23687.38  |__sysbench
22:59:24        0         -      1459   2959.22  19080.58  |__sysbench
22:59:24        0         -      1460   4139.81  22142.72  |__sysbench
22:59:24        0      1462         -      0.97     17.48  pidstat
22:59:24        0         -      1462      0.97     16.50  |__pidstat

从上面的结果可以看出,sysbench 子线程发生非自愿切换次数相当多,所以我们找到了罪魁祸首,就是因为大量的线程存在,超过了 CPU 运行能力,需要被内核调度,所以发生了大量的上下文切换。

我们之前在用 vmstat 查看系统上下文切换时,发现中断次数也随着升高了不少,中断只属于内核态,所以 pidstat 是无法查看其信息的,我们必须从 /proc/interrupts 中读取相关信息。/proc 实际上是 Linux 的一个虚拟文件系统, 用于内核和用户之间的通信。我们使用 watch -d cat /proc/interrupts 观察变化情况:

中断

我们可以看到变化最快的是两种,时钟中断处理和重调度中断(RES),我中 RES 用来唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为处理器间中断(Inter-Processor Interrupts,IPI)。所以,这里的中断升高还是因为过多任务的调度问题,跟前面上下文切换次数的分析结果是一致的。

最后,每秒多少次上下文切换才算正常,取决于系统本身 CPU 性能,但是当上下文切换次数达到几十万而且居高不下的时候,就很可能出现性能问题,这时候还需要关注上下文切换类型,再做分析,例如:

  • 资源上下文切换过多,说明进程都在等资源;

  • 非自愿上下文切换次数过多,说明进程被强制调度,也就是在争抢 CPU,说明 CPU 的确成了瓶颈;

  • 中断次数变多,说明 CPU 被中断处理程序占用,需要通过查看 /proc/interrupts 查看具体中断类型。

CPU 使用率过高

经常遇到的情况是,我们知道CPU的使用率较高,我们也能够用 topps 以及 pidstat 等工具查到哪个进程在占用大量 CPU,但我们就是无从下手,不知道具体到进程中的哪个函数导致的,在学习 《Linux 性能优化实战》之后,学到两种方法,今天来介绍一下。

perf top

perf top,类似于 top,它能够实时显示占用 CPU 时钟最多的函数或者指令,因此可以用来查找热点函数,使用界面如下所示:

perf-top

输出结果中,第一行包含三个数据,分别是采样数(Samples)、事件类型(event)和事件总数量(Event count)。比如这个例子中,perf 总共采集了 30 个 CPU 时钟事件,而总事件数则为 5024401。另外,采样数需要我们特别注意。如果采样数过少(比如只有十几个),那下面的排序和百分比就没什么实际参考价值了。

再往下看是一个表格式样的数据,每一行包含四列,分别是:

  • 第一列 Overhead ,是该符号的性能事件在所有采样中的比例,用百分比来表示。
  • 第二列 Shared ,是该函数或指令所在的动态共享对象(Dynamic Shared Object),如内核、进程名、动态链接库名、内核模块名等。
  • 第三列 Object ,是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。
  • 第四列 Symbol ,是符号名,也就是函数名。当函数名未知时,用十六进制的地址来表示。
perf record 和 perf report

perf top 虽然实时展示了系统的性能信息,但它的缺点是并不保存数据,也就无法用于离线或者后续的分析。而 perf record 则提供了保存数据的功能,保存后的数据,需要你用 perf report 解析展示

$ perf record # 按Ctrl+C终止采样
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.452 MB perf.data (6093 samples) ]

$ perf report # 展示类似于perf top的报告
实战体验

写一段垃圾的 Go 代码来体验一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"flag"
"math"
)

var cycles = flag.Int("cycles", 100000, "cycles num")

func main() {
flag.Parse()
x := 0.0001
for i := 0; i < *cycles; i++ {
x += math.Sqrt(x)
}
}

编译好之后我们运行:

./perf_linux -cycles 10000000000

在另一个终端我们使用 perf top -g -p 29126 查看指定进程:

perf-top1.png

我们找到占比最多的 main.main 函数也是我们的入口函数,使用方向下键选中,并且按 Enter 键进入:

perf-top2.png

然后再选择 main.main,点击 Enter 键:

perf-top3.png

然后点击 Enter 进入,根据占比我们就大概知道是什么位置了:

perf-top4.png

另外对于一些短命程序导致的 CPU 升高,很难定位,看到的 CPU 被消耗光,但是却找不到根因,可以使用 pstreeperf record 以及 execsnoop 来定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"flag"
"fmt"
"os"
"os/exec"
"sync"
)

var (
cycles = flag.Int("cycles", 10000000000, "cycles times")
threads = flag.Int("threads", 5, "threads num")
)

func main() {
flag.Parse()
var wg sync.WaitGroup
wg.Add(*threads)
for i := 0; i < *threads; i++ {
go func() {
defer wg.Done()
for i := 0; i < *cycles; i++ {
cmd := exec.Command("/usr/bin/stress", "-t", "1", "-d", "1")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println(err)
}
}
}()
}
wg.Wait()
}

然后我们启动并且运行,效果如下视频:

期间我们使用上节用到的 perf recordpref report 命令输出性能报告,并且使用 pstree 命令输出了进程族谱,查看到异常的 stress 进程是哪个父进程启动的,而且使用 execsnoop 监控短时进程,正是由于这种短时进程大量启动导致CPU等待IO,导致貌似空闲的 CPU 没了。

cpu_high.png

进程状态

topps 是最常用的查看进程状态的工具,输出中 S 列(也就是 Status 列)表示进程的状态,经常可以看到 RDZSI 等几个状态,它们分别是什么意思呢?

$ top
PID   USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
28961 root      20   0   43816   3148   4040 R   3.2  0.0   0:00.01 top
620   root      20   0   37280  33676    908 D   0.3  0.4   0:00.01 app
1     root      20   0  160072   9416   6752 S   0.0  0.1   0:37.64 systemd
1896  root      20   0       0      0      0 Z   0.0  0.0   0:00.00 devapp
2     root      20   0       0      0      0 S   0.0  0.0   0:00.10 kthreadd
4     root       0 -20       0      0      0 I   0.0  0.0   0:00.00 kworker/0:0H
6     root       0 -20       0      0      0 I   0.0  0.0   0:00.00 mm_percpu_wq
7     root      20   0       0      0      0 S   0.0  0.0   0:06.37 ksoftirqd/0
  • R 是 Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
  • D 是 Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。
  • Z 是 Zombie 的缩写,它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)。
  • S 是 Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
  • I 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。前面说了,硬件交互导致的不可中断进程用 D 表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用 Idle 正是为了区分这种情况。要注意,D 状态的进程会导致平均负载升高, I 状态的进程却不会。
  • T 也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要你用 fg 命令,恢复到前台运行)。而当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行。
  • X 是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。

其中僵尸进程,这是多进程应用很容易碰到的问题。正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程。通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由 init 进程回收后也会消亡。一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免。

当使用 ps 命令查看进程状态是,会遇到一些奇怪的输出:

$ ps aux | grep /app
root      4009  0.0  0.0   4376  1008 pts/0    Ss+  05:51   0:00 /app
root      4287  0.6  0.4  37280 33660 pts/0    D+   05:54   0:00 /app
root      4288  0.6  0.4  37280 33668 pts/0    D+   05:54   0:00 /app

其中 SD 我们知道是什么意思,又来个 s+,其实可以通过 man ps 查询。其实 s 表示这个进程是一个会话的领导进程,而 + 表示前台进程组。进程组 表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员,而 会话 是指共享同一个控制终端的一个或多个进程组。

比如,我们通过 SSH 登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话。而我们在终端中运行的命令以及它们的子进程,就构成了一个个的进程组,其中,在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。

iowait 较高问题分析

有时候我们使用 top 工具查看系统性能时,会发现 iowait 特别高,例如:

$ top
top - 05:56:23 up 17 days, 16:45,  2 users,  load average: 2.00, 1.68, 1.39
Tasks: 247 total,   1 running,  79 sleeping,   0 stopped, 115 zombie
%Cpu0  :  0.0 us,  0.7 sy,  0.0 ni, 38.9 id, 60.5 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  0.7 sy,  0.0 ni,  4.7 id, 94.6 wa,  0.0 hi,  0.0 si,  0.0 st
...

例如,这里两个 CPU 的使用率情况,用户 CPU 和系统 CPU 都不高,但 iowait 分别是 60.5% 和 94.6%,运行 dstat 1 10 观察 CPU 和 I/O 的使用情况:

dstat

从 dstat 的输出,我们可以看到,每当 iowait 升高(wai)时,磁盘的读请求(read)都会很大。这说明 iowait 的升高跟磁盘的读请求有关,很可能就是磁盘读导致的。我们继续使用 top 命令查看,可能会发现一些 D 状态进程,例如:

$ top
...
1083 root      20   0   70052  65716    200 D   2.6  1.6   0:00.08 app
1084 root      20   0   70052  65716    200 D   2.6  1.6   0:00.08 app
1082 root      20   0   37016   3468   2760 R   0.7  0.1   0:00.06 top
   1 root      20   0    4512   1584   1500 S   0.0  0.0   0:00.11 app
...

我们再使用 pidstat 命令查看进程的 I/O 统计数据:

# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据
root@748d6a9c77d1:/# pidstat -d -p 1083 1 3
Linux 4.19.76-linuxkit (748d6a9c77d1)     05/31/20     _x86_64_    (4 CPU)

15:30:48      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
15:30:49        0      1083      0.00      0.00      0.00       0  app
15:30:50        0      1083      0.00      0.00      0.00       0  app
15:30:51        0      1083      0.00      0.00      0.00       0  app
Average:        0      1083      0.00      0.00      0.00       0  app

在这个输出中, kB_rd 表示每秒读的 KB 数, kB_wr 表示每秒写的 KB 数,iodelay 表示 I/O 的延迟(单位是时钟周期)。它们都是 0,那就表示此时没有任何的读写,说明问题不是 1083 进程导致的。那要怎么知道,到底是哪个进程在进行磁盘读写呢?我们继续使用 pidstat,但这次去掉进程号,干脆就来观察所有进程的 I/O 使用情况。

root@748d6a9c77d1:/# pidstat -d 1 20
Linux 4.19.76-linuxkit (748d6a9c77d1)     05/31/20     _x86_64_    (4 CPU)

15:34:18      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command

15:34:19      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
15:34:20        0      1221 364543.50      0.00      0.00       3  app
15:34:20        0      1222 370687.50      0.00      0.00       4  app

15:34:20      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
15:34:21        0      1221 946176.50      0.00      0.00      49  app
15:34:21        0      1222 940032.50      0.00      0.00      50  app

15:34:21      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command

15:34:22      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command

15:34:23      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command

发现确实 app 进程导致,那我们怎么知道 app 进程在执行什么 I/O 操作呢?我们知道,进程想要访问磁盘,就必须使用系统调用,所以接下来,重点就是找出 app 进程的系统调用了。

strace 正是最常用的跟踪进程系统调用的工具。我们从 pidstat 的输出中拿到进程的 PID 号,然后在终端中运行 strace 命令,并用 -p 参数指定 PID 号:

root@748d6a9c77d1:/# strace -p 1221
strace: attach: ptrace(PTRACE_SEIZE, 1221): Operation not permitted

失望,没有得到想要的结果,但这是为什么呢?我们再使用 ps 命令查看发现它已经变僵尸了,僵尸进程实际上已经推出了,所以无法追踪:

root@748d6a9c77d1:/# ps -aux | grep 1221
root      1221  0.1  0.0      0     0 pts/0    Z+   15:34   0:00 [app] <defunct>

我们前面使用过 perf recordperf report 命令来输出系统函数调用报告,我们在这里依然可以用它:

perf-top-iowait

swapper 是内核中的调度进程,你可以先忽略掉。我们来看其他信息,你可以发现, app 的确在通过系统调用 sys_read() 读取数据。并且从 new_sync_readblkdev_direct_IO 能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。看来,罪魁祸首是 app 内部进行了磁盘的直接 I/O ,接着我们就可以找我们的应用程序哪里直接读文件了。

僵尸进程分析

我们在 top 的输出结果中也看到大量的 Z 进程,我们知道 Z 进程代表该进程已经退出,只是父进程没有回收其资源导致。因为大量的 Z 进程会耗尽系统 pid 号,所以我们还是要解决它,解决他们的本质是还要父进程里面消灭他们。

我们随便找一个僵尸进程,查找他的父进程:

root@748d6a9c77d1:/# pstree -aps 43
app,1
    `-(app,43)

然后我们去我们的程序查看,进程在创建子进程后,有没有等待他结束。我们可以使用如下命令杀死父进程进而杀死僵尸进程:

ps -A -o stat,ppid,pid,cmd | grep -e ‘^[Zz]’ | awk ‘{print $2}’ | xargs kill -9

软中断

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

  • 上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。上半部直接处理硬件请求,也就是硬件中断,特点是快速执行

  • 下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。下半部分则由内核触发,特点是快速执行。

举个例子,网卡接收到数据包后,会通过硬件中断的方式,通知内核有新的数据到了。这时,内核就应该调用中断处理程序来响应它。然后更新一下硬件寄存器的状态(表示数据已经读好了),最后再发送一个软中断信号,通知下半部做进一步的处理。

实际上,上半部会打断 CPU 正在执行的任务,然后立即执行中断处理程序。而下半部以内核线程的方式执行,并且每个 CPU 都对应一个软中断内核线程,名字为 ksoftirqd/CPU 编号,比如说, 0CPU 对应的软中断内核线程的名字就是 ksoftirqd/0

root@iZ94lcu45k0Z:~# ps -aux | grep ksoftirqd
root         7  0.0  0.0      0     0 ?        S    Jan29   0:32 [ksoftirqd/0]
root     30590  0.0  0.1  14428  1012 pts/0    S+   22:46   0:00 grep --color=auto ksoftirqd

proc 文件系统。它是一种内核空间和用户空间进行通信的机制,可以用来查看内核的数据结构,或者用来动态修改内核的配置。其中:

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

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

运行下面的命令,查看 /proc/softirqs 文件的内容,你就可以看到各种类型软中断在不同 CPU 上的累积运行次数:

root@913ecec6c8c3:/# cat /proc/softirqs
                CPU0       CPU1       CPU2       CPU3
      HI:          0          0          0          0
   TIMER:      56364     101156      79623     107630
  NET_TX:          4         11          8          7
  NET_RX:         19        673          0          1
   BLOCK:       2759       3107       2028       4077
IRQ_POLL:          0          0          0          0
 TASKLET:          1          0          0          0
   SCHED:      41586      70250      44559      69909
 HRTIMER:          0          0          0          0
     RCU:      30472      95774      38898      49439

从输出结果中可以看出,软中断包括了10个类别,分别对应不同的工作类型,比如 NET_RX 表示网络接收中断,而 NET_TX 表示网络发送中断。要注意同一种软中断在不同 CPU 上的分布情况,也就是同一行的内容。正常情况下,同一种中断在不同 CPU 上的累积次数应该差不多。比如这个界面中,NET_RX 在 CPU0 和 CPU1 上的中断次数基本是同一个数量级,相差不大。软中断不只包括了刚刚所讲的硬件设备中断处理程序的下半部,一些内核自定义的事件也属于软中断,比如内核调度和 RCU 锁(Read-Copy Update 的缩写,RCU 是 Linux 内核中最常用的锁之一)等。

洪水攻击

网上经常看到洪水攻击 SYN FLOOD 的讨论,今天就摸你洪水攻击,致使 CPU 升高,不干正事的例子。准备一个nginx的简易网站,例如:

$ curl http://192.168.0.30/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

接着我们在运行 hping3 命令,来模拟 nginx 的客户端请求:

# -S参数表示设置TCP协议的SYN(同步序列号),-p表示目的端口为80
# -i u100表示每隔100微秒发送一个网络帧
# 注:如果你在实践过程中现象不明显,可以尝试把100调小,比如调成10甚至1
$ hping3 -S -p 80 -i u100 192.168.0.30

我们再进入 nginx 所在服务器,运行 top 命令查看系统资源使用情况:

# top运行后按数字1切换到显示所有CPU
$ top
top - 10:50:58 up 1 days, 22:10,  1 user,  load average: 0.00, 0.00, 0.00
Tasks: 122 total,   1 running,  71 sleeping,   0 stopped,   0 zombie
%Cpu0  :  0.0 us,  0.0 sy,  0.0 ni, 96.7 id,  0.0 wa,  0.0 hi,  3.3 si,  0.0 st
%Cpu1  :  0.0 us,  0.0 sy,  0.0 ni, 95.6 id,  0.0 wa,  0.0 hi,  4.4 si,  0.0 st
...

PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    7 root      20   0       0      0      0 S   0.3  0.0   0:01.64 ksoftirqd/0
   16 root      20   0       0      0      0 S   0.3  0.0   0:01.97 ksoftirqd/1
 2663 root      20   0  923480  28292  13996 S   0.3  0.3   4:58.66 docker-containe
 3699 root      20   0       0      0      0 I   0.3  0.0   0:00.13 kworker/u4:0
 3708 root      20   0   44572   4176   3512 R   0.3  0.1   0:00.07 top
    1 root      20   0  225384   9136   6724 S   0.0  0.1   0:23.25 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.03 kthreadd
...

平均负载是0,就绪队列里面就只有一个进程,平均 CPU 使用率最高的也只有 4.4%,进程列表,CPU 使用率最高的进程也只有 0.3%,毫无异常。但是 ksoftirqd/0 软中断进程使用率最高,而且排在最前面,毫无方法的情况下,就拿排头兵开刀,我们观察一下软中断的变化情况:

$ watch -d cat /proc/softirqs
                CPU0       CPU1
      HI:          0          0
   TIMER:    1083906    2368646
  NET_TX:         53          9
  NET_RX:    1550643    1916776
   BLOCK:          0          0
IRQ_POLL:          0          0
 TASKLET:     333637       3930
   SCHED:     963675    2293171
 HRTIMER:          0          0
     RCU:    1542111    1590625

通过 /proc/softirqs 文件内容的变化情况,你可以发现, TIMER(定时中断)、NET_RX(网络接收)、SCHED(内核调度)、RCU(RCU 锁)等这几个软中断都在不停变化。

其中,NET_RX,也就是网络数据包接收软中断的变化速率最快。而其他几种类型的软中断,是保证 Linux 调度、时钟和临界区保护这些正常工作所必需的,所以它们有一定的变化倒是正常的。

我们通过 sar 工具查看网络系统的收发情况,它不仅可以观察网络收发的吞吐量(BPS,每秒收发的字节数),还可以观察网络收发的 PPS,即每秒收发的网络帧数。

# -n DEV 表示显示网络收发的报告,间隔1秒输出一组数据
$ sar -n DEV 1
15:03:46        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
15:03:47         eth0  12607.00   6304.00    664.86    358.11      0.00      0.00      0.00      0.01
15:03:47      docker0   6302.00  12604.00    270.79    664.66      0.00      0.00      0.00      0.00
15:03:47           lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
15:03:47  veth9f6bbcd   6302.00  12604.00    356.95    664.66      0.00      0.00      0.00      0.05

sar 的输出界面,简单介绍一下:

  • 第二列:IFACE 表示网卡。
  • 第三、四列:rxpck/s 和 txpck/s 分别表示每秒接收、发送的网络帧数,也就是 PPS。
  • 第五、六列:rxkB/s 和 txkB/s 分别表示每秒接收、发送的千字节数,也就是 BPS。

对网卡 eth0 来说,每秒接收的网络帧数比较大,达到了 12607,而发送的网络帧数则比较小,只有 6304;每秒接收的千字节数只有 664 KB,而发送的千字节数更小,只有 358 KB。就是说接收的 PPS 比较大,而接收的 BPS 却很小,只有 664 KB。直观来看网络帧应该都是比较小的,我们稍微计算一下,664*1024/12607 = 54 字节,说明平均每个网络帧只有 54 字节,这显然是很小的网络帧,也就是我们通常所说的小包问题。

那现在就是要知道这些小包是从哪里发来的,我们使用 tcpdump 抓包工具指定网卡 eth0,并且指定 TCP 协议和 80 端口精准抓包。

# -i eth0 只抓取eth0网卡,-n不解析协议名和主机名
# tcp port 80表示只抓取tcp协议并且端口号为80的网络帧
$ tcpdump -i eth0 -n tcp port 80
15:11:32.678966 IP 192.168.0.2.18238 > 192.168.0.30.80: Flags [S], seq 458303614, win 512, length 0
...

从 tcpdump 的输出中,可以发现,从 192.168.0.218238 端口发送到 192.168.0.3080 端口有大量的 SYN(Flags[S]) 包,再加上前面用 sar 发现的, PPS 超过 12000 的现象,现在我们可以确认,这就是从 192.168.0.2 这个地址发送过来的 SYN FLOOD 攻击。

SYN FLOOD 问题最简单的解决方法,就是从交换机或者硬件防火墙中封掉来源 IP,这样 SYN FLOOD 网络帧就不会发送到服务器中。

CPU 性能问题定位

原文:https://time.geekbang.org/column/article/72685

分析 CPU 的性能瓶颈,我们得有一些指标来衡量,量化分析,首先我们想到的是 CPU 使用率,这也是实际环境中最常见的一个性能指。CPU 使用率描述了非空闲时间占总 CPU 时间的百分比,根据 CPU 上运行任务的不同,又被分为用户 CPU、系统 CPU、等待 I/O CPU、软中断和硬中断等。

  • 用户 CPU 使用率,包括用户态 CPU 使用率(user)和低优先级用户态 CPU 使用率(nice),表示 CPU 在用户态运行的时间百分比。用户 CPU 使用率高,通常说明有应用程序比较繁忙。

  • 系统 CPU 使用率,表示 CPU 在内核态运行的时间百分比(不包括中断)。系统 CPU 使用率高,说明内核比较繁忙。

  • 等待 I/O 的 CPU 使用率,通常也称为 iowait,表示等待 I/O 的时间百分比。iowait 高,通常说明系统与硬件设备的 I/O 交互时间比较长。

  • 软中断和硬中断的 CPU 使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。

  • 除了上面这些,还有在虚拟化环境中会用到的窃取 CPU 使用率(steal)和客户 CPU 使用率(guest),分别表示被其他虚拟机占用的 CPU 时间百分比,和运行客户虚拟机的 CPU 时间百分比。

其次是 平均负载(Load Average),也就是系统的平均活跃进程数。它反应了系统的整体负载情况,主要包括三个数值,分别指过去 1 分钟、过去 5 分钟和过去 15 分钟的平均负载。理想情况下,平均负载等于逻辑 CPU 个数,这表示每个 CPU 都恰好被充分利用。如果平均负载大于逻辑 CPU 个数,就表示负载比较重了。

最后就是平时不容易观测到的 上下文切换,包括:

  • 无法获取资源而导致的自愿上下文切换;

  • 被系统强制调度导致的非自愿上下文切换。

上下文切换,本身是保证 Linux 正常运行的一项核心功能。但过多的上下文切换,会将原本运行进程的 CPU 时间,消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,缩短进程真正运行的时间,成为性能瓶颈。

除了上面几种,还有一个指标,CPU 缓存的命中率。由于 CPU 发展的速度远快于内存的发展,CPU 的处理速度就比内存的访问速度快得多。这样,CPU 在访问内存的时候,免不了要等待内存的响应。为了协调这两者巨大的性能差距,CPU 缓存(通常是多级缓存)就出现了。

cpu-cache

就像上面这张图显示的,CPU 缓存的速度介于 CPU 和内存之间,缓存的是热点的内存数据。根据不断增长的热点数据,这些缓存按照大小不同分为 L1、L2、L3 等三级缓存,其中 L1 和 L2 常用在单核中, L3 则用在多核中。从 L1 到 L3,三级缓存的大小依次增大,相应的,性能依次降低(当然比内存还是好得多)。而它们的命中率,衡量的是 CPU 缓存的复用情况,命中率越高,则表示性能越好。

这些指标都很有用,需要我们熟练掌握,总结成了一张图,帮助分类和记忆,可以当成 CPU 性能分析的“指标筛选”清单。

cpu-perf-index

性能分析工具

有了指标,我们得用相应的工具分析得住这些指标。

  • 首先,平均负载。我们可以用 uptime 查看系统的平均负载;在的值平均负载升高后,又可以用 mpstatpidstat ,分别观察了每个 CPU 和每个进程 CPU 的使用情况,进而找出了导致平均负载升高的进程。

  • 其次,上下文切换,用 vmstat 查看系统的上下文切换次数和中断次数;然后通过 pidstat ,观察了进程的自愿上下文切换和非自愿上下文切换情况;最后通过 pidstat ,观察线程的上下文切换情况,找出了上下文切换次数增多的根源,

  • 进程 CPU 使用率升高,先用 top 查看了系统和进程的 CPU 使用情况,发现 CPU 使用率升高的进程。再用 perf top,观察进程的调用链,最终找出 CPU 升高的根源。

  • 系统的 CPU 使用率升高,先用 top 观察到了系统 CPU 升高,但通过 toppidstat ,却找不出高 CPU 使用率的进程。最终通过 perf recordperf report 查找祸根,可能是一些短时进程。对于短时进程,我还介绍了一个专门的工具 execsnoop,它可以实时监控进程调用的外部命令。

  • iowait 升高,用 top 观察到了 iowait 升高,并发现了大量的不可中断进程和僵尸进程,接着我们用 dstat 发现是这是由磁盘读导致的,于是又通过 pidstat 找出了相关的进程。但我们用 strace 查看进程系统调用却失败了,最终还是用 perf 分析进程调用链, 。

  • 软中断,通过 top 观察到,系统的软中断 CPU 使用率升高,接着查看 /proc/softirqs, 找到了几种变化速率较快的软中断;然后通过 sar 命令,发现是网络小包的问题,最后再用 tcpdump ,找出网络帧的类型和来源,确定是一个 SYN FLOOD 攻击导致的。

日常工作中,备下所有的性能指标查看工具是不可能地,所以必要时请看下图,根据指标找工具:

find-tool-by-index

但是需要记住每个工具大概是干什么的,需要的时候才可以快速下手:

find-index-by-tool

快速搞定 CPU 使用率升高的问题

虽然 CPU 的性能指标比较多,但要知道,既然都是描述系统的 CPU 性能,它们就不会是完全孤立的,很多指标间都有一定的关联。想弄清楚性能指标的关联性,就要通晓每种性能指标的工作原理。

举个例子,用户 CPU 使用率高,我们应该去排查进程的用户态而不是内核态。因为用户 CPU 使用率反映的就是用户态的 CPU 使用情况,而内核态的 CPU 使用情况只会反映到系统 CPU 使用率上。

有了一些基本认识,就可以帮助缩小排查范围,为了缩小排查范围,我通常会先运行几个支持指标较多的工具,如 topvmstatpidstat 。如下图:

check-range.png

通过这张图可以发现,这三个命令,几乎包含了所有重要的 CPU 性能指标,比如:从 top 的输出可以得到各种 CPU 使用率以及僵尸进程和平均负载等信息。从 vmstat 的输出可以得到上下文切换次数、中断次数、运行状态和不可中断状态的进程数。从 pidstat 的输出可以得到进程的用户 CPU 使用率、系统 CPU 使用率、以及自愿上下文切换和非自愿上下文切换情况。

CPU 性能优化思路

在发现性能问题的瓶颈后,我们先要搞明白思路才能开始优化:

  • 首先,得有指标衡量性能优化的效果,本次性能优化提升了多少;
  • 其次,当多个因素共同造成性能问题时,优化顺序如何选择;
  • 最后,当存在提升性能的多种方法时,该如何选择;

对于衡量指标,我们可以从应用程序角度和系统资源两个维度,以 web 应用为例:

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

对于多个性能问题共存的情况,应该先认真思考,哪个影响性能最严重,就从哪里入手,要找到根因。对于多种存在的优化方法时,选择成本最低的。

应用程序优化

首先,从应用程序的角度来说,降低 CPU 使用率的最好方法当然是,排除所有不必要的工作,只保留最核心的逻辑。比如减少循环的层次、减少递归、减少动态内存分配等等。除此之外,还有其他的性能优化方法:

  • 编译器优化:很多编译器都会提供优化选项,适当开启它们,在编译阶段你就可以获得编译器的帮助,来提升性能。比如, gcc 就提供了优化选项 -O2,开启后会自动对应用程序的代码进行优化。

  • 算法优化:使用复杂度更低的算法,可以显著加快处理速度。比如,在数据比较大的情况下,可以用 O(nlogn) 的排序算法(如快排、归并排序等),代替 O(n^2) 的排序算法(如冒泡、插入排序等)。

  • 异步处理:使用异步处理,可以避免程序因为等待某个资源而一直阻塞,从而提升程序的并发处理能力。比如,把轮询替换为事件通知,就可以避免轮询耗费 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 上。

内存性能

我们通常所说的内存容量,其实指的是物理内存。物理内存也称为 主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问 虚拟内存

虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长(也就是单个 CPU 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。常见的 32 位操作系统和 64 为操作系统虚拟地址空间表示如下:

虚拟内存

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

并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张 页表,记录虚拟地址与物理地址的映射关系,如下图所示:

内存映射

页表实际上存储在 CPU 的内存管理单元 MMU 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是 4 KB 大小。这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间。页的大小只有 4 KB ,导致的另一个问题就是,整个页表会变得非常大。了解决页表项过多的问题,Linux 提供了两种机制,也就是 多级页表大页(HugePage)

多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。Linux 用的正是四级页表来管理内存页,如下图所示,虚拟地址被分为 5 个部分,前 4 个表项用于选择页,而最后一个索引表示页内偏移。

多级页表

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

虚拟内存空间分布

虚拟内存空间,如下图所示,从下到上被分为只读段,数据段,堆,文件映射,栈以及内核空间。

virtual-mem-distribute

  • 只读段,包括代码和常量等。

  • 数据段,包括全局变量等。

  • 堆,包括动态分配的内存,从低地址开始向上增长,如 C 的 malloc 函数。

  • 文件映射段,包括动态库、共享内存等,从高地址开始向下增长,如使用 mmap() 函数映射。

  • 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。

内存分配与回收

malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk()mmap()

对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。

而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc 只对大块内存使用 mmap 的原因。

对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用 free() 或 unmap() ,来释放这些不用的内存。

系统不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:

  • 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;

  • 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;

  • 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。

其中,第二种方式回收不常访问的内存时,会用到交换分区(以下简称 Swap)。Swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。

所以,你可以发现,Swap 把系统的可用内存变大了。不过要注意,通常只在内存不足时,才会发生 Swap 交换。并且由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性能问题。

OOM(Out of Memory),其实是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:

  • 一个进程消耗的内存越大,oom_score 就越大;

  • 一个进程运行占用的 CPU 越多,oom_score 就越小。

进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。为了实际工作的需要,管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。

oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。

比如用下面的命令,你就可以把 sshd 进程的 oom_adj 调小为 -16,这样, sshd 进程就不容易被 OOM 杀死。

echo -16 > /proc/$(pidof sshd)/oom_adj

内存使用情况

free 是一个比较常见的查看内存使用情况的工具:

root@iZ94lcu45k0Z:~# free  -h
               total        used        free      shared  buff/cache   available
Mem:           985M        105M        107M         11M        771M        688M
Swap:          947M          0B        947M

free 输出的是一个表格,表格总共有两行六列,这两行分别是物理内存 Mem 和交换分区 Swap 的使用情况,而六列中,每列数据的含义分别为:

  • 第一列,total 是总内存大小;
  • 第二列,used 是已使用内存的大小,包含了共享内存;
  • 第三列,free 是未使用内存的大小;
  • 第四列,shared 是共享内存的大小;
  • 第五列,buff/cache 是缓存和缓冲区的大小;
  • 最后一列,available 是新进程可用内存的大小。available 不仅包含未使用内存,还包括了可回收的缓存,所以一般会比未使用内存更大。不过,并不是所有缓存都可以回收,因为有些缓存可能正在使用中。

free 显示的是整个系统的内存使用情况。如果你想查看进程的内存使用情况,可以用 top 或者 ps 等工具。比如,下面是 top 的输出示例:

按下M切换到内存排序
$ top
...
KiB Mem :  8169348 total,  6871440 free,   267096 used,  1030812 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  7607492 avail Mem
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
  430 root      19  -1  122360  35588  23748 S   0.0  0.4   0:32.17 systemd-journal
 1075 root      20   0  771860  22744  11368 S   0.0  0.3   0:38.89 snapd
 1048 root      20   0  170904  17292   9488 S   0.0  0.2   0:00.24 networkd-dispat
    1 root      20   0   78020   9156   6644 S   0.0  0.1   0:22.92 systemd
12376 azure     20   0   76632   7456   6420 S   0.0  0.1   0:00.01 systemd
12374 root      20   0  107984   7312   6304 S   0.0  0.1   0:00.00 sshd
...

top 输出界面的顶端,也显示了系统整体的内存使用情况,这些数据跟 free 类似,下面的内容中,跟内存相关的几列数据:

  • VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内。

  • RES 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 Swap 和共享内存。

  • SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等。

  • %MEM 是进程使用物理内存占系统总内存的百分比

除了要认识这些基本信息,在查看 top 输出时,要注意两点。

  • 第一,虚拟内存通常并不会全部分配物理内存。从上面的输出,你可以发现每个进程的虚拟内存都比常驻内存大得多。
  • 第二,共享内存 SHR 并不一定是共享的,比方说,程序的代码段、非共享的动态链接库,也都算在 SHR 里。当然,SHR 也包括了进程间真正共享的内存。所以在计算多个进程的内存使用时,不要把所有进程的 SHR 直接相加得出结果。

Buffer 和 Cache

这里我们着重学习下 BufferCache,我们可以用 free 获得这个指标,这个指标的详细意思可以通过 man free 获取到:

DESCRIPTION
   free  displays  the  total amount of free and used physical and swap memory in the system, as well as the buffers and caches used by
   the kernel. The information is gathered by parsing /proc/meminfo. The displayed columns are:

   total  Total installed memory (MemTotal and SwapTotal in /proc/meminfo)

   used   Used memory (calculated as total - free - buffers - cache)

   free   Unused memory (MemFree and SwapFree in /proc/meminfo)

   shared Memory used (mostly) by tmpfs (Shmem in /proc/meminfo)

   buffers
          Memory used by kernel buffers (Buffers in /proc/meminfo)

   cache  Memory used by the page cache and slabs (Cached and SReclaimable in /proc/meminfo)

   buff/cache
          Sum of buffers and cache

    ....

从上面看出 Buffer 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。Cache 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 CachedSReclaimable 之和。

proc 文件系统同时是很多性能工具的最终数据来源,既然 Buffers、Cached、SReclaimable 这几个指标不容易理解,我们继续通过 man proc 获取文档。

Buffers %lu
    Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).

Cached %lu
In-memory cache for files read from the disk (the page cache).  Doesn't include SwapCached.
...
SReclaimable %lu (since Linux 2.6.19)
    Part of Slab, that might be reclaimed, such as caches.

SUnreclaim %lu (since Linux 2.6.19)
    Part of Slab, that cannot be reclaimed on memory pressure.

通过这个文档,我们可以看到:

  • Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。

  • Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。
    ·

  • SReclaimableSlab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。

通过 vmstat 命令可以查看 BufferCache 的使用情况,如下图:

vmstat-buff-cache.png

  • buff 和 cache 就是我们前面看到的 Buffers 和 Cache,单位是 KB。

  • bi 和 bo 则分别表示块设备读取和写入的大小,单位为块 / 秒。因为 Linux 中块的大小是 1KB,所以这个单位也就等价于 KB/s。

详情查看文章:基础篇:怎么理解内存中的Buffer和Cache?

经过该文章中的实验,总结得出:

  • Buffer 既可以用作“将要写入磁盘数据的缓存”,也可以用作“从磁盘读取数据的缓存”。

  • Cache 既可以用作“从文件读取数据的页缓存”,也可以用作“写文件的页缓存”。

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

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

缓存命中率

所谓 缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。

实际上,缓存是现在所有高并发系统必需的核心模块,主要作用就是把经常访问的数据(也就是热点数据),提前读入到内存中。这样,下次访问时就可以直接从内存读取数据,而不需要经过硬盘,从而加快应用程序的响应速度。

cachestatcachetop ,它们正是查看系统缓存命中情况的工具。cachestat 提供了整个操作系统缓存的读写命中情况。cachetop 提供了每个进程的缓存命中情况。

例如,运行 cachestat 以 1 秒的事件建个,输出3组统计数据:

$ cachestat 1 3

TOTAL MISSES HITS DIRTIES BUFFERS_MB CACHED_MB
2 0 2 1 17 279
2 0 2 1 17 279
2 0 2 1 17 279

这些指标从左到右依次表示:

  • TOTAL ,表示总的 I/O 次数;
  • MISSES ,表示缓存未命中的次数;
  • HITS ,表示缓存命中的次数;
  • DIRTIES, 表示新增到缓存中的脏页数;
  • BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位;
  • CACHED_MB 表示 Cache 的大小,以 MB 为单位。

再来看一个 cachetop 的运行界面:

$ cachetop
11:58:50 Buffers MB: 258 / Cached MB: 347 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
13029 root     python                  1        0        0     100.0%       0.0%

输出跟 top 类似,默认按照缓存的命中次数(HITS)排序,展示了每个进程的缓存命中情况。具体到每一个指标,这里的 HITS、MISSES 和 DIRTIES ,跟 cachestat 里的含义一样,分别代表间隔时间内的缓存命中次数、未命中次数以及新增到缓存中的脏页数。

应用中,我们可以通过利用系统的缓存加快读写磁盘或文件的速度。详情请查看 如何利用系统缓存优化程序的运行效率?

查看文件缓存大小

除了缓存的命中率外,可以使用 pcstat 查看指定文件的缓存大小,安装好之后,如下查看 /bin/ls 的缓存情况:

$ pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name    | Size (bytes)   | Pages      | Cached    | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792         | 33         | 0         | 000.000 |
+---------+----------------+------------+-----------+---------+

这个输出中,Cached 就是 /bin/ls 在缓存中的大小,而 Percent 则是缓存的百分比。你看到它们都是 0,这说明 /bin/ls 并不在缓存中。如果你执行一下 ls 命令,再运行相同的命令来查看的话,就会发现 /bin/ls 都在缓存中了:

$ ls
$ pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name    | Size (bytes)   | Pages      | Cached    | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792         | 33         | 33        | 100.000 |
+---------+----------------+------------+-----------+---------+

内存泄漏

进程的内存空间中,用户空间内存包括多个不同的内存段,比如只读段、数据段、堆、栈以及文件映射段等,这些内存段正是应用程序使用内存的基本方式。其中:

  • 栈内存由系统自动分配和管理。局部变量分配在栈空间上,一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题。

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

  • 只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。

  • 数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。

  • 内存映射段,包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。

可以使用 bcc 软件包中的工具 memleak 来检测内存泄漏,例如:

$ docker cp app:/app /app
$ /usr/share/bcc/tools/memleak -p $(pidof app) -a
Attaching to pid 12512, Ctrl+C to quit.
[03:00:41] Top 10 stacks with outstanding allocations:
    addr = 7f8f70863220 size = 8192
    addr = 7f8f70861210 size = 8192
    addr = 7f8f7085b1e0 size = 8192
    addr = 7f8f7085f200 size = 8192
    addr = 7f8f7085d1f0 size = 8192
    40960 bytes in 5 allocations from stack
        fibonacci+0x1f [app]
        child+0x4f [app]
        start_thread+0xdb [libpthread-2.27.so] 

详细可以查看原文:案例篇:内存泄漏了,我该如何定位和处理?

SWAP

参考文章:

内存资源紧张时会引发内存回收或者OOM。其中 OOM 就是系统杀死占用大量内存的进程,释放这些内存,再分配给其他更需要的进程。内存回收,也就是系统释放掉可以回收的内存,比如我前面讲过的缓存和缓冲区,就属于可回收内存。它们在内存管理中,通常被叫做文件页(File-backed Page)。大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。

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

  • 可以在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中;

  • 也可以交给系统,由内核线程 pdflush 负责这些脏页的刷新。

除了缓存和缓冲区,通过内存映射获取的文件映射页,也是一种常见的文件页。它也可以被释放掉,下次再访问的时候,从文件重新读取。

应用程序分配的堆内存,也就是我们内存管理中常说的匿名页,如果很少被访问到,但是又不能直接释放,可是现在内存又很紧张,怎么办呢?SWAP 应用而生,先将内存换出到磁盘中,用到的时候再将他们从文件换回内存。

Linux 操作系统在系统内存不足的情况下,会进行直接内存回收,例如分配大块内存时现有内存不足。除此之外,还有一个专门的内核线程来定期回收内存,kswapd0

root@iZ94lcu45k0Z:~# ps -ef | grep kswapd0
root        34     2  0 Jan29 ?        00:00:01 [kswapd0]

当系统的可用内存小于 /proc/sys/vm/min_free_kbytesmin_free_kbytes 时,就会触发内存回收。

从上面可以看出,内存回收有两种方式,回收的内存既包括了文件页,又包括了匿名页。

  • 对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。

  • 而对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存。

实际回收内存时,根据 /proc/sys/vm/swappiness 定义的值,来表明倾向于使用 swap 进行匿名页回收的程度,数值范围是:0-100,越大越倾向于 swap。但是,即使设置成 0,当剩余内存+文件页小于页高阈值时,还是会触发 swap。

通过 free 命令可以确认是否打开了 swap:

$ free
            total        used        free      shared  buff/cache   available
Mem:        8169348      331668     6715972         696     1121708     7522896
Swap:             0           0           0

从这个 free 输出你可以看到,Swap 的大小是 0,这说明我的机器没有配置 Swap。

要开启 Swap,我们首先要清楚,Linux 本身支持两种类型的 Swap,即 Swap 分区和 Swap 文件。以 Swap 文件为例,在第一个终端中运行下面的命令开启 Swap,我这里配置 Swap 文件的大小为 8GB:

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

可以通过 sar 命令观察内存指标的变化情况:

# 间隔1秒输出一组数据
# -r表示显示内存使用情况,-S表示显示Swap使用情况
$ sar -r -S 1
04:39:56    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
04:39:57      6249676   6839824   1919632     23.50    740512     67316   1691736     10.22    815156    841868         4

04:39:56    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:39:57      8388604         0      0.00         0      0.00

04:39:57    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
04:39:58      6184472   6807064   1984836     24.30    772768     67380   1691736     10.22    847932    874224        20

04:39:57    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:39:58      8388604         0      0.00         0      0.00

sar 的输出结果是两个表格,第一个表格表示内存的使用情况,第二个表格表示 Swap 的使用情况。其中,各个指标名称前面的 kb 前缀,表示这些指标的单位是 KB。大部分指标我们都已经见过了,剩下的几个新出现的指标:

  • kbcommit,表示当前系统负载需要的内存。它实际上是为了保证系统内存不溢出,对需要内存的估计值。%commit,就是这个值相对总内存的百分比。

  • kbactive,表示活跃内存,也就是最近使用过的内存,一般不会被系统回收

  • kbinact,表示非活跃内存,也就是不常访问的内存,有可能会被系统回收。

可以通过 cachetop 命令观察缓存的使用情况:

$ cachetop 5
12:28:28 Buffers MB: 6349 / Cached MB: 87 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
18280 root     python                 22        0        0     100.0%       0.0%
18279 root     dd                  41088    41022        0      50.0%      50.0%

可以通过以下的命令关闭 Swap:

$ swapoff -a

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

$ swapoff -a && swapon -a 

内存性能问题定位

本部分内容来自:套路篇:如何“快准狠”找到系统内存的问题?

内存性能指标

为了分析内存的性能瓶颈,首先要知道怎么衡量内存的性能,需要量化。首先,就是系统的内存使用情况:

  • 已用内存和剩余内存;
  • 共享内存的使用量,它是通过 tmpfs 实现的,所以它的大小也就是 tmpfs 使用的内存大小。tmpfs 其实也是一种特殊的缓存。
  • 可用内存是新进程可以使用的最大内存,它包括剩余内存和可回收缓存。
  • 缓存包括两部分,一部分是磁盘读取文件的页缓存,用来缓存从磁盘读取的数据,可以加快以后再次访问的速度。另一部分,则是 Slab 分配器中的可回收内存。
  • 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据。这样,内核就可以把分散的写集中起来,统一优化磁盘写入。

其次是进程内存使用情况,比如进程的虚拟内存、常驻内存、共享内存以及 Swap 内存等。

  • 虚拟内存,包括了进程代码段、数据段、共享内存、已经申请的堆内存和已经换出的内存等。这里要注意,已经申请的内存,即使还没有分配物理内存,也算作虚拟内存。
  • 常驻内存是进程实际使用的物理内存,不过,它不包括 Swap 和共享内存。常驻内存一般会换算成占系统总内存的百分比,也就是进程的内存使用率。
  • 共享内存,既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等。
  • Swap 内存,是指通过 Swap 换出到磁盘的内存。

从前面的学习中得到,系统调用内存分配请求后,并不会立刻为其分配物理内存,而是在请求首次访问时,通过缺页异常来分配。缺页异常又分为下面两种场景。缺页异常分为:

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

显然,主缺页异常升高,就意味着需要磁盘 I/O,那么内存访问也会慢很多。

最后就是 swap 使用情况,比如 Swap 的已用空间、剩余空间、换入速度和换出速度等。

  • 已用空间和剩余空间很好理解,就是字面上的意思,已经使用和没有使用的内存空间。
  • 换入和换出速度,则表示每秒钟换入和换出内存的大小。

内存性能指标

内存性能工具

有了指标,自然需要工具获取这些指标,通过下面的图我们可以看出哪些指标由哪些工具获得:

内存性能指标获取

或者说,通过常用的工具能获取到哪些性能指标:

内存性能指标获取

常用分析套路

为了迅速定位内存问题,通常可以先运行几个覆盖面比较大的性能工具,比如 free、top、vmstat、pidstat 等,具体可以分为:

  1. 先用 free 和 top,查看系统整体的内存使用情况。
  2. 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
  3. 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等。

分析过程如图:

内存性能问题分析过程

图中列出了最常用的几个内存工具,和相关的分析流程。其中,箭头表示分析的方向,举几个例子帮助理解。

第一个例子,当通过 free,发现大部分内存都被缓存占用后,可以使用 vmstat 或者 sar 观察一下缓存的变化趋势,确认缓存的使用是否还在继续增大。如果继续增大,则说明导致缓存升高的进程还在运行,那就能用缓存 / 缓冲区分析工具(比如 cachetop、slabtop 等),分析这些缓存到底被哪里占用。

第二个例子,当 free 一下,发现系统可用内存不足时,首先要确认内存是否被缓存 / 缓冲区占用。排除缓存 / 缓冲区后,可以继续用 pidstat 或者 top,定位占用内存最多的进程。找出进程后,再通过进程内存空间工具(比如 pmap),分析进程地址空间中内存的使用情况就可以了。

第三个例子,当通过 vmstat 或者 sar 发现内存在不断增长后,可以分析中是否存在内存泄漏的问题。比如你可以使用内存分配分析工具 memleak ,检查是否存在内存泄漏。如果存在内存泄漏问题,memleak 会为你输出内存泄漏的进程以及调用堆栈。

注意,上图中没有列出所有性能工具,只给出了最核心的几个。可以先把重心先放在核心工具上,通过案例和真实环境的实践,掌握使用方法和分析思路。

内存问题常用优化思路
  1. 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
  2. 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。
  3. 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。
  4. 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。
  5. 通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死。

I/O 性能篇

同 CPU、内存一样,磁盘和文件系统的管理,也是操作系统最核心的功能。

  • 磁盘为系统提供了最基本的持久化存储。
  • 文件系统则在磁盘的基础上,提供了一个用来管理文件的树状结构。

索引节点和目录项

文件系统,本身是对存储设备上的文件,进行组织管理的机制。组织方式不同,就会形成不同的文件系统。为了方便管理,Linux 文件系统为每个文件都分配两个数据结构,索引节点(index node)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。

  • 索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间。
  • 目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。

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

举个例子,通过硬链接为文件创建的别名,就会对应不同的目录项,不过这些目录项本质上还是链接同一个文件,所以,它们的索引节点相同。

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

可以查看下面对的图,来理解目录项,索引节点以及文件数据的关系:

dentry-inode-data.png

需要注意的是:

  • 目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据。在前面的 Buffer 和 Cache 原理中提到过,为了协调慢速磁盘与快速 CPU 的性能差异,文件内容会缓存到页缓存 Cache 中。

  • 磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区。其中:

    • 超级块,存储整个文件系统的状态。
    • 索引节点区,用来存储索引节点。
    • 数据块区,则用来存储文件数据。

虚拟文件系统

目录项、索引节点、逻辑块以及超级块,构成了 Linux 文件系统的四大基本要素。不过Linux 内核为了支持各种不同的文件系统,在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS(Virtual File System)。

VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互就可以了,而不需要再关心底层各种文件系统的实现细节。

可以查看下面的 Linux 文件系统架构图,帮助理解系统调用,VFS,缓存,文件系统以及块存储之间的关系。

file-sys-architecture.png

通过这张图,可以看到,在 VFS 的下方,Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。按照存储位置的不同,这些文件系统可以分为三类。

  • 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。

  • 第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。

  • 第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、iSCSI 等。

这些文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。拿第一类,也就是基于磁盘的文件系统为例,在安装系统时,要先挂载一个根目录(/),在根目录下再把其他文件系统(比如其他的磁盘分区、/proc 文件系统、/sys 文件系统、NFS 等)挂载进来。

文件系统 I/O

文件系统挂载到挂载点后,就能通过挂载点,再去访问它管理的文件了。VFS 提供了一组标准的文件访问接口。这些接口以系统调用的方式,提供给应用程序使用。

文件读写方式的各种差异,导致 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,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接 I/O。直接 I/O、非直接 I/O,本质上还是和文件系统交互。在数据库等场景中,可能会看到,跳过文件系统读写磁盘的情况,也就是通常所说的裸 I/O。

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

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

      比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。

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

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

      举个例子,在操作文件时,如果设置了 O_SYNC 或者 O_DSYNC 标志,就代表同步 I/O。如果设置了 O_DSYNC,就要等文件数据写入磁盘后,才能返回;而 O_SYNC,则是在 O_DSYNC 基础上,要求文件元数据也要写入磁盘后,才能返回。

      再比如,在访问管道或者网络套接字时,设置了 O_ASYNC 选项后,相应的 I/O 就是异步 I/O。这样,内核会再通过 SIGIO 或者 SIGPOLL,来通知进程文件是否可读写。

文件系统性能

容量

使用 df 命令来查看系统容量,如下:

$ df -h /dev/sda1 
Filesystem      Size  Used Avail Use% Mounted on 
/dev/sda1        29G  3.1G   26G  11% / 

有时候,明明碰到了空间不足的问题,可是用 df 查看磁盘空间后,却发现剩余空间还有很多。可能是因为索引节点区被耗尽,因为磁盘在格式化的时候,索引节点容量已经固定,如果有大量的小文件导致索引节点区容量不足,会导致这种情况,查看索引节点区容量方式如下:

$ df -i /dev/sda1 
Filesystem      Inodes  IUsed   IFree IUse% Mounted on 
/dev/sda1      3870720 157460 3713260    5% / 
缓存

可以用 free 或 vmstat,来观察页缓存的大小。复习一下,free 输出的 Cache,是页缓存和可回收 Slab 缓存的和,你可以从 /proc/meminfo ,直接得到它们的大小:

$ cat /proc/meminfo | grep -E "SReclaimable|Cached" 
Cached:           748316 kB 
SwapCached:            0 kB 
SReclaimable:     179508 kB 

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

比如,运行下面的命令,你就可以得到,所有目录项和各种文件系统索引节点的缓存情况:

root@iZ94lcu45k0Z:~# cat /proc/slabinfo | grep -E '^#|dentry|inode'
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
btrfs_inode            0      0   1144   14    4 : tunables    0    0    0 : slabdata      0      0      0
ufs_inode_cache        0      0    808   10    2 : tunables    0    0    0 : slabdata      0      0      0
qnx4_inode_cache       0      0    680   12    2 : tunables    0    0    0 : slabdata      0      0      0
hfs_inode_cache        0      0    832   19    4 : tunables    0    0    0 : slabdata      0      0      0
minix_inode_cache      0      0    672   12    2 : tunables    0    0    0 : slabdata      0      0      0
ntfs_big_inode_cache      0      0    960    8    2 : tunables    0    0    0 : slabdata      0      0      0
ntfs_inode_cache       0      0    296   13    1 : tunables    0    0    0 : slabdata      0      0      0
xfs_inode              0      0    960    8    2 : tunables    0    0    0 : slabdata      0      0      0
mqueue_inode_cache      8      8    960    8    2 : tunables    0    0    0 : slabdata      1      1      0
fuse_inode             0      0    832   19    4 : tunables    0    0    0 : slabdata      0      0      0
ecryptfs_inode_cache      0      0   1024    8    2 : tunables    0    0    0 : slabdata      0      0      0
fat_inode_cache        0      0    744   11    2 : tunables    0    0    0 : slabdata      0      0      0
squashfs_inode_cache      0      0    704   11    2 : tunables    0    0    0 : slabdata      0      0      0
ext4_inode_cache   41294  41325   1088   15    4 : tunables    0    0    0 : slabdata   2755   2755      0
hugetlbfs_inode_cache     13     13    624   13    2 : tunables    0    0    0 : slabdata      1      1      0
sock_inode_cache     184    198    704   11    2 : tunables    0    0    0 : slabdata     18     18      0
shmem_inode_cache   1507   1507    712   11    2 : tunables    0    0    0 : slabdata    137    137      0
proc_inode_cache    1224   1224    680   12    2 : tunables    0    0    0 : slabdata    102    102      0
inode_cache        28175  28275    608   13    2 : tunables    0    0    0 : slabdata   2175   2175      0
dentry             88284  88284    192   21    1 : tunables    0    0    0 : slabdata   4204   4204      0

具体含义可以通过 man slabinfo 得到,在实际性能分析中,更常使用 slabtop ,来找到占用内存最多的缓存类型。

# 按下c按照缓存大小排序,按下a按照活跃对象数排序 
$ slabtop 
Active / Total Objects (% used)    : 277970 / 358914 (77.4%) 
Active / Total Slabs (% used)      : 12414 / 12414 (100.0%) 
Active / Total Caches (% used)     : 83 / 135 (61.5%) 
Active / Total Size (% used)       : 57816.88K / 73307.70K (78.9%) 
Minimum / Average / Maximum Object : 0.01K / 0.20K / 22.88K 

OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME 
69804  23094   0%    0.19K   3324       21     13296K dentry 
16380  15854   0%    0.59K   1260       13     10080K inode_cache 
58260  55397   0%    0.13K   1942       30      7768K kernfs_node_cache 
485    413   0%    5.69K     97        5      3104K task_struct 
1472   1397   0%    2.00K     92       16      2944K kmalloc-2048 

上图中,目录项和索引节点占用了最多的 Slab 缓存。不过它们占用的内存其实并不大,加起来也只有 23MB 左右。

磁盘 I/O 工作原理

原文:
基础篇:Linux 磁盘I/O是怎么工作的(上)
基础篇:Linux 磁盘I/O是怎么工作的(下)

磁盘类型
根据存储介质分类

磁盘是可以持久化存储的设备,根据存储介质的不同,常见磁盘可以分为两类:机械磁盘和固态磁盘。

  • 机械磁盘,也称为硬盘驱动器 (Hard Disk Driver),通常缩写为 HDD。机械磁盘主要由盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。

    显然,如果 I/O 请求刚好连续,那就不需要磁道寻址,自然可以获得最佳性能。这其实就是我们熟悉的,连续 I/O 的工作原理。与之相对应的,当然就是随机 I/O,它需要不停地移动磁头,来定位数据位置,所以读写速度就会比较慢。

  • 固态磁盘 (Solid State Disk),通常缩写为 SSD,由固态电子元器件组成。固态磁盘不需要磁道寻址,所以,不管是连续 I/O,还是随机 I/O 的性能,都比机械磁盘要好得多。

其实,无论机械磁盘,还是固态磁盘,相同磁盘的随机 I/O 都要比连续 I/O 慢很多,因为:

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

此外,机械磁盘和固态磁盘还分别有一个最小的读写单位。机械磁盘的最小读写单位是扇区,一般大小为 512 字节。而固态磁盘的最小读写单位是页,通常大小是 4KB、8KB 等。

根据接口分类

按照接口来分类,比如可以把硬盘分为 IDE(Integrated Drive Electronics)、SCSI(Small Computer System Interface) 、SAS(Serial Attached SCSI) 、SATA(Serial ATA) 、FC(Fibre Channel) 等。

不同的接口,往往分配不同的设备名称。比如, IDE 设备会分配一个 hd 前缀的设备名,SCSI 和 SATA 设备会分配一个 sd 前缀的设备名。如果是多块同类型的磁盘,就会按照 a、b、c 等的字母顺序来编号。

按照使用方式

当把磁盘接入服务器后,按照不同的使用方式,又可以把它们划分为多种不同的架构。

最简单的,就是直接作为独立磁盘设备来使用。这些磁盘,往往还会根据需要,划分为不同的逻辑分区,每个分区再用数字编号。比如我们前面多次用到的 /dev/sda ,还可以分成两个分区 /dev/sda1 和 /dev/sda2。

另一个比较常用的架构,是把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列,也就是 RAID(Redundant Array of Independent Disks),从而可以提高数据访问的性能,并且增强数据存储的可靠性。

最后一种架构,是把这些磁盘组合成一个网络存储集群,再通过 NFS、SMB、iSCSI 等网络存储协议,暴露给服务器使用。

其实在 Linux 中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。

通用块层

同虚拟文件系统 VFS 类似,为了减小不同块设备的差异带来的影响,Linux 通过一个统一的通用块层,来管理各种不同的块设备。通用块层,其实是处在文件系统和磁盘驱动中间的一个块设备抽象层。它主要有两个功能 。

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

其中,对 I/O 请求排序的过程,也就是我们熟悉的 I/O 调度。事实上,Linux 内核支持四种 I/O 调度算法,分别是 NONE、NOOP、CFQ 以及 DeadLine。

I/O 栈

我们可以把 Linux 存储系统的 I/O 栈,由上到下分为三个层次,分别是文件系统层、通用块层和设备层。这三个 I/O 层的关系如下图所示,这其实也是 Linux 存储系统的 I/O 栈全景图。

linux storage stack digram

根据这张 I/O 栈的全景图,我们可以更清楚地理解,存储系统 I/O 的工作原理。

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

存储系统的 I/O ,通常是整个系统中最慢的一环。所以, Linux 通过多种缓存机制来优化 I/O 效率。比方说,为了优化文件访问的性能,会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制,以减少对下层块设备的直接调用。同样,为了优化块设备的访问效率,会使用缓冲区,来缓存块设备的数据。

磁盘性能指标

说到磁盘性能的衡量标准,必须要提到五个常见指标,也就是我们经常用到的,使用率、饱和度、IOPS、吞吐量以及响应时间等。这五个指标,是衡量磁盘性能的基本指标。

  • 使用率,是指磁盘处理 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 的大小,综合来分析。

举个例子,在数据库、大量小文件等这类随机读写比较多的场景中,IOPS 更能反映系统的整体性能;

而在多媒体等顺序读写较多的场景中,吞吐量才更能反映系统的整体性能。一般来说,我们在为应用程序的服务器选型时,要先对磁盘的 I/O 性能进行基准测试,以便可以准确评估,磁盘性能是否可以满足应用程序的需求。

磁盘 I/O 观测

iostat 是最常用的磁盘 I/O 性能观测工具,它提供了每个磁盘的使用率、IOPS、吞吐量等各种常见的性能指标,当然,这些指标实际上来自 /proc/diskstats。

# -d -x表示显示所有磁盘I/O的指标
$ iostat -d -x 1 
Device            r/s     w/s     rkB/s     wkB/s   rrqm/s   wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  svctm  %util 
loop0            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 
loop1            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 
sda              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 
sdb              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 

iostat 提供了非常丰富的性能指标。第一列的 Device 表示磁盘设备的名字,其他各列指标,虽然数量较多,但是每个指标的含义都很重要,如下图:

iostat-index-table.png

这些指标中,你要注意:

  • %util ,就是我们前面提到的磁盘 I/O 使用率;
  • r/s+ w/s ,就是 IOPS;
  • rkB/s+wkB/s ,就是吞吐量;
  • r_await+w_await ,就是响应时间。

在观测指标时,也别忘了结合请求的大小( rareq-sz 和 wareq-sz)一起分析。

进程 I/O 观测

除了每块磁盘的 I/O 情况,每个进程的 I/O 情况也是我们需要关注的重点。

上面提到的 iostat 只提供磁盘整体的 I/O 性能数据,缺点在于,并不能知道具体是哪些进程在进行磁盘读写。要观察进程的 I/O 情况,你还可以使用 pidstat 和 iotop 这两个工具。pidstat 是我们的老朋友了,这里我就不再啰嗦它的功能了。给它加上 -d 参数,你就可以看到进程的 I/O 情况,如下所示:

$ pidstat -d 1 
13:39:51      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command 
13:39:52      102       916      0.00      4.00      0.00       0  rsyslogd

从 pidstat 的输出你能看到,它可以实时查看每个进程的 I/O 情况,包括下面这些内容。

  • 用户 ID(UID)和进程 ID(PID) 。
    -= 每秒读取的数据大小(kB_rd/s) ,单位是 KB。
  • 每秒发出的写请求数据大小(kB_wr/s) ,单位是 KB。
  • 每秒取消的写请求数据大小(kB_ccwr/s) ,单位是 KB。
  • 块 I/O 延迟(iodelay),包括等待同步块 I/O 和换入块 I/O 结束的时间,单位是时钟周期。

除了可以用 pidstat 实时查看,根据 I/O 大小对进程排序,也是性能分析中一个常用的方法。这一点,我推荐另一个工具, iotop。它是一个类似于 top 的工具,你可以按照 I/O 大小对进程排序,然后找到 I/O 较大的那些进程。

iotop 的输出如下所示:

$ iotop
Total DISK READ :       0.00 B/s | Total DISK WRITE :       7.85 K/s 
Actual DISK READ:       0.00 B/s | Actual DISK WRITE:       0.00 B/s 
TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND 
15055 be/3 root        0.00 B/s    7.85 K/s  0.00 %  0.00 % systemd-journald 

案例分析(进程狂打日志)

有时候当你使用 top 进程发现系统的 iowait 异常,内存大量花销在 Cache/Buffer 时,就应该往 io 方面的问题考虑,如下:

# 按1切换到每个CPU的使用情况 
$ top 
top - 14:43:43 up 1 day,  1:39,  2 users,  load average: 2.48, 1.09, 0.63 
Tasks: 130 total,   2 running,  74 sleeping,   0 stopped,   0 zombie 
%Cpu0  :  0.7 us,  6.0 sy,  0.0 ni,  0.7 id, 92.7 wa,  0.0 hi,  0.0 si,  0.0 st 
%Cpu1  :  0.0 us,  0.3 sy,  0.0 ni, 92.3 id,  7.3 wa,  0.0 hi,  0.0 si,  0.0 st 
KiB Mem :  8169308 total,   747684 free,   741336 used,  6680288 buff/cache 
KiB Swap:        0 total,        0 free,        0 used.  7113124 avail Mem 

PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND 
18940 root      20   0  656108 355740   5236 R   6.3  4.4   0:12.56 python 
1312 root      20   0  236532  24116   9648 S   0.3  0.3   9:29.80 python3 

观察 top 的输出,会发现,CPU0 的使用率非常高,它的系统 CPU 使用率(sys%)为 6%,而 iowait 超过了 90%。这说明 CPU0 上,可能正在运行 I/O 密集型的进程。

接着查看进程部分的 CPU 使用情况。你会发现, python 进程的 CPU 使用率已经达到了 6%,而其余进程的 CPU 使用率都比较低,不超过 0.3%。看起来 python 是个可疑进程,记下 python 进程的 PID 号 18940,稍后分析。

最后再看内存的使用情况,总内存 8G,剩余内存只有 730 MB,而 Buffer/Cache 占用内存高达 6GB 之多,这说明内存主要被缓存占用。虽然大部分缓存可回收,我们还是得了解下缓存的去处,确认缓存使用都是合理的。

我们再使用 iostat 命令来观察 I/O 情况:

# -d表示显示I/O性能指标,-x表示显示扩展统计(即所有I/O指标) 
$ iostat -x -d 1 
Device            r/s     w/s     rkB/s     wkB/s   rrqm/s   wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  svctm  %util 
loop0            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 
sdb              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00 
sda              0.00   64.00      0.00  32768.00     0.00     0.00   0.00   0.00    0.00 7270.44 1102.18     0.00   512.00  15.50  99.20

观察 iostat 的最后一列,会看到,磁盘 sda 的 I/O 使用率已经高达 99%,很可能已经接近 I/O 饱和。

再看前面的各个指标,每秒写磁盘请求数是 64 ,写大小是 32 MB,写请求的响应时间为 7 秒,而请求队列长度则达到了 1100。

超慢的响应时间和特长的请求队列长度,进一步验证了 I/O 已经饱和的猜想。此时,sda 磁盘已经遇到了严重的性能瓶颈。

再继续使用 pidstat 或者 iotop 来观察进程的 I/O 情况,使用 pidstat 加上 -d 参数,就可以显示每个进程的 I/O 情况:

$ pidstat -d 1 

15:08:35      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command 
15:08:36        0     18940      0.00  45816.00      0.00      96  python 

15:08:36      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command 
15:08:37        0       354      0.00      0.00      0.00     350  jbd2/sda1-8 
15:08:37        0     18940      0.00  46000.00      0.00      96  python 
15:08:37        0     20065      0.00      0.00      0.00    1503  kworker/u4:2 

从 pidstat 的输出,你可以发现,只有 python 进程的写比较大,而且每秒写的数据超过 45 MB,比上面 iostat 发现的 32MB 的结果还要大。很明显,正是 python 进程导致了 I/O 瓶颈。

接下来,因为文件读写涉及到系统调用,我们在终端中运行 strace 命令,并通过 -p 18940 指定 python 进程的 PID 号查看:

$ strace -p 18940 
strace: Process 18940 attached 
...
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f7aee9000 
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f682e8000 
write(3, "2018-12-05 15:23:01,709 - __main"..., 314572844 
) = 314572844 
munmap(0x7f0f682e8000, 314576896)       = 0 
write(3, "\n", 1)                       = 1 
munmap(0x7f0f7aee9000, 314576896)       = 0 
close(3)                                = 0 
stat("/tmp/logtest.txt.1", {st_mode=S_IFREG|0644, st_size=943718535, ...}) = 0 

从上面的 write 系统调用可以看出,进程向文件描述符编号为3的文件中,写入了 300MB 的数据,从 stat 的系统调用中,可以看到,它正在获取 /tmp/logtest.txt.1 的状态。 这种“点 + 数字格式”的文件,在日志回滚中非常常见。我们可以猜测,这是第一个日志回滚文件,而正在写的日志文件路径,则是 /tmp/logtest.txt。

接下来,我们在终端中运行下面的 lsof 命令,看看进程 18940 都打开了哪些文件:

$ lsof -p 18940 
COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF    NODE NAME 
python  18940 root  cwd    DIR   0,50      4096 1549389 / 
python  18940 root  rtd    DIR   0,50      4096 1549389 / 
… 
python  18940 root    2u   CHR  136,0       0t0       3 /dev/pts/0 
python  18940 root    3w   REG    8,1 117944320     303 /tmp/logtest.txt 

其中:

  • FD 示文件描述符号
  • TYPE 表示文件类型
  • NAME 表示文件路径

至此,感觉找到了狂写磁盘的进程,就是进程ID为 18940 的 Python 进程。

I/O 性能问题定位

原文:套路篇:如何迅速分析出系统I/O的瓶颈在哪里?

问题定位得从量化的指标开始,从 CPU,内存一样,学习指标,掌握工具,定位问题。文件系统和磁盘 I/O 的性能指标都很有用,需要熟练掌握们才能应对千奇百怪的问题,为了方便记忆复习,偷了一张图,如下:

io性能指标

性能工具

掌握文件系统和磁盘 I/O 的性能指标后,还要知道,怎样去获取这些指标,也就是搞明白工具的使用问题。常用的工具及其能做的事情整理如下:

  • df,它既可以查看文件系统数据的空间容量,也可以查看索引节点的容量。至于文件系统缓存,可以通过 /proc/meminfo/proc/slabinfo 以及 slabtop 等各种来源,观察页缓存、目录项缓存、索引节点缓存以及具体文件系统的缓存情况。

  • iostatpidstat 可以查看磁盘和进程的 I/O 情况,都是最常用的 I/O 性能分析工具。通过 iostat ,我们可以得到磁盘的 I/O 使用率、吞吐量、响应时间以及 IOPS 等性能指标;而通过 pidstat ,则可以观察到进程的 I/O 吞吐量以及块设备 I/O 的延迟等。

  • 如果用 top 查看系统的 CPU 使用情况,发现 iowait 比较高;然后可以用 iostat 发现了磁盘的 I/O 使用率瓶颈,然后可以用 pidstat 找出了大量 I/O 的进程;最后,通过 stracelsof,我们找出了问题进程正在读写的文件,并最终锁定性能问题的来源。

查看性能指标时,我们可以有两种思路,一种是以需要的性能触发,找到相应的工具查看,如下图:

io-index-tool.png

另一种是,利用手头现有的工具得到尽可能想要的,如下图:

io-tool-index.png

一般情况下,多种性能指标间都有一定的关联性,不要完全孤立的看待他们。想弄清楚性能指标的关联性,就要通晓每种性能指标的工作原理。为了缩小排查范围,一般通常会先运行那几个支持指标较多的工具,如 iostat、vmstat、pidstat 等。然后再根据观察到的现象,结合系统和应用程序的原理,寻找下一步的分析方向,这个分析过程一般可以参考下图:

io-locate.png

I/O 基准测试

fio 是最常用的文件系统和磁盘 I/O 性能基准测试工具。它提供了大量的可定制化选项,可以用来测试,裸盘或者文件系统在各种场景下的 I/O 性能,包括了不同块大小、不同 I/O 引擎以及是否使用缓存等场景。

fio 的安装比较简单,你可以执行下面的命令来安装它,安装完成后,就可以执行 man fio 查询它的使用方法。:

# Ubuntu
apt-get install -y fio

# CentOS
yum install -y fio 

fio 的选项非常多,下面列出一些最常用的选项。这些常见场景包括随机读、随机写、顺序读以及顺序写等,你可以执行下面这些命令来测试:

# 随机读
fio -name=randread -direct=1 -iodepth=64 -rw=randread -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb

# 随机写
fio -name=randwrite -direct=1 -iodepth=64 -rw=randwrite -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb

# 顺序读
fio -name=read -direct=1 -iodepth=64 -rw=read -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb

# 顺序写
fio -name=write -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb 

参数解释如下:

  • direct,表示是否跳过系统缓存。上面示例中,设置的 1 ,就表示跳过系统缓存。
  • iodepth,表示使用异步 I/O(asynchronous I/O,简称 AIO)时,同时发出的 I/O 请求上限。在上面的示例中,设置的是 64。
  • rw,表示 I/O 模式。我的示例中, read/write 分别表示顺序读 / 写,而 randread/randwrite 则分别表示随机读 / 写。
  • ioengine,表示 I/O 引擎,它支持同步(sync)、异步(libaio)、内存映射(mmap)、网络(net)等各种 I/O 引擎。上面示例中,设置的 libaio 表示使用异步 I/O。
  • bs,表示 I/O 的大小。示例中设置成了 4K(这也是默认值)。
  • filename,表示文件路径,当然,它可以是磁盘路径(测试磁盘性能),也可以是文件路径(测试文件系统性能)。示例中,设置成了磁盘 /dev/sdb。不过注意,用磁盘路径测试写,会破坏这个磁盘中的文件系统,所以在使用前,你一定要事先做好数据备份。

下面是一个报告示例:

read: (g=0): rw=read, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.1
Starting 1 process
Jobs: 1 (f=1): [R(1)][100.0%][r=16.7MiB/s,w=0KiB/s][r=4280,w=0 IOPS][eta 00m:00s]
read: (groupid=0, jobs=1): err= 0: pid=17966: Sun Dec 30 08:31:48 2018
read: IOPS=4257, BW=16.6MiB/s (17.4MB/s)(1024MiB/61568msec)
    slat (usec): min=2, max=2566, avg= 4.29, stdev=21.76
    clat (usec): min=228, max=407360, avg=15024.30, stdev=20524.39
    lat (usec): min=243, max=407363, avg=15029.12, stdev=20524.26
    clat percentiles (usec):
    |  1.00th=[   498],  5.00th=[  1020], 10.00th=[  1319], 20.00th=[  1713],
    | 30.00th=[  1991], 40.00th=[  2212], 50.00th=[  2540], 60.00th=[  2933],
    | 70.00th=[  5407], 80.00th=[ 44303], 90.00th=[ 45351], 95.00th=[ 45876],
    | 99.00th=[ 46924], 99.50th=[ 46924], 99.90th=[ 48497], 99.95th=[ 49021],
    | 99.99th=[404751]
bw (  KiB/s): min= 8208, max=18832, per=99.85%, avg=17005.35, stdev=998.94, samples=123
iops        : min= 2052, max= 4708, avg=4251.30, stdev=249.74, samples=123
lat (usec)   : 250=0.01%, 500=1.03%, 750=1.69%, 1000=2.07%
lat (msec)   : 2=25.64%, 4=37.58%, 10=2.08%, 20=0.02%, 50=29.86%
lat (msec)   : 100=0.01%, 500=0.02%
cpu          : usr=1.02%, sys=2.97%, ctx=33312, majf=0, minf=75
IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
    submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
    complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
    issued rwt: total=262144,0,0, short=0,0,0, dropped=0,0,0
    latency   : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=16.6MiB/s (17.4MB/s), 16.6MiB/s-16.6MiB/s (17.4MB/s-17.4MB/s), io=1024MiB (1074MB), run=61568-61568msec

Disk stats (read/write):
sdb: ios=261897/0, merge=0/0, ticks=3912108/0, in_queue=3474336, util=90.09% 

这个报告中,需要我们重点关注的是, slat、clat、lat ,以及 bw 和 iops 这几行。先来看刚刚提到的前三个参数。事实上,slat、clat、lat 都是指 I/O 延迟(latency)。不同之处在于:

  • slat ,是指从 I/O 提交到实际执行 I/O 的时长(Submission latency);
  • clat ,是指从 I/O 提交到 I/O 完成的时长(Completion latency);
  • 而 lat ,指的是从 fio 创建 I/O 到 I/O 完成的总时长。

这里需要注意的是,对同步 I/O 来说,由于 I/O 提交和 I/O 完成是一个动作,所以 slat 实际上就是 I/O 完成的时间,而 clat 是 0。而从示例可以看到,使用异步 I/O(libaio)时,lat 近似等于 slat + clat 之和。

bw ,它代表吞吐量。在上面的示例中,可以看到,平均吞吐量大约是 16 MB(17005 KiB/1024)。

iops ,其实就是每秒 I/O 的次数,上面示例中的平均 IOPS 为 4250。

fio 支持 I/O 的重放。借助前面提到过的 blktrace,再配合上 fio,就可以实现对应用程序 I/O 模式的基准测试。你需要先用 blktrace ,记录磁盘设备的 I/O 访问情况;然后使用 fio ,重放 blktrace 的记录。

# 使用blktrace跟踪磁盘I/O,注意指定应用程序正在操作的磁盘
$ blktrace /dev/sdb

# 查看blktrace记录的结果
# ls
sdb.blktrace.0  sdb.blktrace.1

# 将结果转化为二进制文件
$ blkparse sdb -d sdb.bin

# 使用fio重放日志
$ fio --name=replay --filename=/dev/sdb --direct=1 --read_iolog=sdb.bin 

I/O 性能优化

I/O 优化可以从应用程序、文件系统以及磁盘角度来进行优化。

应用程序处于整个 I/O 栈的最上端,它可以通过系统调用,来调整 I/O 模式(如顺序还是随机、同步还是异步), 同时,它也是 I/O 数据的最终来源。优化应用程序的 I/O 性能有以下几种方式:

  • 可以用追加写代替随机写,减少寻址开销,加快 I/O 写的速度。
  • 可以借助缓存 I/O ,充分利用系统缓存,降低实际 I/O 的次数。
  • 可以在应用程序内部构建自己的缓存,或者用 Redis 这类外部缓存系统。这样,一方面,能在应用程序内部,控制缓存的数据和生命周期;另一方面,也能降低其他应用程序使用缓存对自身的影响。
  • 在需要频繁读写同一块磁盘空间时,可以用 mmap 代替 read/write,减少内存的拷贝次数。
  • 在需要同步写的场景中,尽量将写请求合并,而不是让每个请求都同步写入磁盘,即可以用 fsync() 取代 O_SYNC。
  • 在多个应用程序共享相同磁盘时,为了保证 I/O 不被某个应用完全占用,推荐使用 cgroups 的 I/O 子系统,来限制进程 / 进程组的 IOPS 以及吞吐量。

应用程序访问普通文件时,实际是由文件系统间接负责,文件在磁盘中的读写。所以,跟文件系统中相关的也有很多优化 I/O 性能的方式。如下:

  • 可以根据实际负载场景的不同,选择最适合的文件系统。比如 Ubuntu 默认使用 ext4 文件系统,而 CentOS 7 默认使用 xfs 文件系统。
  • 选好文件系统后,还可以进一步优化文件系统的配置选项,包括文件系统的特性(如 ext_attr、dir_index)、日志模式(如 journal、ordered、writeback)、挂载选项(如 noatime)等等。
  • 可以优化文件系统的缓存。

    • 可以优化 pdflush 脏页的刷新频率(比如设置 dirty_expire_centisecs 和 dirty_writeback_centisecs)以及脏页的限额(比如调整 dirty_background_ratio 和 dirty_ratio 等)。
    • 再如,还可以优化内核回收目录项缓存和索引节点缓存的倾向,即调整 vfs_cache_pressure(/proc/sys/vm/vfs_cache_pressure,默认值 100),数值越大,就表示越容易回收。
  • 在不需要持久化时,你还可以用内存文件系统 tmpfs,以获得更好的 I/O 性能 。

数据的持久化存储,最终还是要落到具体的物理磁盘中,同时,磁盘也是整个 I/O 栈的最底层。从磁盘角度出发,自然也有很多有效的性能优化方法。

  • 最简单有效的优化方法,就是换用性能更好的磁盘,比如用 SSD 替代 HDD。
  • 可以使用 RAID ,把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列。这样做既可以提高数据的可靠性,又可以提升数据的访问性能。
  • 针对磁盘和应用程序 I/O 模式的特征,我们可以选择最适合的 I/O 调度算法。比方说,SSD 和虚拟机中的磁盘,通常用的是 noop 调度算法。而数据库应用,推荐使用 deadline 算法。
  • 可以对应用程序的数据,进行磁盘级别的隔离。比如,我们可以为日志、数据库等 I/O 压力比较重的应用,配置单独的磁盘。
  • 在顺序读比较多的场景中,可以增大磁盘的预读数据,比如,你可以通过下面两种方法,调整 /dev/sdb 的预读大小。

    • 调整内核选项 /sys/block/sdb/queue/read_ahead_kb,默认大小是 128 KB,单位为 KB。
    • 使用 blockdev 工具设置,比如 blockdev —setra 8192 /dev/sdb,注意这里的单位是 512B(0.5KB),所以它的数值总是 read_ahead_kb 的两倍。
  • 要注意,磁盘本身出现硬件错误,也会导致 I/O 性能急剧下降,可以查看 dmesg 中是否有硬件 I/O 故障的日志。 还可以使用 badblocks、smartctl 等工具,检测磁盘的硬件问题,或用 e2fsck 等来检测文件系统的错误。如果发现问题,你可以使用 fsck 等工具来修复。

网络性能篇

同 CPU、内存以及 I/O 一样,网络也是 Linux 系统最核心的功能。网络是一种把不同计算机或网络设备连接到一起的技术,它本质上是一种进程间通信方式,特别是跨系统的进程间通信,必须要通过网络才能进行。随着高并发、分布式、云计算、微服务等技术的普及,网络的性能也变得越来越重要。

网络模型

谈起网络模型,大家肯定都知道 OSI 七层模型以及 TCP/IP 网络模型,其中七层模型分别为:

  • 应用层,负责为应用程序提供统一的接口。
  • 表示层,负责把数据转换成兼容接收系统的格式。
  • 会话层,负责维护计算机之间的通信连接。
  • 传输层,负责为数据加上传输表头,形成数据包。
  • 网络层,负责数据的路由和转发。
  • 数据链路层,负责 MAC 寻址、错误侦测和改错。
  • 物理层,负责在物理网络中传输数据帧。

但是由于其模型复杂,建议虽好,但是没人实现,所以在 Linux 中经常用到的是 TCP/IP 四层模型:

  • 应用层,负责向用户提供一组应用程序,比如 HTTP、FTP、DNS 等。
  • 传输层,负责端到端的通信,比如 TCP、UDP 等。
  • 网络层,负责网络包的封装、寻址和路由,比如 IP、ICMP 等。
  • 网络接口层,负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。

TCP/IP 四层模型和 OSI 模型之间并不是孤立的,是存在对应关系的:

网络模型

Linux 网络栈

有了 TCP/IP 模型后,在进行网络传输时,数据包就会按照协议栈,对上一层发来的数据进行逐层处理;然后封装上该层的协议头,再发送给下一层。

当然,网络包在每一层的处理逻辑,都取决于各层采用的网络协议。比如在应用层,一个提供 REST API 的应用,可以使用 HTTP 协议,把它需要传输的 JSON 数据封装到 HTTP 协议中,然后向下传递给 TCP 层。

而封装做的事情就很简单了,只是在原来的负载前后,增加固定格式的元数据,原始的负载数据并不会被修改。比如,以通过 TCP 协议通信的网络包为例,通过下面这张图,我们可以看到,应用程序数据在每个层的封装格式。

其中:

  • 传输层在应用程序数据前面增加了 TCP 头;
  • 网络层在 TCP 数据包前增加了 IP 头;
  • 而网络接口层,又在 IP 数据包前后分别增加了帧头和帧尾。

这些新增的头部和尾部,都按照特定的协议格式填充,其中增加了网络包的大小,但我们都知道,物理链路中并不能传输任意大小的数据包。网络接口配置的最大传输单元(MTU),就规定了最大的 IP 包大小。在我们最常用的以太网中,MTU 默认值是 1500(这也是 Linux 的默认值)。

一旦网络包超过 MTU 的大小,就会在网络层分片,以保证分片后的 IP 包不大于 MTU 值。显然,MTU 越大,需要的分包也就越少,自然,网络吞吐能力就越好。

理解了 TCP/IP 网络模型和网络包的封装原理后,很容易能想到,Linux 内核中的网络栈,其实也类似于 TCP/IP 的四层结构。如下图所示,就是 Linux 通用 IP 网络栈的示意图:

linux 通用 IP 网络栈

我们从上到下来看这个网络栈,可以发现,

  • 最上层的应用程序,需要通过系统调用,来跟套接字接口进行交互;
  • 套接字的下面,就是我们前面提到的传输层、网络层和网络接口层;
  • 最底层,则是网卡驱动程序以及物理网卡设备。

网卡是发送和接收网络包的基本设备。在系统启动过程中,网卡通过内核中的网卡驱动程序注册到系统中。而在网络收发过程中,内核通过中断跟网卡进行交互。网卡硬中断只处理最核心的网卡数据读取或发送,而协议栈中的大部分逻辑,都会放到软中断中处理,因为网络包的处理非常复杂。

网络收包流程

当一个网络帧到达网卡后,网卡会通过 DMA 方式,把这个网络包放到收包队列中;然后通过硬中断,告诉中断处理程序已经收到了网络包。

接着,网卡中断处理程序会为网络帧分配内核数据结构(sk_buff),并将其拷贝到 sk_buff 缓冲区中;然后再通过软中断,通知内核收到了新的网络帧。

接下来,内核协议栈从缓冲区中取出网络帧,并通过网络协议栈,从下到上逐层处理这个网络帧。比如:

  • 在链路层检查报文的合法性,找出上层协议的类型(比如 IPv4 还是 IPv6),再去掉帧头、帧尾,然后交给网络层。

  • 网络层取出 IP 头,判断网络包下一步的走向,比如是交给上层处理还是转发。当网络层确认这个包是要发送到本机后,就会取出上层协议的类型(比如 TCP 还是 UDP),去掉 IP 头,再交给传输层处理。

  • 传输层取出 TCP 头或者 UDP 头后,根据 < 源 IP、源端口、目的 IP、目的端口 > 四元组作为标识,找出对应的 Socket,并把数据拷贝到 Socket 的接收缓存中。

  • 最后,应用程序就可以使用 Socket 接口,读取到新接收到的数据了。

如下图,左半部分表示接收流程,右半部分表示发包流程,粉色箭头表示处理路径:

收发流程

网络发包流程

网络包的发送流程就是上图的右半部分,很容易发现,网络包的发送方向,正好跟接收方向相反。

首先,应用程序调用 Socket API(比如 sendmsg)发送网络包。由于这是一个系统调用,所以会陷入到内核态的套接字层中。套接字层会把数据包放到 Socket 发送缓冲区中。

接下来,网络协议栈从 Socket 发送缓冲区中,取出数据包;再按照 TCP/IP 栈,从上到下逐层处理。比如,传输层和网络层,分别为其增加 TCP 头和 IP 头,执行路由查找确认下一跳的 IP,并按照 MTU 大小进行分片。

分片后的网络包,再送到网络接口层,进行物理地址寻址,以找到下一跳的 MAC 地址。然后添加帧头和帧尾,放到发包队列中。这一切完成后,会有软中断通知驱动程序:发包队列中有新的网络帧需要发送。

最后,驱动程序通过 DMA ,从发包队列中读出网络帧,并通过物理网卡把它发送出去。

网络性能指标

我们通常用带宽、吞吐量、延时、PPS(Packet Per Second)等指标衡量网络的性能。

  • 带宽,表示链路的最大传输速率,单位通常为 b/s (比特 / 秒)。
  • 吞吐量,表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽限制,而吞吐量 / 带宽,也就是该网络的使用率。
  • 延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。PPS,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。
  • PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。

除了这些指标,网络的可用性(网络能否正常通信)、并发连接数(TCP 连接数量)、丢包率(丢包百分比)、重传率(重新传输的网络包比例)等也是常用的性能指标。

网络配置查看

分析网络问题的第一步,通常是查看网络接口的配置和状态。可以使用 ifconfig 或者 ip 命令,来查看网络的配置。

ifconfig 和 ip 分别属于软件包 net-tools 和 iproute2,iproute2 是 net-tools 的下一代。通常情况下它们会在发行版中默认安装。但如果你找不到 ifconfig 或者 ip 命令,可以安装这两个软件包。

ifconfig 示例:

root@iZ94lcu45k0Z:~# ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.18.142.1  netmask 255.255.240.0  broadcast 172.18.143.255
        ether 00:16:3e:02:c0:3c  txqueuelen 1000  (Ethernet)
        RX packets 20372388  bytes 2446988456 (2.4 GB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 23773340  bytes 7281209455 (7.2 GB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

root@iZ94lcu45k0Z:~#

ip 命令示例:

root@iZ94lcu45k0Z:~# ip -s addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:16:3e:02:c0:3c brd ff:ff:ff:ff:ff:ff
    inet 172.18.142.1/20 brd 172.18.143.255 scope global dynamic eth0
    valid_lft 302392834sec preferred_lft 302392834sec
    RX: bytes  packets  errors  dropped overrun mcast
    2447069436 20373350 0       0       0       0
    TX: bytes  packets  errors  dropped carrier collsns
    7281352242 23773865 0       0       0       0
root@iZ94lcu45k0Z:~#

ifconfig 和 ip 命令输出的指标基本相同,只是显示格式略微不同。比如,它们都包括了网络接口的状态标志、MTU 大小、IP、子网、MAC 地址以及网络包收发的统计信息。其中:

  • 网络接口的状态标志。ifconfig 输出中的 RUNNING ,或 ip 输出中的 LOWER_UP ,都表示物理网络是连通的,即网卡已经连接到了交换机或者路由器中。如果你看不到它们,通常表示网线被拔掉了。

  • MTU 的大小。MTU 默认大小是 1500,根据网络架构的不同(比如是否使用了 VXLAN 等叠加网络),你可能需要调大或者调小 MTU 的数值。

  • 网络接口的 IP 地址、子网以及 MAC 地址。这些都是保障网络功能正常工作所必需的,需要确保配置正确。

  • 网络收发的字节数、包数、错误数以及丢包情况,特别是 TX 和 RX 部分的 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,通常表示出现了网络 I/O 问题。其中:

    • errors 表示发生错误的数据包数,比如校验错误、帧同步错误等;
    • dropped 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer,但因为内存不足等原因丢包;
    • overruns 表示超限数据包数,即网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;
    • carrier 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等;
    • collisions 表示碰撞数据包数。
套接字信息

可以用 netstat 或者 ss ,来查看套接字、网络栈、网络接口以及路由表的信息。比如:

# head -n 3 表示只显示前面3行
# -l 表示只显示监听套接字
# -n 表示显示数字地址和端口(而不是名字)
# -p 表示显示进程信息
$ netstat -nlp | head -n 3
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      840/systemd-resolve

# -l 表示只显示监听套接字
# -t 表示只显示 TCP 套接字
# -n 表示显示数字地址和端口(而不是名字)
# -p 表示显示进程信息
$ ss -ltnp | head -n 3
State    Recv-Q    Send-Q        Local Address:Port        Peer Address:Port
LISTEN   0         128           127.0.0.53%lo:53               0.0.0.0:*        users:(("systemd-resolve",pid=840,fd=13))
LISTEN   0         128                 0.0.0.0:22               0.0.0.0:*        users:(("sshd",pid=1459,fd=3))

netstat 和 ss 的输出也是类似的,都展示了套接字的状态、接收队列、发送队列、本地地址、远端地址、进程 PID 和进程名称等。

其中,接收队列(Recv-Q)和发送队列(Send-Q)需要你特别关注,它们通常应该是 0。当你发现它们不是 0 时,说明有网络包的堆积发生。当然还要注意,在不同套接字状态下,它们的含义不同。

当套接字处于连接状态(Established)时,

  • Recv-Q 表示套接字缓冲还没有被应用程序取走的字节数(即接收队列长度)。
  • Send-Q 表示还没有被远端主机确认的字节数(即发送队列长度)。

当套接字处于监听状态(Listening)时,

  • Recv-Q 表示全连接队列的长度。
  • Send-Q 表示全连接队列的最大长度。

所谓全连接,是指服务器收到了客户端的 ACK,完成了 TCP 三次握手,然后就会把这个连接挪到全连接队列中。这些全连接中的套接字,还需要被 accept() 系统调用取走,服务器才可以开始真正处理客户端的请求。

协议栈统计信息

使用 netstat 或 ss ,也可以查看协议栈的信息:

$ netstat -s
...
Tcp:
    3244906 active connection openings
    23143 passive connection openings
    115732 failed connection attempts
    2964 connection resets received
    1 connections established
    13025010 segments received
    17606946 segments sent out
    44438 segments retransmitted
    42 bad segments received
    5315 resets sent
    InCsumErrors: 42
...

$ ss -s
Total: 186 (kernel 1446)
TCP:   4 (estab 1, closed 0, orphaned 0, synrecv 0, timewait 0/0), ports 0

Transport Total     IP        IPv6
*    1446      -         -
RAW    2         1         1
UDP    2         2         0
TCP    4         3         1
...

这些协议栈的统计信息都很直观。ss 只显示已经连接、关闭、孤儿套接字等简要统计,而 netstat 则提供的是更详细的网络协议栈信息。

比如,上面 netstat 的输出示例,就展示了 TCP 协议的主动连接、被动连接、失败重试、发送和接收的分段数量等各种信息。

网络吞吐和 PPS

给 sar 增加 -n 参数就可以查看网络的统计信息,比如网络接口(DEV)、网络接口错误(EDEV)、TCP、UDP、ICMP 等等。执行下面的命令,你就可以得到网络接口统计信:

# 数字1表示每隔1秒输出一组数据
$ sar -n DEV 1
Linux 4.15.0-1035-azure (ubuntu)   01/06/19   _x86_64_  (2 CPU)

13:21:40        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
13:21:41         eth0     18.00     20.00      5.79      4.25      0.00      0.00      0.00      0.00
13:21:41      docker0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
13:21:41           lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00

简单解释下它们的含义:

  • rxpck/s 和 txpck/s 分别是接收和发送的 PPS,单位为包 / 秒。
  • rxkB/s 和 txkB/s 分别是接收和发送的吞吐量,单位是 KB/ 秒。
  • rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包 / 秒。
  • %ifutil 是网络接口的使用率,即半双工模式下为 (rxkB/s+txkB/s)/Bandwidth,而全双工模式下为 max(rxkB/s, txkB/s)/Bandwidth。

其中,Bandwidth 可以用 ethtool 来查询,它的单位通常是 Gb/s 或者 Mb/s,不过注意这里小写字母 b ,表示比特而不是字节。我们通常提到的千兆网卡、万兆网卡等,单位也都是比特。如下你可以看到,我的 eth0 网卡就是一个千兆网卡:

$ ethtool eth0 | grep Speed
Speed: 1000Mb/s
连通性和延时

我们通常使用 ping ,来测试远程主机的连通性和延时,而这基于 ICMP 协议。比如,执行下面的命令,你就可以测试本机到 114.114.114.114 这个 IP 地址的连通性和延时:

# -c3表示发送三次ICMP包后停止
$ ping -c3 114.114.114.114
PING 114.114.114.114 (114.114.114.114) 56(84) bytes of data.
64 bytes from 114.114.114.114: icmp_seq=1 ttl=54 time=244 ms
64 bytes from 114.114.114.114: icmp_seq=2 ttl=47 time=244 ms
64 bytes from 114.114.114.114: icmp_seq=3 ttl=67 time=244 ms

--- 114.114.114.114 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 244.023/244.070/244.105/0.034 ms

ping 的输出,可以分为两部分。

第一部分,是每个 ICMP 请求的信息,包括 ICMP 序列号(icmp_seq)、TTL(生存时间,或者跳数)以及往返延时。

第二部分,则是三次 ICMP 请求的汇总。

比如上面的示例显示,发送了 3 个网络包,并且接收到 3 个响应,没有丢包发生,这说明测试主机到 114.114.114.114 是连通的;平均往返延时(RTT)是 244ms,也就是从发送 ICMP 开始,到接收到 114.114.114.114 回复的确认,总共经历 244ms。

网络性能基准测试

转发性能

网络接口层和网络层,它们主要负责网络包的封装、寻址、路由以及发送和接收。在这两个网络协议层中,每秒可处理的网络包数 PPS,就是最重要的性能指标。特别是 64B 小包的处理能力,值得我们特别关注。

Linux 内核自带的高性能网络测试工具 pktgen。pktgen 支持丰富的自定义选项,方便你根据实际需要构造所需网络包,从而更准确地测试出目标服务器的性能。

不过,在 Linux 系统中,你并不能直接找到 pktgen 命令。因为 pktgen 作为一个内核线程来运行,需要你加载 pktgen 内核模块后,再通过 /proc 文件系统来交互。下面就是 pktgen 启动的两个内核线程和 /proc 文件系统的交互文件:

$ modprobe pktgen
$ ps -ef | grep pktgen | grep -v grep
root     26384     2  0 06:17 ?        00:00:00 [kpktgend_0]
root     26385     2  0 06:17 ?        00:00:00 [kpktgend_1]
$ ls /proc/net/pktgen/
kpktgend_0  kpktgend_1  pgctrl

pktgen 在每个 CPU 上启动一个内核线程,并可以通过 /proc/net/pktgen 下面的同名文件,跟这些线程交互;而 pgctrl 则主要用来控制这次测试的开启和停止。

在使用 pktgen 测试网络性能时,需要先给每个内核线程 kpktgend_X 以及测试网卡,配置 pktgen 选项,然后再通过 pgctrl 启动测试。

以发包测试为例,假设发包机器使用的网卡是 eth0,而目标机器的 IP 地址为 192.168.0.30,MAC 地址为 11:11:11:11:11:11。

接下来,就是一个发包测试的示例。

# 定义一个工具函数,方便后面配置各种测试选项
function pgset() {
    local result
    echo $1 > $PGDEV

    result=`cat $PGDEV | fgrep "Result: OK:"`
    if [ "$result" = "" ]; then
        cat $PGDEV | fgrep Result:
    fi
}

# 为0号线程绑定eth0网卡
PGDEV=/proc/net/pktgen/kpktgend_0
pgset "rem_device_all"   # 清空网卡绑定
pgset "add_device eth0"  # 添加eth0网卡

# 配置eth0网卡的测试选项
PGDEV=/proc/net/pktgen/eth0
pgset "count 1000000"    # 总发包数量
pgset "delay 5000"       # 不同包之间的发送延迟(单位纳秒)
pgset "clone_skb 0"      # SKB包复制
pgset "pkt_size 64"      # 网络包大小
pgset "dst 192.168.0.30" # 目的IP
pgset "dst_mac 11:11:11:11:11:11"  # 目的MAC

# 启动测试
PGDEV=/proc/net/pktgen/pgctrl
pgset "start"

测试完成后,结果可以从 /proc 文件系统中获取。通过下面代码段中的内容,我们可以查看刚才的测试报告:

$ cat /proc/net/pktgen/eth0
Params: count 1000000  min_pkt_size: 64  max_pkt_size: 64
    frags: 0  delay: 0  clone_skb: 0  ifname: eth0
    flows: 0 flowlen: 0
...
Current:
    pkts-sofar: 1000000  errors: 0
    started: 1534853256071us  stopped: 1534861576098us idle: 70673us
...
Result: OK: 8320027(c8249354+d70673) usec, 1000000 (64byte,0frags)
120191pps 61Mb/sec (61537792bps) errors: 0

可以看到,测试报告主要分为三个部分:

  • 第一部分的 Params 是测试选项;
  • 第二部分的 Current 是测试进度,其中, packts so far(pkts-sofar)表示已经发送了 100 万个包,也就表明测试已完成。
  • 第三部分的 Result 是测试结果,包含测试所用时间、网络包数量和分片、PPS、吞吐量以及错误数。
TCP/UDP 性能

iperf 或者 netperf 常用来测试 TCP 和 UDP 的性能,下面以 iperf 为例,看一下 TCP 性能的测试方法。目前,iperf 的最新版本为 iperf3,可以运行下面的命令来安装:

# Ubuntu
apt-get install iperf3
# CentOS
yum install iperf3

然后,在目标机器上启动 iperf 服务端:

# -s表示启动服务端,-i表示汇报间隔,-p表示监听端口
$ iperf3 -s -i 1 -p 10000

接着,在另一台机器上运行 iperf 客户端,运行测试:

# -c表示启动客户端,192.168.0.30为目标服务器的IP
# -b表示目标带宽(单位是bits/s)
# -t表示测试时间
# -P表示并发数,-p表示目标服务器监听端口
$ iperf3 -c 192.168.0.30 -b 1G -t 15 -P 2 -p 10000

稍等一会儿(15 秒)测试结束后,回到目标服务器,查看 iperf 的报告:

[ ID] Interval           Transfer     Bandwidth
...
[SUM]   0.00-15.04  sec  0.00 Bytes  0.00 bits/sec                  sender
[SUM]   0.00-15.04  sec  1.51 GBytes   860 Mbits/sec                  receiver
HTTP 性能

ab 是 Apache 自带的 HTTP 压测工具,主要测试 HTTP 服务的每秒请求数、请求延迟、吞吐量以及请求延迟的分布情况等。运行下面的命令,就可以安装 ab 工具:

# Ubuntu
$ apt-get install -y apache2-utils
# CentOS
$ yum install -y httpd-tools

测试案例结果:

# -c表示并发请求数为1000,-n表示总的请求数为10000
$ ab -c 1000 -n 10000 http://192.168.0.30/
...
Server Software:        nginx/1.15.8
Server Hostname:        192.168.0.30
Server Port:            80

...

Requests per second:    1078.54 [#/sec] (mean)
Time per request:       927.183 [ms] (mean)
Time per request:       0.927 [ms] (mean, across all concurrent requests)
Transfer rate:          890.00 [Kbytes/sec] received

Connection Times (ms)
            min  mean[+/-sd] median   max
Connect:        0   27 152.1      1    1038
Processing:     9  207 843.0     22    9242
Waiting:        8  207 843.0     22    9242
Total:         15  233 857.7     23    9268

Percentage of the requests served within a certain time (ms)
50%     23
66%     24
75%     24
80%     26
90%    274
95%   1195
98%   2335
99%   4663
100%   9268 (longest request)
应用负载性能

使用 ab 工具,可以得到某个页面的访问性能,但这个结果跟用户的实际请求,很可能不一致。因为用户请求往往会附带着各种各种的负载(payload),而这些负载会影响 Web 应用程序内部的处理逻辑,从而影响最终性能。

为了得到应用程序的实际性能,就要求性能工具本身可以模拟用户的请求负载,而 iperf、ab 这类工具就无能为力了。幸运的是,我们还可以用 wrk、TCPCopy、Jmeter 或者 LoadRunner 等实现这个目标。

wrk 为例,它是一个 HTTP 性能测试工具,内置了 LuaJIT,方便根据实际需求,生成所需的请求负载,或者自定义响应的处理方法。

wrk 工具本身不提供 yum 或 apt 的安装方法,需要通过源码编译来安装。比如,你可以运行下面的命令,来编译和安装 wrk:

$ https://github.com/wg/wrk
$ cd wrk
$ apt-get install build-essential -y
$ make
$ sudo cp wrk /usr/local/bin/

wrk 的命令行参数比较简单。比如,我们可以用 wrk ,来重新测一下前面已经启动的 Nginx 的性能。

 -c表示并发连接数1000,-t表示线程数为2
$ wrk -c 1000 -t 2 http://192.168.0.30/
Running 10s test @ http://192.168.0.30/
2 threads and 1000 connections
Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    65.83ms  174.06ms   1.99s    95.85%
    Req/Sec     4.87k   628.73     6.78k    69.00%
96954 requests in 10.06s, 78.59MB read
Socket errors: connect 0, read 0, write 0, timeout 179
Requests/sec:   9641.31
Transfer/sec:      7.82MB

wrk 最大的优势,是其内置的 LuaJIT,可以用来实现复杂场景的性能测试。wrk 在调用 Lua 脚本时,可以将 HTTP 请求分为三个阶段,即 setup、running、done,如下图所示:

wrk

比如,你可以在 setup 阶段,为请求设置认证参数:

-- example script that demonstrates response handling and
-- retrieving an authentication token to set on all future
-- requests

token = nil
path  = "/authenticate"

request = function()
    return wrk.format("GET", path)
end

response = function(status, headers, body)
    if not token and status == 200 then
        token = headers["X-Token"]
        path  = "/resource"
        wrk.headers["X-Token"] = token
    end
end

在执行测试时,通过 -s 选项,执行脚本的路径:

$ wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/ 

域名与 DNS 解析

域名由一串用点分割开的字符组成,被用作互联网中的某一台或某一组计算机的名称,目的就是为了方便识别,互联网中提供各种服务的主机位置。

域名是全球唯一的,需要通过专门的域名注册商才可以申请注册。为了组织全球互联网中的众多计算机,域名同样用点来分开,形成一个分层的结构。而每个被点分割开的字符串,就构成了域名中的一个层级,并且位置越靠后,层级越高。

极客时间的网站 time.geekbang.org 为例,来理解域名的含义。这个字符串中,最后面的 org 是顶级域名,中间的 geekbang 是二级域名,而最左边的 time 则是三级域名。

点(.)是所有域名的根,也就是说所有域名都以点作为后缀,也可以理解为,在域名解析的过程中,所有域名都以点结束。

域名

域名主要是为了方便让人记住,而 IP 地址是机器间的通信的真正机制。把域名转换为 IP 地址的服务,是域名解析服务(DNS),而对应的服务器就是域名服务器,网络协议则是 DNS 协议。

DNS 协议在 TCP/IP 栈中属于应用层,不过实际传输还是基于 UDP 或者 TCP 协议(UDP 居多) ,并且域名服务器一般监听在端口 53 上。

系统管理员在配置 Linux 系统的网络时,除了需要配置 IP 地址,还需要给它配置 DNS 服务器,这样它才可以通过域名来访问外部服务。可以执行下面的命令查看系统域名服务配置:

$ cat /etc/resolv.conf
nameserver 114.114.114.114

另外,DNS 服务通过资源记录的方式,来管理所有数据,它支持 A、CNAME、MX、NS、PTR 等多种类型的记录。比如:

  • A 记录,用来把域名转换成
  • IP 地址;CNAME 记录,用来创建别名;
  • NS 记录,则表示该域名对应的域名服务器地址。

当我们访问某个网址时,就需要通过 DNS 的 A 记录,查询该域名对应的 IP 地址,然后再通过该 IP 来访问 Web 服务。以极客时间的网站 time.geekbang.org 为例,执行下面的 nslookup 命令,就可以查询到这个域名的 A 记录,可以看到,它的 IP 地址是 39.106.233.176:

$ nslookup time.geekbang.org
# 域名服务器及端口信息
Server:    114.114.114.114
Address:  114.114.114.114#53

# 非权威查询结果
Non-authoritative answer:
Name:  time.geekbang.org
Address: 39.106.233.17

DNS 查询实际上是一个递归过程,可以通过 dig 命令来查看整个递归查询过程:

# +trace表示开启跟踪查询
# +nodnssec表示禁止DNS安全扩展
$ dig +trace +nodnssec time.geekbang.org

; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> +trace +nodnssec time.geekbang.org
;; global options: +cmd
.      322086  IN  NS  m.root-servers.net.
.      322086  IN  NS  a.root-servers.net.
.      322086  IN  NS  i.root-servers.net.
.      322086  IN  NS  d.root-servers.net.
.      322086  IN  NS  g.root-servers.net.
.      322086  IN  NS  l.root-servers.net.
.      322086  IN  NS  c.root-servers.net.
.      322086  IN  NS  b.root-servers.net.
.      322086  IN  NS  h.root-servers.net.
.      322086  IN  NS  e.root-servers.net.
.      322086  IN  NS  k.root-servers.net.
.      322086  IN  NS  j.root-servers.net.
.      322086  IN  NS  f.root-servers.net.
;; Received 239 bytes from 114.114.114.114#53(114.114.114.114) in 1340 ms

org.      172800  IN  NS  a0.org.afilias-nst.info.
org.      172800  IN  NS  a2.org.afilias-nst.info.
org.      172800  IN  NS  b0.org.afilias-nst.org.
org.      172800  IN  NS  b2.org.afilias-nst.org.
org.      172800  IN  NS  c0.org.afilias-nst.info.
org.      172800  IN  NS  d0.org.afilias-nst.org.
;; Received 448 bytes from 198.97.190.53#53(h.root-servers.net) in 708 ms

geekbang.org.    86400  IN  NS  dns9.hichina.com.
geekbang.org.    86400  IN  NS  dns10.hichina.com.
;; Received 96 bytes from 199.19.54.1#53(b0.org.afilias-nst.org) in 1833 ms

time.geekbang.org.  600  IN  A  39.106.233.176
;; Received 62 bytes from 140.205.41.16#53(dns10.hichina.com) in 4 ms

dig trace 的输出,主要包括四部分。

  • 第一部分,是从 114.114.114.114 查到的一些根域名服务器(.)的 NS 记录。
  • 第二部分,是从 NS 记录结果中选一个(h.root-servers.net),并查询顶级域名 org. 的 NS 记录。
  • 第三部分,是从 org. 的 NS 记录中选择一个(b0.org.afilias-nst.org),并查询二级域名 geekbang.org. 的 NS 服务器。
  • 第四部分,就是从 geekbang.org. 的 NS 服务器(dns10.hichina.com)查询最终主机 time.geekbang.org. 的 A 记录。
DNS 缓存

要为系统开启 DNS 缓存,就需要你做额外的配置。最简单的方法,就是使用 dnsmasq。dnsmasq 是最常用的 DNS 缓存服务之一,还经常作为 DHCP 服务来使用。它的安装和配置都比较简单,性能也可以满足绝大多数应用程序对 DNS 缓存的需求。

centos 安装:

yum -y install dnsmasq
systemctl start dnsmasq

tcpdump 抓包

tcpdump 是最常用的一个网络分析工具。它基于 libpcap ,利用内核中的 AF_PACKET 套接字,抓取网络接口中传输的网络包;并提供了强大的过滤规则,帮助从大量的网络包中,挑出最想关注的信息。

tcpdump 展示了每个网络包的详细细节,这就要求,在使用前,你必须要对网络协议有基本了解。而要了解网络协议的详细设计和实现细节, RFC 当然是最权威的资料。不过,RFC 的内容,对初学者来说可能并不友好。如果对网络协议还不太了解,推荐学习《TCP/IP 详解》,特别是第一卷的 TCP/IP 协议族。这是每个程序员都要掌握的核心基础知识。再回到 tcpdump 工具本身,它的基本使用方法,还是比较简单的,也就是 tcpdump [选项] [过滤表达式]

tcpdump 官方文档:https://www.tcpdump.org/manpages/tcpdump.1.html

fileter 手册:https://www.tcpdump.org/manpages/pcap-filter.7.html

tcpdump 提供了大量的选项以及过滤表达式,大多数情况下掌握常用的即可,常用的选项如下:

tcpdump 选项

常用的过滤选项如下:

tcpdump 过滤器

tcpdump 输出格式如下:

时间戳 协议 源地址.源端口 > 目的地址.目的端口 网络包详细信息

示例如下:

tcpdump -nn udp port 53 or host 35.190.27.188 -w ping.pcap

tcpdump -nn udp port 53 or host 35.190.27.188

DDOS 检测与防御

DDoS 的前身是 DoS(Denail of Service),即拒绝服务攻击,指利用大量的合理请求,来占用过多的目标资源,从而使目标服务无法响应正常请求。

DDoS(Distributed Denial of Service) 则是在 DoS 的基础上,采用了分布式架构,利用多台主机同时攻击目标主机。这样,即使目标服务部署了网络防御设备,面对大量网络请求时,还是无力应对。

从攻击的原理上来看,DDoS 可以分为下面几种类型。

  • 第一种,耗尽带宽。无论是服务器还是路由器、交换机等网络设备,带宽都有固定的上限。带宽耗尽后,就会发生网络拥堵,从而无法传输其他正常的网络报文。
  • 第二种,耗尽操作系统的资源。网络服务的正常运行,都需要一定的系统资源,像是 CPU、内存等物理资源,以及连接表等软件资源。一旦资源耗尽,系统就不能处理其他正常的网络连接。
  • 第三种,消耗应用程序的运行资源。应用程序的运行,通常还需要跟其他的资源或系统交互。如果应用程序一直忙于处理无效请求,也会导致正常请求的处理变慢,甚至得不到响应。

当有一天你的网站响应变慢,并且通过 tcpdump 查到很多 SYN 包的就要小心了,可能是遭遇了 SYN 攻击,例如:

# -i eth0 只抓取eth0网卡,-n不解析协议名和主机名
# tcp port 80表示只抓取tcp协议并且端口号为80的网络帧
$ tcpdump -i eth0 -n tcp port 80
09:15:48.287047 IP 192.168.0.2.27095 > 192.168.0.30: Flags [S], seq 1288268370, win 512, length 0
09:15:48.287050 IP 192.168.0.2.27131 > 192.168.0.30: Flags [S], seq 2084255254, win 512, length 0
09:15:48.287052 IP 192.168.0.2.27116 > 192.168.0.30: Flags [S], seq 677393791, win 512, length 0
09:15:48.287055 IP 192.168.0.2.27141 > 192.168.0.30: Flags [S], seq 1276451587, win 512, length 0
09:15:48.287068 IP 192.168.0.2.27154 > 192.168.0.30: Flags [S], seq 1851495339, win 512, length 0

这时候再通过 sar 命令查看网络报收发情况:

$ sar -n DEV 1
08:55:49        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
08:55:50      docker0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
08:55:50         eth0  22274.00    629.00   1174.64     37.78      0.00      0.00      0.00      0.02
08:55:50           lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00

发现有大量的小包(PPS 很大,但是 BPS却很小),这个时候要更加坚信遭受到的是 SYN 洪水攻击,它的原理是:

  • 客户端构造大量的 SYN 包,请求建立 TCP 连接;
  • 服务器收到包后,会向源 IP 发送 SYN+ACK 报文,并等待三次握手的最后一次 ACK 报文,直到超时。

等待状态的 TCP 连接,通常也称为半开连接。由于连接表的大小有限,大量的半开连接就会导致连接表迅速占满,从而无法建立新的 TCP 连接。参考下面这张 TCP 状态图,你能看到,此时,服务器端的 TCP 连接,会处于 SYN_RECEIVED 状态:

tcp状态图

查看 TCP 半开连接的方法,关键在于 SYN_RECEIVED 状态的连接。我们可以使用 netstat ,来查看所有连接的状态,不过要注意,SYN_REVEIVED 的状态,通常被缩写为 SYN_RECV,例如执行下面的 netstat 命令:

# -n表示不解析名字,-p表示显示连接所属进程
$ netstat -n -p | grep SYN_REC
tcp        0      0 192.168.0.30:80          192.168.0.2:12503      SYN_RECV    -
tcp        0      0 192.168.0.30:80          192.168.0.2:13502      SYN_RECV    -
tcp        0      0 192.168.0.30:80          192.168.0.2:15256      SYN_RECV    -
tcp        0      0 192.168.0.30:80          192.168.0.2:18117      SYN_RECV    -

遇到这种工具可以通过 linux 的防火墙以及调整内核参数进行初步的防御,可以通过 iptables 进行限制:

# 将来源ip为 192.168.0.2 的报文直接丢掉
$ iptables -I INPUT -s 192.168.0.2 -p tcp -j REJECT
# 限制syn并发数为每秒1次
$ iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT
# 限制单个IP在60秒新建立的连接数为10
$ iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT

半开状态的连接数增多可能组织你连接到server进行操作,可以通过 sysctl 命令调整系统内核参数:

$ sysctl -w net.ipv4.tcp_max_syn_backlog=1024
net.ipv4.tcp_max_syn_backlog = 1024
# 修改 SYN_RECV 状态的连接重试次数,默认是5
$ sysctl -w net.ipv4.tcp_synack_retries=1
net.ipv4.tcp_synack_retries = 1
# TCP SYN Cookies 也是一种专门防御 SYN Flood 攻击的方法,可以通过下面的方式开启
$ sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1

sysctl 命令修改的配置都是临时的,重启后这些配置就会丢失。所以,为了保证配置持久化,你还应该把这些配置,写入 /etc/sysctl.conf 文件中。

$ cat /etc/sysctl.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_max_syn_backlog = 1024

写入 /etc/sysctl.conf 的配置,需要执行 sysctl -p 命令后,才会动态生效

网络延迟测试

我们可以用 ping 来测试网络延迟,ping 基于 ICMP 协议,它通过计算 ICMP 回显响应报文与 ICMP 回显请求报文的时间差,来获得往返延时。这个过程并不需要特殊认证,常被很多网络攻击利用,比如端口扫描工具 nmap、组包工具 hping3 等等。

所以,为了避免这些问题,很多网络服务会把 ICMP 禁止掉,这也就导致我们无法用 ping ,来测试网络服务的可用性和往返延时

禁止ping有以下两种方法,第一种是修改内核参数: net.ipv4.icmp_echo_ignore_all,值为0表示允许,值为1表示禁止。

临时允许或禁止可通过:echo 0(1) >/proc/sys/net/ipv4/icmp_echo_ignore_all 来实现;

允许允许或禁止可通过修改 /etc/sysctl.conf 中的 net.ipv4.icmp_echo_ignore_all 配置选项来完成,最后通过 sysctl -p 命令来更新。

第二种实现通过防火墙进行限制,例如允许 ping 可以通过如下命令:

iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT

禁止ping可通过如下设置:

iptables -A INPUT -p icmp --icmp-type 8 -s 0/0 -j DROP

当 ping 不能用的时候,可以用 traceroute 或 hping3 的 TCP 和 UDP 模式,来获取网络延迟。例如:

# -c表示发送3次请求,-S表示设置TCP SYN,-p表示端口号为80
$ hping3 -c 3 -S -p 80 baidu.com
HPING baidu.com (eth0 123.125.115.110): S set, 40 headers + 0 data bytes
len=46 ip=123.125.115.110 ttl=51 id=47908 sport=80 flags=SA seq=0 win=8192 rtt=20.9 ms
len=46 ip=123.125.115.110 ttl=51 id=6788  sport=80 flags=SA seq=1 win=8192 rtt=20.9 ms
len=46 ip=123.125.115.110 ttl=51 id=37699 sport=80 flags=SA seq=2 win=8192 rtt=20.9 ms

--- baidu.com hping statistic ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 20.9/20.9/20.9 ms

从 hping3 的结果中,你可以看到,往返延迟 RTT 为 20.9ms。或者使用 traceroute 命令:

root@iZ94lcu45k0Z:~# traceroute --tcp -p 80 -n baidu.com
traceroute to baidu.com (220.181.38.148), 30 hops max, 60 byte packets
1  * * *
2  * * *
3  11.219.127.37  6.947 ms  7.282 ms 11.219.127.33  7.558 ms
4  11.219.127.146  4.666 ms  4.774 ms *
5  10.255.70.121  0.456 ms 10.54.164.89  0.456 ms 10.54.164.97  0.482 ms
6  117.49.38.38  1.007 ms 45.112.222.174  1.105 ms 117.49.38.86  0.450 ms
7  117.49.38.18  1.003 ms 117.49.37.242  1.945 ms 42.120.242.217  1.902 ms
8  183.61.45.13  1.897 ms 183.2.184.137  1.654 ms 183.61.45.5  2.172 ms
9  183.2.182.117  12.446 ms 58.61.162.129  2.909 ms *
10  119.147.220.137  5.560 ms 119.147.222.93  4.830 ms 119.147.220.133  4.914 ms
11  202.97.22.153  43.237 ms 202.97.44.161  47.276 ms *
12  36.110.246.130  37.567 ms 36.110.247.66  38.779 ms 36.110.246.134  44.276 ms
13  * * 36.110.246.197  37.131 ms
14  220.181.17.146  38.029 ms 220.181.182.30  38.523 ms 220.181.182.170  38.955 ms
15  * * *
16  * * *
17  * * *
18  * 220.181.38.148  39.111 ms *

traceroute 会在路由的每一跳发送三个包,并在收到响应后,输出往返延时。如果无响应或者响应超时(默认 5s),就会输出一个星号。

快速确认和Nagle算法

https://time.geekbang.org/column/article/82833

NAT 性能优化

网络地址转换(Network Address Translation),缩写为 NAT,也是一个可能导致网络延迟的因素。NAT 技术可以重写 IP 数据包的源 IP 或者目的 IP,被普遍地用来解决公网 IP 地址短缺的问题。它的主要原理就是,网络中的多台主机,通过共享同一个公网 IP 地址,来访问外网资源。同时,由于 NAT 屏蔽了内网网络,自然也就为局域网中的机器提供了安全隔离。

NAT 的主要目的,是实现地址转换。根据实现方式的不同,NAT 可以分为三类:

  • 静态 NAT,即内网 IP 与公网 IP 是一对一的永久映射关系;
  • 动态 NAT,即内网 IP 从公网 IP 池中,动态选择一个进行映射;
  • 网络地址端口转换 NAPT(Network Address and Port Translation),即把内网 IP 映射到公网 IP 的不同端口上,让多个内网 IP 可以共享同一个公网 IP 地址。

NAPT 是目前最流行的 NAT 类型,我们在 Linux 中配置的 NAT 也是这种类型。而根据转换方式的不同,可以把 NAPT 分为三类。

  • 第一类是源地址转换 SNAT,即目的地址不变,只替换源 IP 或源端口。SNAT 主要用于,多个内网 IP 共享同一个公网 IP ,来访问外网资源的场景。
  • 第二类是目的地址转换 DNAT,即源 IP 保持不变,只替换目的 IP 或者目的端口。DNAT 主要通过公网 IP 的不同端口号,来访问内网的多种服务,同时会隐藏后端服务器的真实 IP 地址。
  • 第三类是双向地址转换,即同时使用 SNAT 和 DNAT。当接收到网络包时,执行 DNAT,把目的 IP 转换为内网 IP;而在发送网络包时,执行 SNAT,把源 IP 替换为外部 IP。

双向地址转换,其实就是外网 IP 与内网 IP 的一对一映射关系,所以常用在虚拟化环境中,为虚拟机分配浮动的公网 IP 地址。原理如下图:

NAT

从图中,你可以发现:

  • 当服务器访问 baidu.com 时,NAT 网关会把源地址,从服务器的内网 IP 192.168.0.2 替换成公网 IP 地址 100.100.100.100,然后才发送给 baidu.com;
  • 当 baidu.com 发回响应包时,NAT 网关又会把目的地址,从公网 IP 地址 100.100.100.100 替换成服务器内网 IP 192.168.0.2,然后再发送给内网中的服务器。

网络性能优化

由于底层是其上方各层的基础,底层性能也就决定了高层性能。所以我们要清楚,底层性能指标,其实就是对应高层的极限性能。我们从下到上来理解这一点。

首先是网络接口层和网络层,它们主要负责网络包的封装、寻址、路由,以及发送和接收。每秒可处理的网络包数 PPS,就是它们最重要的性能指标(特别是在小包的情况下)。可以用内核自带的发包工具 pktgen ,来测试 PPS 的性能。

再向上到传输层的 TCP 和 UDP,它们主要负责网络传输。对它们而言,吞吐量(BPS)、连接数以及延迟,就是最重要的性能指标。可以用 iperf 或 netperf ,来测试传输层的性能。不过要注意,网络包的大小,会直接影响这些指标的值。所以,通常,你需要测试一系列不同大小网络包的性能。

最后,再往上到了应用层,最需要关注的是吞吐量(BPS)、每秒请求数以及延迟等指标。你可以用 wrk、ab 等工具,来测试应用程序的性能。不过,这里要注意的是,测试场景要尽量模拟生产环境,这样的测试才更有价值。比如,可以到生产环境中,录制实际的请求情况,再到测试中回放。总之,根据这些基准指标,再结合已经观察到的性能瓶颈,我们就可以明确性能优化的目标。

网络性能工具

从网络性能指标出发,更容易把性能工具同系统工作原理关联起来,对性能问题有宏观的认识和把握。这样,当想查看某个性能指标时,就能清楚知道,可以用哪些工具。总结如下图:

网络性能工具

从性能工具出发,迅速找出想要观察的性能指标。特别是在工具有限的情况下,我们更要充分利用好手头的每一个工具,用少量工具也要尽力挖掘出大量信息。

网络性能工具

应用程序优化

从网络 I/O 的角度来说,主要有下面两种优化思路。

  • 第一种是最常用的 I/O 多路复用技术 epoll,主要用来取代 select 和 poll。这其实是解决 C10K 问题的关键,也是目前很多网络应用默认使用的机制。

  • 第二种是使用异步 I/O(Asynchronous I/O,AIO)。AIO 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。等到 I/O 完成后,系统会用事件通知的方式,告诉应用程序结果。不过,AIO 的使用比较复杂,你需要小心处理很多边缘情况。

而从进程的工作模型来说,也有两种不同的模型用来优化。

  • 第一种,主进程 + 多个 worker 子进程。其中,主进程负责管理网络连接,而子进程负责实际的业务处理。这也是最常用的一种模型。
  • 第二种,监听到相同端口的多进程模型。在这种模型下,所有进程都会监听相同接口,并且开启 SO_REUSEPORT 选项,由内核负责,把请求负载均衡到这些监听进程中去。

除了网络 I/O 和进程的工作模型外,应用层的网络协议优化,也是至关重要的一点,总结了常见的几种优化方法。

  • 使用长连接取代短连接,可以显著降低 TCP 建立连接的成本。在每秒请求次数较多时,这样做的效果非常明显。

  • 使用内存等方式,来缓存不常变化的数据,可以降低网络 I/O 次数,同时加快应用程序的响应速度。

  • 使用 Protocol Buffer 等序列化的方式,压缩网络 I/O 的数据量,可以提高应用程序的吞吐。

  • 使用 DNS 缓存、预取、HTTPDNS 等方式,减少 DNS 解析的延迟,也可以提升网络 I/O 的整体速度。

套接字优化

套接字可以屏蔽掉 Linux 内核中不同协议的差异,为应用程序提供统一的访问接口。每个套接字,都有一个读写缓冲区。读缓冲区,缓存了远端发过来的数据。如果读缓冲区已满,就不能再接收新的数据。写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会被阻塞。所以,为了提高网络的吞吐量,通常需要调整这些缓冲区的大小。比如:

  • 增大每个套接字的缓冲区大小 net.core.optmem_max;
  • 增大套接字接收缓冲区大小 net.core.rmem_max 和发送缓冲区大小 net.core.wmem_max;
  • 增大 TCP 接收缓冲区大小 net.ipv4.tcp_rmem 和发送缓冲区大小 net.ipv4.tcp_wmem。

套接字的内核选项,整理成了一个表格,方便在需要时参考:

套接字内核选项

tcp_rmem 和 tcp_wmem 的三个数值分别是 min,default,max,系统会根据这些设置,自动调整 TCP 接收 / 发送缓冲区的大小。

udp_mem 的三个数值分别是 min,pressure,max,系统会根据这些设置,自动调整 UDP 发送缓冲区的大小。

表格中的数值只提供参考价值,具体应该设置多少,还需要根据实际的网络状况来确定。比如,发送缓冲区大小,理想数值是吞吐量 * 延迟,这样才可以达到最大网络利用率。

除此之外,套接字接口还提供了一些配置选项,用来修改网络连接的行为:

  • 为 TCP 连接设置 TCP_NODELAY 后,就可以禁用 Nagle 算法;
  • 为 TCP 连接开启 TCP_CORK 后,可以让小包聚合成大包后再发送(注意会阻塞小包的发送);
  • 使用 SO_SNDBUF 和 SO_RCVBUF ,可以分别调整套接字发送缓冲区和接收缓冲区的大小。
传输层优化

在请求数比较大的场景下,可能会看到大量处于 TIME_WAIT 状态的连接,它们会占用大量内存和端口资源。这时,我们可以优化与 TIME_WAIT 状态相关的内核选项,比如采取下面几种措施。

  • 增大处于 TIME_WAIT 状态的连接数量 net.ipv4.tcp_max_tw_buckets ,并增大连接跟踪表的大小 net.netfilter.nf_conntrack_max。
  • 减小 net.ipv4.tcp_fin_timeout 和 net.netfilter.nf_conntrack_tcp_timeout_time_wait ,让系统尽快释放它们所占用的资源。

  • 开启端口复用 net.ipv4.tcp_tw_reuse。这样,被 TIME_WAIT 状态占用的端口,还能用到新建的连接中。

  • 增大本地端口的范围 net.ipv4.ip_local_port_range 。这样就可以支持更多连接,提高整体的并发能力。

  • 增加最大文件描述符的数量。你可以使用 fs.nr_open 和 fs.file-max ,分别增大进程和系统的最大文件描述符数;或在应用程序的 systemd 配置文件中,配置 LimitNOFILE ,设置应用程序的最大文件描述符数。

为了缓解 SYN FLOOD 等,利用 TCP 协议特点进行攻击而引发的性能问题,可以考虑优化与 SYN 状态相关的内核选项,比如采取下面几种措施。

  • 增大 TCP 半连接的最大数量 net.ipv4.tcp_max_syn_backlog ,或者开启 TCP SYN Cookies net.ipv4.tcp_syncookies ,来绕开半连接数量限制的问题(注意,这两个选项不可同时使用)。

  • 减少 SYN_RECV 状态的连接重传 SYN+ACK 包的次数 net.ipv4.tcp_synack_retries

在长连接的场景中,通常使用 Keepalive 来检测 TCP 连接的状态,以便对端连接断开后,可以自动回收。但是,系统默认的 Keepalive 探测间隔和重试次数,一般都无法满足应用程序的性能要求。所以,这时候需要优化与 Keepalive 相关的内核选项,比如:

  • 缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time;
  • 缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl;
  • 减少 Keepalive 探测失败后,一直到通知应用程序前的重试次数 net.ipv4.tcp_keepalive_probes。

传输层优化

优化 TCP 性能时,你还要注意,如果同时使用不同优化方法,可能会产生冲突。比如,就像网络请求延迟的案例中我们曾经分析过的,服务器端开启 Nagle 算法,而客户端开启延迟确认机制,就很容易导致网络延迟增大。

UDP 的优化。UDP 提供了面向数据报的网络协议,它不需要网络连接,也不提供可靠性保障。所以,UDP 优化,相对于 TCP 来说,要简单得多。这里也总结了常见的几种优化方案。跟上篇套接字部分提到的一样,

  • 增大套接字缓冲区大小以及 UDP 缓冲区范围;
  • 跟前面 TCP 部分提到的一样,增大本地端口号的范围;
  • 根据 MTU 大小,调整 UDP 数据包的大小,减少或者避免分片的发生
网络层优化

网络层,负责网络包的封装、寻址和路由,包括 IP、ICMP 等常见协议。在网络层,最主要的优化,其实就是对路由、 IP 分片以及 ICMP 等进行调优。

第一种,从路由和转发的角度出发,可以调整下面的内核选项。

  • 在需要转发的服务器中,比如用作 NAT 网关的服务器或者使用 Docker 容器时,开启 IP 转发,即设置 net.ipv4.ip_forward = 1。

  • 调整数据包的生存周期 TTL,比如设置 net.ipv4.ip_default_ttl = 64。注意,增大该值会降低系统性能。

  • 开启数据包的反向地址校验,比如设置 net.ipv4.conf.eth0.rp_filter = 1。这样可以防止 IP 欺骗,并减少伪造 IP 带来的 DDoS 问题。

第二种,从分片的角度出发,最主要的是调整 MTU(Maximum Transmission Unit)的大小。

通常,MTU 的大小应该根据以太网的标准来设置。以太网标准规定,一个网络帧最大为 1518B,那么去掉以太网头部的 18B 后,剩余的 1500 就是以太网 MTU 的大小。在使用 VXLAN、GRE 等叠加网络技术时,要注意,网络叠加会使原来的网络包变大,导致 MTU 也需要调整。比如,就以 VXLAN 为例,它在原来报文的基础上,增加了 14B 的以太网头部、 8B 的 VXLAN 头部、8B 的 UDP 头部以及 20B 的 IP 头部。换句话说,每个包比原来增大了 50B。所以,我们就需要把交换机、路由器等的 MTU,增大到 1550, 或者把 VXLAN 封包前(比如虚拟化环境中的虚拟网卡)的 MTU 减小为 1450。另外,现在很多网络设备都支持巨帧,如果是这种环境,你还可以把 MTU 调大为 9000,以提高网络吞吐量。

第三种,从 ICMP 的角度出发,为了避免 ICMP 主机探测、ICMP Flood 等各种网络问题,可以通过内核选项,来限制 ICMP 的行为。

  • 比如,你可以禁止 ICMP 协议,即设置 net.ipv4.icmp_echo_ignore_all = 1。这样,外部主机就无法通过 ICMP 来探测主机。

  • 或者,你还可以禁止广播 ICMP,即设置 net.ipv4.icmp_echo_ignore_broadcasts = 1。

链路层优化

链路层优化

其他优化方式

第一种,使用 DPDK 技术,跳过内核协议栈,直接由用户态进程用轮询的方式,来处理网络请求。同时,再结合大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。

第二种,使用内核自带的 XDP 技术,在网络包进入内核协议栈前,就对其进行处理,这样也可以实现很好的性能。

火焰图展示 perf record 数据

例如我们采用如下方式获取指定进程的调用栈信息:

perf record -a -g -p 9 — sleep 30

针对 perf 汇总数据的展示问题,Brendan Gragg 发明了火焰图,通过矢量图的形式,更直观展示汇总结果。下图就是一个针对 mysql 的火焰图示例。

这张图看起来像是跳动的火焰,因此也就被称为火焰图。要理解火焰图,我们最重要的是区分清楚横轴和纵轴的含义。

  • 横轴表示采样数和采样比例。一个函数占用的横轴越宽,就代表它的执行时间越长。同一层的多个函数,则是按照字母来排序。

  • 纵轴表示调用栈,由下往上根据调用关系逐个展开。换句话说,上下相邻的两个函数中,下面的函数,是上面函数的父函数。这样,调用栈越深,纵轴就越高。

  • 另外,要注意图中的颜色,并没有特殊含义,只是用来区分不同的函数。

火焰图是动态的矢量图格式,所以它还支持一些动态特性。比如,鼠标悬停到某个函数上时,就会自动显示这个函数的采样数和采样比例。而当你用鼠标点击函数时,火焰图就会把该层及其上的各层放大,方便你观察这些处于火焰图顶部的调用栈的细节。

上面 mysql 火焰图的示例,就表示了 CPU 的繁忙情况,这种火焰图也被称为 on-CPU 火焰图。如果我们根据性能分析的目标来划分,火焰图可以分为下面这几种。

  • on-CPU 火焰图:表示 CPU 的繁忙情况,用在 CPU 使用率比较高的场景中。
  • off-CPU 火焰图:表示 CPU 等待 I/O、锁等各种资源的阻塞情况。
  • 内存火焰图:表示内存的分配和释放情况。
  • 热 / 冷火焰图:表示将 on-CPU 和 off-CPU 结合在一起综合展示。
  • 差分火焰图:表示两个火焰图的差分情况,红色表示增长,蓝色表示衰减。差分火焰图常用来比较不同场景和不同时期的火焰图,以便分析系统变化前后对性能的影响情况。

接下来,运用火焰图来观察刚才 perf record 得到的记录。

首先,我们先下载几个能从 perf record 记录生成火焰图的工具,这些工具都放在 https://github.com/brendangregg/FlameGraph 上面。你可以执行下面的命令来下载:

$ git clone https://github.com/brendangregg/FlameGraph
$ cd FlameGraph

安装好工具后,要生成火焰图,其实主要需要三个步骤:

  • 执行 perf script ,将 perf record 的记录转换成可读的采样记录;

  • 执行 stackcollapse-perf.pl 脚本,合并调用栈信息;

  • 执行 flamegraph.pl 脚本,生成火焰图。

不过,在 Linux 中,我们可以使用管道,来简化这三个步骤的执行过程。假设刚才用 perf record 生成的文件路径为 /root/perf.data,执行下面的命令,你就可以直接生成火焰图:

$ perf script -i /root/perf.data | ./stackcollapse-perf.pl —all | ./flamegraph.pl > ksoftirqd.svg

执行成功后,使用浏览器打开 ksoftirqd.svg ,就可以看到生成的火焰图了。如下图所示:

火焰图

内核线程

在 Linux 中,用户态进程的“祖先”,都是 PID 号为 1 的 init 进程。比如,现在主流的 Linux 发行版中,init 都是 systemd 进程;而其他的用户态进程,会通过 systemd 来进行管理。

Linux 中的各种进程,除了用户态进程外,还有大量的内核态线程。按说内核态的线程,应该先于用户态进程启动,可是 systemd 只管理用户态进程。那么,内核态线程又是谁来管理的呢?

实际上,Linux 在启动过程中,有三个特殊的进程,也就是 PID 号最小的三个进程。

  • 0 号进程为 idle 进程,这也是系统创建的第一个进程,它在初始化 1 号和 2 号进程后,演变为空闲任务。当 CPU 上没有其他任务执行时,就会运行它。

  • 1 号进程为 init 进程,通常是 systemd 进程,在用户态运行,用来管理其他用户态进程。

  • 2 号进程为 kthreadd 进程,在内核态运行,用来管理内核线程。所以,要查找内核线程,我们只需要从 2 号进程开始,查找它的子孙进程即可。比如,你可以使用 ps 命令,来查找 kthreadd 的子进程:

    $ ps -f —ppid 2 -p 2
    UID PID PPID C STIME TTY TIME CMD
    root 2 0 0 12:02 ? 00:00:01 [kthreadd]
    root 9 2 0 12:02 ? 00:00:21 [ksoftirqd/0]
    root 10 2 0 12:02 ? 00:11:47 [rcu_sched]
    root 11 2 0 12:02 ? 00:00:18 [migration/0]

    root 11094 2 0 14:20 ? 00:00:00 [kworker/1:0-eve]
    root 11647 2 0 14:27 ? 00:00:00 [kworker/0:2-cgr]

从上面的输出,内核线程的名称(CMD)都在中括号里(这一点,我们前面内容也有提到过)。所以,更简单的方法,就是直接查找名称包含中括号的进程。比如:

$ ps -ef | grep "\[.*\]"
root         2     0  0 08:14 ?        00:00:00 [kthreadd]
root         3     2  0 08:14 ?        00:00:00 [rcu_gp]
root         4     2  0 08:14 ?        00:00:00 [rcu_par_gp]
...

了解内核线程的基本功能,对我们排查问题有非常大的帮助。比如,我们曾经在软中断案例中提到过 ksoftirqd。它是一个用来处理软中断的内核线程,并且每个 CPU 上都有一个。

如果知道了这一点,那么,以后遇到 ksoftirqd 的 CPU 使用高的情况,就会首先怀疑是软中断的问题,然后从软中断的角度来进一步分析。

其实,除了刚才看到的 kthreadd 和 ksoftirqd 外,还有很多常见的内核线程,我们在性能分析中都经常会碰到,比如下面这几个内核线程。

  • kswapd0:用于内存回收。在 Swap 变高 案例中,我曾介绍过它的工作原理。
  • kworker:用于执行内核工作队列,分为绑定 CPU (名称格式为 kworker/CPU86330)和未绑定 CPU(名称格式为 kworker/uPOOL86330)两类。
  • migration:在负载均衡过程中,把进程迁移到 CPU 上。每个 CPU 都有一个 migration 内核线程。
  • jbd2/sda1-8:jbd 是 Journaling Block Device 的缩写,用来为文件系统提供日志功能,以保证数据的完整性;名称中的 sda1-8,表示磁盘分区名称和设备号。每个使用了 ext4 文件系统的磁盘分区,都会有一个 jbd2 内核线程。
  • pdflush:用于将内存中的脏页(被修改过,但还未写入磁盘的文件页)写入磁盘(已经在 3.10 中合并入了 kworker 中)。

性能实战常用命令

  1. sysstat 是一个软件包,包含监测系统性能及效率的一组工具,这些工具对于我们收集系统性能数据,比如CPU使用率、硬盘和网络吞吐数据,这些数据的收集和分析,有利于我们判断系统是否正常运行,是提高系统运行效率、安全运行服务器的得力助手。包含了一下工具
    • iostat 工具提供CPU使用率及硬盘吞吐效率的数据; #比较核心的工具
    • mpstat 工具提供单个处理器或多个处理器相关数据;
    • pidstat 关于运行中的进程/任务、CPU、内存等的统计信息
    • tapestat reports statistics for tape drives connected to the system.
    • cifsiostat reports CIFS statistics.
    • sar 是一个系统活动报告工具,既可以实时查看系统的当前活动,又可以配置保存和报告历史统计数据。
  2. dstat 是一个新的性能工具,它吸收了 vmstat、iostat、ifstat 等几种工具的优点,可以同时观察系统的 CPU、磁盘 I/O、网络以及内存使用情况

  3. perf-tools Performance analysis tools based on Linux perf_events (aka perf) and ftrace

  4. perf is a performance counter for Linux. With it you can know many secrets of the running linux system.

    ubuntu: sudo apt install linux-tools-common gawk
    centos: sudo yum install perf gawk
    debian: apt-get install -y linux-tools-common linux-tools-generic linux-tools-$(uname -r))
    linux: apt-get install -y linux-perf

  5. stressstress-ng 压力测试工具

  6. sysbench is a scriptable multi-threaded benchmark tool based on LuaJIT. It is most frequently used for database benchmarks, but can also be used to create arbitrarily complex workloads that do not involve a database server.

  7. strace 是最常用的跟踪进程系统调用的工具

    -f 查看线程信息
    strace -f -p 27458

    -f表示跟踪子进程和子线程,-T表示显示系统调用的时长,-tt表示显示跟踪时间
    strace -f -T -tt -p 9085

     来观察 wrk 的系统调用
     $ strace -f wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/
     ...
     setsockopt(52, SOL_TCP, TCP_NODELAY, [1], 4) = 0
     ...
    
  1. pstree 用来显示进程的父子关系,安装方法:

    mac: brew install pstree
    centos: yum -y install psmisc
    ubuntu: apt-get install psmisc

  2. hping3 是一个可以构造 TCP/IP 协议数据包的工具,可以对系统进行安全审计、防火墙测试等。

    -S 参数表示设置TCP协议的SYN(同步序列号),-p表示目的端口为80
    -i u10表示每隔10微秒发送一个网络帧 —rand-source 随机化源ip —flood 尽可能块的发包
    hping3 -S -p 80 -i u10 192.168.0.30

    测试到远端服务器的延迟,可代替 ping 命令
    hping3 -c 3 -S -p 80 baidu.com

  3. tcpdump 是一个常用的网络抓包工具,常用来分析各种网络问题。

  4. bindfs 基本功能是实现目录绑定(类似于 mount --bind

    $ mkdir /tmp/foo
    $ PID=$(docker inspect --format {{.State.Pid}} phpfpm)
    $ bindfs /proc/$PID/root /tmp/foo
    $ perf report --symfs /tmp/foo
    
    使用完成后不要忘记解除绑定
    $ umount /tmp/foo/
    
  5. pidof 根据名称查找正在运行的进程id,例如:pidof sshd

  6. pgrep 根据名字查找进程,例如:pgrep -u root sshd

  7. pkill 根据名字发送信号,例如:pkill -HUP syslogdqq

  8. bcc Tools for BPF-based Linux IO analysis, networking, monitoring, and more

    • cachestat 提供了整个操作系统缓存的读写命中情况。
    • cachetop 提供了每个进程的缓存命中情况。
    • memleak 可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出一个未释放内存和相应调用栈的汇总情况(默认 5 秒)。
    • filetop 基于 Linux 内核的 eBPF(extended Berkeley Packet Filters)机制,主要跟踪内核中文件的读写情况,并输出线程 ID(TID)、读写大小、读写类型以及文件名称。
    • opensnoop 动态跟踪内核中的 open 系统调用

    ubuntu:
    sudo apt-key adv —keyserver keyserver.ubuntu.com —recv-keys 4052245BD4284CDD
    echo “deb https://repo.iovisor.org/apt/xenial xenial main” | sudo tee /etc/apt/sources.list.d/iovisor.list
    sudo apt-get update
    sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)

    centos 可能需要升级内核:
    升级系统
    yum update -y
    安装ELRepo
    rpm —import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
    rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm
    安装新内核
    yum remove -y kernel-headers kernel-tools kernel-tools-libs
    yum —enablerepo=”elrepo-kernel” install -y kernel-ml kernel-ml-devel kernel-ml-headers kernel-ml-tools > kernel-ml-tools-libs kernel-ml-tools-libs-devel
    更新Grub后重启
    grub2-mkconfig -o /boot/grub2/grub.cfg
    grub2-set-default 0
    reboot
    重启后确认内核版本已升级为4.20.0-1.el7.elrepo.x86_64
    uname -r
    安装bcc-tools
    yum install -y bcc-tools
    配置PATH路径
    export PATH=$PATH:/usr/share/bcc/tools
    验证安装成功
    cachestat

    可以把工具加入到系统路径中
    export PATH=$PATH:/usr/share/bcc/tools

  9. pcstat 查看文件在内存中的缓存大小以及缓存比例。

    export GOPATH=~/go
    export PATH=~/go/bin:$PATH
    go get golang.org/x/sys/unix
    go get github.com/tobert/pcstat/pcstat

  10. dd 作为一个磁盘和文件的拷贝工具,经常被拿来测试磁盘或者文件系统的读写性能

    生成一个512MB的临时文件
    dd if=/dev/sda1 of=file bs=1M count=512
    清理缓存
    echo 3 > /proc/sys/vm/drop_caches

  11. 如何统计所有进程的物理内存使用量?

    使用grep查找Pss指标后,再用awk计算累加值
    grep Pss /proc/[1-9]*/smaps | awk ‘{total+=$2}; END {printf “%d kB\n”, total }’
    391266 kB

  12. fio 测试磁盘的 IOPS、吞吐量以及响应时间等核心指标

  13. linux shell while $ while true; do curl http://192.168.0.10:10000/products/geektime; sleep 5; done

  14. lsof 查看指定进程打开的文件

    lsof -p 12875

  15. pstree 查看进程父子关系

    -t表示显示线程,-a表示显示命令行参数
    pstree -t -a -p 27458

  16. nsenter 命令是一个可以在指定进程的命令空间下运行指定程序的命令。它位于util-linux包中。

    一个最典型的用途就是进入容器的网络命令空间。相当多的容器为了轻量级,是不包含较为基础的命令的,比如说ip address,ping,telnet,ss,tcpdump等等命令,这就给调试容器网络带来相当大的困扰:只能通过docker inspect ContainerID命令获取到容器IP,以及无法测试和其他网络的连通性。这时就可以使用nsenter命令仅进入该容器的网络命名空间,使用宿主机的命令调试容器网络。

    参考阅读:

  17. wrk HTTP 应用负载测试

    # 测试80端口性能 -c表示并发连接数100,-t表示线程数为2
    $ # wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30/
    Running 10s test @ http://192.168.0.30/
    2 threads and 100 connections
    Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     9.19ms   12.32ms 319.61ms   97.80%
        Req/Sec     6.20k   426.80     8.25k    85.50%
    Latency Distribution
        50%    7.78ms
        75%    8.22ms
        90%    9.14ms
        99%   50.53ms
    123558 requests in 10.01s, 100.15MB read
    Requests/sec:  12340.91
    Transfer/sec:     10.00MB
    

课外阅读文章

  1. 深入理解linux系统下proc文件系统内容
  2. cpu 核心数与线程数
  3. Linux 入门必看:如何60秒内分析Linux性能