前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Off-CPU分析:窥见冰山下的性能瓶颈

Off-CPU分析:窥见冰山下的性能瓶颈

作者头像
AshinZ
发布2023-11-01 17:06:34
3200
发布2023-11-01 17:06:34
举报

遇到CPU性能问题时,我们常常通过perf来了解CPU上到底在执行什么,以及通过On-CPU火焰图来帮助我们寻找性能瓶颈。但是,这种方式并不能让我们知道不在CPU上运行的进程和线程到底在做什么。在一些场景中,我们会发现CPU的使用率上不去,性能表现很差,这时候我们也许就需要考虑,是不是花在应用请求、异步调用这种Off-CPU的场景上的时间太多了。

Off-CPU分析

性能问题可以划分成两个维度:

  • On-CPU分析:考虑运行在CPU上的进程/线程
  • Off-CPU分析:考虑因为I/O、锁、计数器、换页等事件阻塞的时间

Off-CPU分析是一种研究Off-CPU时间开销、执行上下文等内容的性能分析方法。和CPU Profiling不同,Off-CPU不关注运行在CPU上的进程,它更关注那些被阻塞的进程。如下图所示,很多种原因都会让进程/线程进入到阻塞状态,可能是进程自身的磁盘I/O、网络I/O等;也可能是因为别的进程导致的上下文切换:总之,这些都会引入延时。

线程状态 https://www.brendangregg.com/offcpuanalysis.html

Off-CPU分析是On-CPU分析的补充,这样我们就可以理解整个线程执行的时间里都干了些什么。Off-CPU分析和追踪技术不一样,因为其主要关内核调度器中阻塞的部分,很容易获得全部的信息。

Off-CPU分析方法

在开始介绍Off-CPU分析之前,可以先了解一些常见的分析方法。

CPU 采样

很多传统的Profiling工具通过采样来获取整个CPU上执行任务的指令地址或者调用栈信息,并且进行计数统计。这能够帮助我们估算整个系统上的执行情况。例如我们常常通过perf来进行CPU采样:

代码语言:javascript
复制
perf record -ag -F 999 -- sleep 5 

考虑如下的调用关系:

A call B

尽管通过On-CPU分析我们可以得到热点路径,但是我们并不能知道程序不在CPU上的时候发生了什么。

应用追踪

Application Tracing

通过追踪我们可以知道函数开始和退出的时间,从而计算出函数执行的时间。如果我们能够知道消耗的时间和CPU时间,我们就可以知道哪些是拖慢CPU运行的,哪些是被阻塞的。

追踪的优点是我们可以知道很精确的时间,但是缺点也很明显:如果我们追踪全部的进程,我们可以知道全部的信息,但是也会带来很高的负载从而影响性能;而如果我们只观察某个程序,可能我们寻找尝试很久才能找到那个真正影响性能的程序。

Off-CPU追踪

Off-CPU Tracing

Off-CPU追踪只追踪切换进程的内核系统函数信息,包括时间戳和用户态调用栈。这样我们就可以聚焦于Off-CPU事件,而不需要去追踪全部的应用函数,或者去猜测尝试是哪个应用。

Off-CPU Sampling

Off-CPU Sampling

Off-CPU采样通过时序采样来捕获被阻塞的线程调用栈信息,也可以通过wall-time采样来实现:采样全部时间全部线程的信息,然后再进行过滤。

Off-CPU采样在系统采样器中很少使用。在On-CPU采样中,每当CPU的计时器产生中断,就会记录当前CPU上进程的调用栈信息,从而产生一次记录;然而Off-CPU采样需要做与之不同的事情,要么在每个应用中设置计时器唤醒他们并记录堆栈,要么让内核按照一定时间间隔遍历线程并捕获堆栈。

Off-CPU开销

进行Off-CPU追踪的时候,我们需要注意切换(Scheduler)事件是非常频繁的:在某些极端场景中,每秒钟会有上百万次切换事件,在这种情况中,即使每次追踪只会增加很小的负载,最终负载也会因为过多的切换事件而变得非常大。Off-CPU采样也有相同的问题,因为系统中可能有上千个线程需要被不断采样,这比跨CPU的采样计数负载还要高几个数量级。

进行Off-CPU分析要求我们对每一步操作可能带来的开销非常敏感。dump下全部的进程事件到用户空间再处理是不可以的,这很容易每秒钟产生几个G的文件。

将这些文件写到文件系统和存储设备并进行处理会消耗更多的CPU。这就是为什么像eBPF这样能够在内核中做归纳的追踪技术的重要性,它们能够减少开销。此外,我们还需要注意反馈循环:追踪器可能会追踪他们自身。

如果对内核调度追踪不熟悉的话,可以先追踪0.1秒,然后慢慢的增加,同时关注追踪对系统CPU使用率、应用程序请求率和应用程序延迟的影响。同时最好关注上下文切换(context switch)的数量,并在上下文切换比例高的机器上更加小心。

