相关系列文章
早先跟张师傅(挖坑的张师傅)有学习过一些火焰图的东西,在后来的工作和实践中帮助很大,故将这个火焰图的系列梳理,总结并在公司内部分享,现在将文档再加以总结分享输出出来,里面有一些资料是之前网上很多前辈有的,如果相同,纯属借鉴
本系列主要介绍火焰图是什么东东,介绍systemmap原理,以及火焰图原理,如何生成火焰图,以及如何用火焰图定位问题。
我们可以回想下自己现在服务中是如何定位性能问题的,假如我们的线上出现了大部分的接口请求耗时增加,服务性能下降,但是依赖的相关方服务都反馈他们的服务一切正常,这个时候大家会怎么做?
如果有经验的同事这个时刻,可能会从GC,日志,和请求链路上寻找线索,比如dump下内存快照,线程堆栈,和查询多个请求接口之前的共同异常点来判断,但是如果没有经验的同学就有可能抓瞎了,没有思路(就比如我刚入职的时候)
俗话说 “工欲善其事,必先利其器”,我们作为服务的开发者,代码的编写者,如果没有两把刷子,没有金刚钻,也很难把这个活干好。linux中有很多性能检测和调优的工具,火焰图就是基于其中的perf 工具将其采集的数据更加直观的体现出来。
因为我们主要分析的是CPU的火焰图,所以在开篇我们先回顾一下CPU的一些相关知识
我们都知道我们CPU在运行的时候因为多任务"并行"的关系,会进行很多的CPU时间片的切换,也就是说CPU可能执行一会这个任务,然后再执行一会另外一个任务,因为每个任务都是不一样的,所以在切换任务的时候势必会进行线程上下文的切换
1. CPU上下文:CPU执行任务前必须依赖的环境
- CPU寄存器
- 程序计数器
2. 上下文切换的类型
-
进程上下文切换(每次进程上下文的切换需要几十纳秒到数微妙的CPU时间)
- 保存当前进程的内核堆栈和CPU寄存器
- 保存当前进程的虚拟内存和栈
- 加载新进程的内核堆栈和寄存器
- 刷新进程的虚拟内存和用户栈
-
线程上下文切换
-
中断上下文切换(响应硬件的时间,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件)
系统调用的过程中是有上下文切换的,并且发生了两次上下文切换(保存用户的指令位置,保存寄存器的值),一次用户态切换到内核态,一次内核态切回用户态
3. 分析进程对CPU的占用:
-
内核中用就绪队列来维护所有处于可运行状态的进程,可运行状态不包括等待IO、休眠等状态的进程。
-
进程调度器负责从就绪队列中选择处于可运行状态的进程来执行。
-
而所有不处于可运行状态的进程,并不占用CPU资源,这些进程都等待被相关的事件比如网络IO唤醒,唤醒之后的进程更改状态为可运行状态,同时加入到就绪队列中,然后才能被调度器算法选择执行。
因此在一个进程的整个生命周期中,虽然进程看上去一直存在,但是并不是所有时候都在占用CPU,根据占用CPU与否分为on cpu 和 off cpu。
进程因为各种原因(被其他进程抢占、自己调用了sleep系统调用主动进入睡眠状态、等待网络IO等)被剥夺了执行权的时候,首先会调用deactivate_task函数从就绪队列中删除,接下来调用context_switch函数进行进程的上下文切换,这个时候旧的进程失去CPU的执行权,此时正式进入off cpu时间中
其实火焰图就是分析on cpu和off cpu的时间,如果on cpu或者off cpu的时间过长,就要看看为什么进程一直持有cpu,或者进程一直拿不到cpu
4. 做一个小实验: 观测CPU上下文切换
我们想要了解火焰图生成的原理,首先就要知道怎么找到CPU上下文切换的时机,下面用一个小例子演示下
- 测试程序 fake_make.c:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
int get_job_num(int argc, char *const *argv);
void *busy(void *args) {
while (1);
}
int main(int argc, char *argv[]) {
int num = get_job_num(argc, argv);
printf("job num: %d\n", num);
pthread_t threads[num];
int i;
for (i = 0; i < num; ++i) {
pthread_create(&threads[i], NULL, busy, NULL);
}
for (i = 0; i < num; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
int get_job_num(int argc, char *const *argv) {
if (argc <= 1) {
printf("illegal args\nusage ./fake_make -j8\n");
exit(10);
}
int num;
const char *optstring = "j:"; // 有三个选项-abc,其中c选项后有冒号,所以后面必须有参数
int ret;
ret = getopt(argc, argv, optstring);
if (ret != 'j') {
printf("illegal args\nusage ./fake_make -j8\n");
exit(1);
}
num = atoi(optarg);
return num;
}
4.1 Systemtap 观测CPU上下文切换
systemtap 提供的一个探针 (probe)和函数的内置库tapsap,可以在我们编写的 systemtap 脚本中复用他们 ,类似于C语言的库函数,我们可以使用它已经编写好的函数和功能。
我们要观测CPU上下文切换,其实就是在要找到CPU 上任务运行切换的时机,systemtap的内置库stap 中有一个和调度器相关的一些探针,其中就有有关CPU_OFF(被替换下CPU)的探针。
/**
* probe scheduler.cpu_off - Process is about to stop running on a cpu
*
* @name: name of the probe point
* @task_prev: the process leaving the cpu (same as current)
* 保存切换之前的进程
* @task_next: the process replacing current
* 保存切换之前的进程
* @idle: boolean indicating whether current is the idle process
* 表示当前CPU是否空闲
*
* Context: The process leaving the cpu.
*
*/
probe scheduler.cpu_off =
kernel.trace("sched_switch") !,
kernel.function("context_switch")
{
name = "cpu_off"
task_prev = $prev
task_next = $next
idle = __is_idle()
}
参考systemtap的 stap,我们自定义一个探针
global csw_count
global idle_count
global csw_total
probe scheduler.cpu_off {
csw_count[task_prev, task_next]++
csw_total+=1
idle_count+=idle
}
function fmt_task(task_prev, task_next) {
return sprintf("tid(%d)->tid(%d)", task_tid(task_prev), task_tid(task_next))
}
function print_cswtop () {
printf ("%45s %10s\n", "Context switch", "COUNT")
foreach ([task_prev, task_next] in csw_count- limit 5) {
printf("%45s %10d\n", fmt_task(task_prev, task_next), csw_count[task_prev, task_next])
}
printf("%45s %10d\n", "csw_total", csw_total)
printf("%45s %10d\n", "idle", idle_count)
delete csw_total
delete csw_count
delete idle_count
}
probe timer.s($1) {
print_cswtop ()
printf("--------------------------------------------------------------\n")
}
-
编译demo程序fake_make.c
gcc fake_make.c -o fake_make
-
执行 fake_make 程序
./fake_make -j4
-
systemtap 查看上下文切换次数
stap cswstap.stap 1
另外linux中还有别的工具可以观测CPU的上下文切换
- vmstat 观测
vmstat pid
- pidstat 观测
pidstat -w -p pid