许吉友 - 运维

CPU 性能排查及调优

用户态与内核态

Linux 系统的空间被分成内核空间和用户空间,执行级别也分为用户态和内核态,且两个空间彼此隔离。

用户进程只能访问自己的空间,它不能读取和修改内核空间的数据,也不能执行内核空间的代码。

CPU 大部分时间都在执行用户空间的代码,也尽量应该让 CPU 运行用户态的代码。

系统调用

如果用户进程想要执行某些影响整个系统的操作(比如操作IO设备),它就要通过系统调用向内核发出请求。如果内核确认其有权这么做,会代表用户进程 完成相关操作。在此期间,内核可以访问用户进程的地址空间。系统调用完成后再返回用户态。

中断

除了系统调用之外,还有一种内核态的使用是中断事件,它是一种信号,发送者可以是硬件,也可以是软件。

程序异常也是一种中断。

上下文切换

除了系统调用和中断处理,抢占式多任务也是消耗内核态资源的主要因素,现代程序都是讲究高并发的,并发是伪并行。

CPU 时间被切分为无数细小的时间片,分发给所有进程,根据进程重要程度,其所获取的时间片大小叶不尽相同。

每个进程依靠时间片来获取CPU的计算处理服务。当时间片花完,内核会从进程收回控制权,转而服务另一个进程。而当前进程的运行时环境(CPU寄存器和页表等内容)都会被保存起来,等到该进程在下轮执行时,再将它的运行时环境复原。这样一个进程切换的过程被称为上下文切换(Context Switch)。

在一个进程超多的系统环境中,上下文切换会非常频繁,这意味着时间片被大量损耗,尤其在进程游走于多个核心之间的时候,这种跨核心切换对系统性能负载影响更大。

使用 vmstat 命令查看上下文切换数量:

$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
13  1 251648 20669048   1036 80835904    0    0  1300   282    0    0 19  6 74  1  0
 8  0 251648 20656740   1036 80855072    0    0     0 226766 88956 142889 22 10 67  2  0
10  0 251648 20599216   1036 80917840    0    0 19328 45345 88058 156199 23 10 63  4  0

其中 cs 就是上下文切换次数。

为优化上下文切换,由以下几种方案应对。

减少无谓的系统调用

strace 是一个用来跟踪系统调用的程序,如果发现 sys 资源占用率高,可以使用 strace 检测一下。

strace -c cat /dev/sdd > /dev/null
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 93.94    8.711861          70    123821           read
  6.06    0.562135           4    123820           write
  0.00    0.000000           0         3           close
  0.00    0.000000           0         5           fstat
  0.00    0.000000           0         7           mmap
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         4           brk
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         2         1 arch_prctl
  0.00    0.000000           0         1           fadvise64
  0.00    0.000000           0         4           openat
------ ----------- ----------- --------- --------- ----------------
100.00    9.273996                247674         2 total

如果 errors 过多,则说明这个程序执行了很多无效的调用。

调整中断分配

irqbalance 是用来控制中断请求的服务,它默认每隔10秒会对中断进行一次平均分配。这种方式未必是最优的。当中断发生时,如果从用户态切换到内核态,,这也是一种上下文切换。

在多处理器环境下,可以考虑让特定的中断在特定的核心上运行,减少或者禁止在某些核心上发生中断,确保重要的进程可以一直运行而不被打扰。

查看 irqbalance 服务状态:

$ systemctl status irqbalance

这个服务的service文件是 /usr/lib/systemd/system/irqbalance.service,配置文件是 /etc/sysconfig/irqbalance

如果想这个服务只运行一次,可以设置 ONESHOT=yes

IRQBALANCE_BANNED_CPUS 可以指定那谢谢核心上不能发生中断。

/proc/irq/ 中列出了所有中断。

查看中断所在CPU的脚本:

#!/bin/bash
for i in `ls /proc/irq/ | grep -P "[0-9]" | sort -n`
do
    echo IRQ-$i: `cat /proc/irq/$i/smp_affinity_list`
done

输出示例:

IRQ-0: 0-47
IRQ-1: 0-47
IRQ-2: 0-47
IRQ-3: 0-47
IRQ-4: 0-47
IRQ-5: 0-47
IRQ-6: 0-47
IRQ-7: 0-47
IRQ-8: 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31
IRQ-9: 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31
IRQ-10: 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31
IRQ-11: 0-47
IRQ-12: 0-47