BrendanGregg曾在一个8CPULinux 4.15机器上运行Mysql做测试追踪带来的性能影响。这台机器每秒会有102k次上下文切换并且CPU不存在空闲,在这种情况下任何追踪的开销都会变得很明显。测试结果如下:

  • 使用perf追踪调度事件带来了9%的性能降低,而当写入到磁盘时会导致12%的性能降低。这个文件在十秒钟的追踪中收集了224MB的文件,处理这个文件的符号转换导致了持续35秒的13%的性能降低,这相当于有一个CPU被用来做符号转换了。总的来看,10秒钟的perf追踪大约带来了持续45秒的9%到13%的性能损耗。
  • 使用eBPF追踪导致了持续10秒的6%的性能降低,在这之前有一秒钟13%的性能降低用来初始化eBPF,之后有6秒钟13%的性能降低来处理数据,总的开销为持续17秒的6%到13%的性能降低。

当追踪时间延长会发生什么呢?对于eBPF来说,它只是捕获并且转换栈,不会随着时间线性增长。将追踪时间从10秒提升到60秒,eBPF的处理时间也只是从6秒变成了7秒。而对于perf来说,同样的时间增长会使得它的处理时间从35秒变成212秒,因为其需要处理大约六倍的追踪数据。

这里的测试场景和真实负载是一致的吗?在大部分的系统中,往往是不会出现102k的上下文切换的,负载会相应变少;然而Mysql的调用栈大约只有20-40层,真实负载却可能有上百层,这两者相互平衡。

实践

Off-CPU时间

Off-CPU时间指的是进程/线程被阻塞的时间,我们可以通过/proc中的统计信息获取,也可以通过追踪每个阻塞事件来获取。

首先我们可以通过已有的工具来获取相关的时间:

代码语言:javascript
复制
[root@VM-16-2-centos ~]# time ls
real    0m0.102s
user    0m0.007s
sys     0m0.006s

我们使用time来查看一个命令对应的时间占比。可以看到ls命令总共用了0.102秒,其中usersys两个部分加起来一共用了0.013秒,剩下的时间就是所谓Off-CPU的时间,也就是被阻塞的时间。在0.102秒钟有0.089秒是被阻塞的时间,这个占比过于大了一点。

我们可以用bcc工具来追踪某个进程被阻塞的时间:

代码语言:javascript
复制
[root@VM-16-2-centos ~]# /usr/share/bcc/tools/cpudist -O -p 5312
Tracing off-CPU time... Hit Ctrl-C to end.
^C
     usecs               : count     distribution
         0 -> 1          : 19       |**************                          |
         2 -> 3          : 49       |************************************    |
         4 -> 7          : 34       |*************************               |
         8 -> 15         : 30       |**********************                  |
        16 -> 31         : 9        |******                                  |
        32 -> 63         : 7        |*****                                   |
        64 -> 127        : 17       |************                            |
       128 -> 255        : 4        |**                                      |
       256 -> 511        : 2        |*                                       |
       512 -> 1023       : 2        |*                                       |
      1024 -> 2047       : 12       |********                                |
      2048 -> 4095       : 9        |******                                  |
      4096 -> 8191       : 1        |                                        |
      8192 -> 16383      : 4        |**                                      |
     16384 -> 32767      : 54       |****************************************|
     32768 -> 65535      : 14       |**********                              |
     65536 -> 131071     : 11       |********                                |
    131072 -> 262143     : 8        |*****                                   |

这里会展示阻塞时间间隔内出现的次数,例如第一行的含义是0-1微秒的阻塞出现了19次。

cpudist使用了kprobe插桩,插桩了内核函数finish_task_switch

代码语言:javascript
复制
static struct rq *finish_task_switch(struct task_struct *prev)

finish_task_switch会被即将运行的进程调用。eBPF可以对这个函数插桩,并通过bpf_get_current_pid_tgidbpf_ktime_get_ns来得到当前的PID和时间戳。基于这些信息就可以统计出如上的分布图。

以直方图的形式了解Off-CPU的时间对于我们排查问题有一点作用,但并不太大。因为我们只能知道多少时间被阻塞了,而不能知道为什么被阻塞,所以我们需要用到Off-CPU分析。

Off-CPU分析

Off-CPU分析是基于调用栈分析Off-CPU时间的方法,可以帮助我们了解进程/线程为什么被阻塞。我们可以基于如下的准则实现Off-CPU追踪分析:

Off-CPU时应用程序的调用栈不会改变。

这意味着我们只需要获取一次程序的调用栈即可,这要么在进程进入CPU执行时进行,要么在进程切换出CPU时进行。一般在进程离开CPU时进行比较简单,因为我们可以记录时间间隔。下面就是基于调用栈计算Off-CPU时间的方法:

代码语言:javascript
复制
on context switch finish: # 上下文切换结束的时候
 sleeptime[prev_thread_id] = timestamp  # 记录上一个进程A的sleep时间是当前时间
 if !sleeptime[thread_id] # 如果当前进程(切换进来的进程)没有记录或为 就说明当前进程不存在阻塞 return
  return
 delta = timestamp - sleeptime[thread_id]  # 通过现在的时间减去上次被切换出CPU时记录的时间来计算当前进程被阻塞的时间
 totaltime[pid, execname, user stack, kernel stack] += delta # 对当前进程添加阻塞时间
 sleeptime[thread_id] = 0 # 清空当前进程的sleep时间,当当前进程切换走的时候就会由下个进程来记录当前进程的sleep时间

on tracer exit:
 for each key in totaltime:
  print key
  print totaltime[key]

简单来说,就是当上下文切换发生时,对于被切换走的进程A,记录它在当前时间进入了sleep;而对于切换进来的进程B,则基于sleeptime这个map去寻找B被切换走的时间,从而计算差值。如下图所示:

Off-CPU时间计算

在计算时间的同时,追踪工具也可以记录下当前进程相关的上下文信息,如pid、用户栈、内核栈等。

我们可以通过bcc工具包中的offcputime工具来做这样的追踪:

代码语言:javascript
复制
[root@VM-16-2-centos ~]# /usr/share/bcc/tools/offcputime -K -p 996942
Tracing off-CPU time (us) of PID 996942 by kernel stack... Hit Ctrl-C to end.
^C
    finish_task_switch
    __sched_text_start
    schedule
    schedule_hrtimeout_range_clock
    poll_schedule_timeout.constprop.14
    do_select
    core_sys_select
    kern_select
    __x64_sys_select
    do_syscall_64
    entry_SYSCALL_64_after_hwframe
    -                barad_agent (996972)
        350404

这里我们通过-K来追踪了内核栈,我们也可以追踪用户栈:

代码语言:javascript
复制
[root@VM-16-2-centos ~]# /usr/share/bcc/tools/offcputime -U -p 996942
Tracing off-CPU time (us) of PID 996942 by user stack... Hit Ctrl-C to end.
^C
    -                barad_agent (3101808)
        6

具体的使用在这里就不讲解了,大家可以自行了解使用。

注意点

最需要注意的点就是前文提到的开销,除此之外可能还需要注意调度延迟和非自愿上下文切换带来的影响。

调度延迟

Off-CPU堆栈信息中,我们无法看到Off-CPU是否包含等待CPU运行队列的时间,也即调度延迟的存在。如果CPU是饱和运行的,那么即使进程阻塞结束了也可能需要在CPU队列中等待。

针对这种情况,我们可以用额外的事件追踪来讲Off-CPU时间分为阻塞时间和调度延迟,不过在实际情况中,CPU饱和是很容易发现的。

非自愿上下文切换

如果你发现从用户态的调用栈看不出任何切换上下文的原因,可能是因为这是非自愿的上下文切换。这通常在CPU饱和时发生,CPU调度器会让进程/线程们轮流执行,并在时间片结束之后切换它们,即使它们刚好在跑一个很重要的任务。

我们可以通过过滤进程状态为TASK_UNINTERRUPTIBLE 的方式来规避:

代码语言:javascript
复制
/usr/share/bcc/tools/offcputime -p `pgrep -nx mysqld` --state 2

Linux 上,非自愿上下文切换发生在 TASK_RUNNING (0) 状态,而我们通常感兴趣的阻塞事件是 TASK_INTERRUPTIBLE (1) TASK_UNINTERRUPTIBLE (2)offcputime 可以使用--state进行匹配。

Off-CPU火焰图

类似我们平常说的火焰图,Off-CPU分析也可以生成火焰图,生成步骤如下所示:

代码语言:javascript
复制
/usr/share/bcc/tools/offcputime -df -p `pgrep -nx mysqld` 30 > out.stacks
git clone https://github.com/brendangregg/FlameGraph
./FlameGraph/flamegraph.pl --color=io --title="Off-CPU Time Flame Graph" --countname=us < out.stacks > out.svg

生成的火焰图如图所示:

Off-CPU火焰图

On-CPU火焰图类似,我们可以借助其来找热点,然后进行针对性优化。

参考资料

  • Off-CPU Analysis(https://www.brendangregg.com/offcpuanalysis.html)
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-07-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程栩的性能优化笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Off-CPU分析
  • Off-CPU分析方法
    • CPU 采样
      • 应用追踪
        • Off-CPU追踪
          • Off-CPU Sampling
          • Off-CPU开销
          • 实践
            • Off-CPU时间
              • Off-CPU分析
              • 注意点
                • 调度延迟
                  • 非自愿上下文切换
                  • Off-CPU火焰图
                  • 参考资料
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档