将中断绑定到某些核心1、3、5、7上:

$ printf '%0x' $[ 2**1 + 2**3 + 2**5 + 2**7 ] > /proc/irq/10/smp_affinity

将进程绑定到核心

默认情况下,进程可以游走于所有的可用核心之间。最好限定进程只在某些核心上面运行,以防止跨核心上下文切换

# 查看 pid 的 CPU 亲和度的 mask 值
$ sudo taskset -p 43203 # 返回 pid 43203's current affinity mask: ffffffff
# 查看人类可读的cpu列表:
$ sudo taskset -pc 43203 # 返回 pid 43203's current affinity list: 0-31
# 启动之前设置 CPU 列表
$ sudo taskset -c <CPU_LIST> <COMMAND>
# 调整一个已经存在的进程的CPU亲和度
$ sudo taskset -p <MASK> <PID>

taskset 虽然可以设置CPU的亲和度,但它无法保证核心分配到的是本地内存,所以 numactl 是推荐替代 taskset 的新方案。

除此之外,在 cgroup 中也可以进行绑核操作。

减少进程数量

业务拆分,加机器!!!

Cache 命中率

CPU中有一级缓存、二级缓存,CPU 中的缓存比内存快得多,所以如果缓存命中率高的话,性能会非常好。

CPU 中的缓存是按行进行缓存的!例如一个二维数组,会先缓存第一行,而不是缓存第一列!

valgrind 命令可以测量 Cache 的命中率,命令如下:

$ valgrind --tool=cachegrind ./command

CPU 调度算法

linux内核的三种调度方法:

实时进程将得到优先调用,实时进程根据实时优先级决定调度权值,分时进程则通过nice和counter值决定权值,nice越小,counter越大,被调度的概率越大,也就是曾经使用了cpu最少的进程将会得到优先调度。

进程大体上分为实时进程和非实时进程,比如音频播放就是典型的实时进程,实时进程的调度策略分为 SCHED_FIFI 和 SCHED_RR。

实时调度适合那些进程数少,且延迟影响严重的场景。使用实时调度的最佳实践是从较低的优先级开始,然后根据实际情况提升优先级,切不可直接将优先级调至最高,以防系统内部的关键进程阻塞在后面无法执行。

只要系统中有实时进程在运行,它总是优先于非实时进程。所以在使用实时调度前,需将它们隔离到特定的核心上去,绑定核心时最好从后向前分配,不要让实时调度策略影响到所有核心,以防止造成阻塞。

如果多个进程需要设置成 SCHED_FIFO,在设置优先级时,要考虑他们是否有依赖关系。优先级会影响执行顺序!!!!

实时优先级的取值范围时 1-99,优先级操作方法:

$ sudo chrt -p 1226
# 启动时设置优先级,其中 -f 是 SCHED_FIFO,-r 是 SCHED_RR
$ sudo chrt -f [1-99] <COMMAND>
$ sudo chrt -r [1-99] <COMMAND>
# 调整优先级
$ sudo chrt -p -r [1-99] <PID>
$ sudo chrt -p -r [1-99] <PID>

分时调度

在 Linux 服务器中,大部分守护进程都是分时调度,他们的调度类型都是 SCHED_OUTER。

SCHED_OTHER只能在静态优先级为0时使用(普通线程)。SCHED_OTHER是标准的Linux分时调度策略(不需要实时机制)。

如何从静态优先级为0的列表中选择运行的线程取决于列表中的dynamic优先级。dynamic优先级基于nice值,且在每次线程准备运行时增加。这种机制保证公平处理所有的SCHED_OTHER线程。

在Linux内核源码中,SCHED_OTHER被称为SCHED_NORMAL。

在 top 命令中,NI 的值就是 NICE 值。

NICE值为-20到+19,普通进程默认为 0。

NICE 值越高,说明它越高尚,愿意把自己的时间奉献给别人。

系统中重要的进程 NICE 值会为 -20。

设置 NICE 值的命令:

$ nice -n [-20-19] <COMMAND>
$ renice [-20-19] <PID>