• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

CPU 性能瓶颈诊断(上)

其它技术 winrains 来源:BryantChang 9个月前 (02-07) 46次浏览

想了半天这篇文章该怎么开头,因为毕竟之前说后面要写两篇C++,然而工作之中面对越来越多的“疑难杂症”让我很崩溃,总是把这些问题推给组里的其他“大牛”也不是个办法,所以决心要用几周的时间好好get一下性能瓶颈诊断的技能。虽然可能过程会很艰苦,但是还是想尽力尝试。这一定会是以后工作中一笔宝贵的财富。同时我也在《极客学院》看了倪鹏飞老师的Linux性能优化实战。讲的真心非常有条理,所以我也把这几篇文章当做是这个课程的课程总结。不管是课程总结也好,还是自己的实战经验也罢,总是希望自己在完成这几篇文章之后,真正能够掌握性能瓶颈诊断的技能。我也按照倪老师的章节划分,按照CPU,内存,I/O以及网络每章总结成一篇文章。希望能和大家一起总结和学习。这篇文章首先我将从整体来简单介绍性能瓶颈,随后我将总结一些CPU性能问题的排查方法。开头有点长了,直接进入正题

性能诊断概述

性能诊断问题其实一直都是各类工程师,尤其是后台工程师(系统开发工程师)的痛点,因为正常的代码在上线前都是经过很严格的测试,包括功能测试以及覆盖一定场景的性能测试。一些表面上比较明显的bug都会在上线前被避免,往往上线之后的一些性能问题都是一些相当隐晦的bug引发的,还有可能是因为其他与这个代码交互的其他系统给我们的系统带来性能问题。再或者说是由于基础设施的一些异常情况造成了系统异常。上面只是提到了问题本身的隐晦和难以排查。然而,这不足成为性能诊断被大多数工程师谈其色变的原因。更加主要的原因个人认为还是下面一个。在我18年的年终总结2018年技术盘点中提到过,每个程序都会运行在特定的系统栈中,在这一点上,倪老师的课程中同样提到了,我在这里直接引用其中的一张图。如下所示。

倪老师将他分成了5层,而个人更习惯分为4层,分别为代码层,运行时环境层,操作系统层以及硬件层。由于系统栈的复杂,可能一个层面的问题最后会通过其他层面指标的异常中获得,例如一个最简单的,Java中的死锁,可能在代码层完全通过静态分析是无法明显的分析出来,而通过jstack这类的运行时环境层的工具很方便的能够分析出。相信很多人都看到过性能大师Brendan Gregg绘制的一张经典性能分析工具的图谱。第一次看到这张图是我研一的时候,不知道大家是什么感受,反正我的第一感觉是胆怯和头大,我勒个去,这都是些什么鬼。。。所以研一那段时间也就用了几个简单的工具就为了完成任务而完成任务,就应付过去了。

直到自己工作后,看到大佬们查问题的方式,他们真的是能够相当熟练的使用那些工具,而我像一个小迷妹一样在边上看着,但是这是工作岗位,作为一个基础平台研发人员,总不能出了问题就交给其他人。因此自己下了这个决心,一定要把这个硬骨头啃下。

之前提到过,一个程序是要运行在特定的系统栈上,程序系统栈在复杂的同时也是互通的,有时候从一个层面不能分析出来的问题可以通过其他层次的一些异常进行解决。这里我们重点关注操作系统层,其实最终程序的执行都是由操作系统来调度,同时分配资源,从资源的角度可以分为CPU,内存,I/O网络,有趣的是,往往一些代码的bug最终都会影响到这些资源的使用。通过操作系统资源的异常再向上层层递进最终能够揪出隐藏在代码里面的“罪魁祸首”。这似乎让我们看到搞定这件事的一丝曙光。在倪老师的课程中也基本是这样的逻辑,也是通过CPU,内存,I/O,网络的异常入手解决问题。所以我的博文也是分别围绕这些异常展开,介绍一些可能引发这些异常的原因以及排查手段和解决措施,从这三个维度来试图建立一个性能问题排查的套路! 这一篇就先从CPU开始讲起。在介绍的过程中可能会涉及到一些相关的操作系统原理的知识点,我会连同这些原理一同进行简单的介绍。由于内容比较多,所以暂时决定分为两篇文来写,第一篇主要介绍常见的CPU指标以及涉及的操作系统原理,第二篇则聚焦于如何获取和观测这些指标,包括普通的程序以及Java程序,因为Java拥有一套比较完善的诊断工具,使用这些工具对Java程序进行Profiling会更加方便。

CPU核心指标

总结了一下倪老师课程中所提到的CPU指标,同时结合自己在公司中经常接触的CPU指标,总结了一些常见的CPU指标,我将分别简单介绍这些CPU指标的定义以及其中涉及的基本原理。

CPU load

  • 含义:平均负载指处于可运行状态不可中断状态的平均进程数,即平均活跃线程数 从这个定义就引出了第一个核心的概念,即进程的状态
    我们在这里首先介绍一下常见的进程状态。

R:Running or Runnable状态,表示进程在CPU的就绪队列中,正在运行或等待运行
D:Disk Sleep缩写,表示不可中断状态睡眠,一般表示进程正在与硬件交互,并且交互过程中不允许其他进程打断(进程不响应异步信号),
举例:
比如执行read系统调用对某个设备文件进行读操作时需要用D来标注,避免进程与设备交互的过程被打断。
执行vfork系统调用后,父进程将进入D状态,知道紫禁城调用exit或exec
Z:Zombie的缩写,指的是实际进程已经结束但父进程并未回收他的资源(PID,进程描述符等)
S:Interrruptible Sleep的缩写,可中断状态睡眠,进程因等待某个事件而被挂起。当事件发生时,进程将会被添加到就绪队列等待调度,进程状态更新为R
I:idle缩写用在不可中断睡眠的内核线程上,硬件交互导致的不可中断进程为D,但某些内核线程可能没有任何负载,使用Idle来区分这种状态,注意D状态会导致平均负载升高,但是I状态的进程不会。(例如等待socket连接,等待信号量)
T:进程停止状态,可以发送SIGSTOP信号来停止进程。这个暂停的进程可以通过发送SIGCONT信号让进城继续运行
X:死亡状态,这个状态指示一个返回状态,在任务列表无法看到这种状态的进程。

下图为进程状态转换图

看完进程状态的转换,再次回到平均负载的概念,下面举一个例子来解释一下平均负载的具体含义。假设平均负载为2

当某台机器有2个CPU,那么则意味着所有的CPU刚好被完全占用

当CPU有4个时,意味着50%的CPU空闲

当CPU只有一个时,则意味着有一半的进程无法获得CPU资源

一般情况下,如果平均负载小于CPU个数,那么这个系统的CPU负载就处在正常范围,如果平均负载高于CPU数量,则需要关注这个系统,可能存在负载过高的情况。而对于平均负载与CPU使用率的关系,二者并不是完全的直接相关,因为平均负载统计的是处于R状态和D状态的进程,而CPU使用率则体现了单位时间内CPU繁忙程度。例如

1、对于CPU密集型应用,进程占用了大量的CPU会导致平均负载升高,这是CPU使用率和平均负载的变化是一致的
2、对于I/O秘籍型应用,进程大部分时间在等待I/O,虽然会导致平均负载升高,然而CPU使用率却不会升高

对于平均负载指标,系统提供了3个不同时间段内的负载,分别为1min,3min,15min,目的是能够让我们能够看到一个系统负载的变化趋势。这里的具体分析方法就不展开说了,详细可见如何理解”平均负载“

CPU上线文切换

提到上下文切换,那么不得不提到CPU的组成以及工作原理,在单核CPU时期,操作系统为了能够支持多个进程同时运行,会将CPU的执行时间分为若干个小段,每个小段成为一个时间片,在某个时间片内CPU服务于某一个进程,这也就是在之前进程状态转换中R状态的进程不仅仅是正在执行的进程,也有可能是等待CPU调度,获取时间片执行的进程。现在CPU发展到了多核,其实与单核的机理是相似的,同样是以不同程序轮流获取CPU执行片的方式运行。那么这里就有一个很容易想到的问题,CPU是如何知道每一个进程运行到了什么地方,好让程序在下一次获取时间片时好继续运行,这里CPU为每个程序准备了特定的程序计数器PC,这个计数器专门负责记录每个程序运行到的位置。

那么CPU做这种轮流分配时间给程序的动作就可以分解为如下几步

1、保存程序A的执行进度
2、从存储器中加载程序B的执行进度
3、开始执行程序B

我们将程序的这些执行信息统一称为程序执行的上下文。注意这里面我为了让大家便于理解,我统一把那些待执行的任务称为程序,程序通常在计算机看来属于”进程“的概念。然而,操作系统可以执行的任务不仅仅只有”进程“,还包括了线程和中断,所以上下文切换的类型也包括了3种。下面将分别展开说明:

  • 进程上下文切换
  • 线程上下文切换
  • 中断上下文切换

进程上下文切换

在介绍进程上下文前,先介绍一种进程执行过程中涉及的另一种CPU上下文切换,我们称之为特权模式切换,在Linux系统中,将进程运行的空间分为内核空间和用户空间,用户空间的进程并不能直接访问内存等硬件设备,必须通过系统调用陷入到内核态中才可以访问这些特权资源。换句话说,一个进程既可以运行在用户空间,又可以运行在内核空间,运行在用户空间的进程的进程成为运行在用户态,相类似的运行在内核空间的进程成为运行在内核态。

系统调用则是用于进程访问特定的特权资源的操作,例如访问内存,磁盘文件等。往往需要进行多次系统调用才能完成一件任务,例如当读取磁盘文件时,就需要以下的步骤

  • 调用open()打开文件
  • 调用read()读取文件内容
  • 调用write()将内容写到标准输出
  • 调用close()关闭文件

在每次系统调用中,CPU需要在陷入内核态之前,首先也需要把用户态的代码执行位置保存在CPU的PC中,然后开始执行内核态代码,执行完毕后,继续执行用户态的其他操作。我们会发先,特权模式切换中,虽然从头到尾执行的都是同一个进程,但同样涉及到了CPU切换。他应该算是进程内的CPU上下文切换。那么它与进程的上下文切换有什么不同呢?

在操作系统中,进程并不是直接使用物理内存来运行程序的,而是使用操作系统分配给其的虚拟内存(在后面介绍内存问题诊断时会详细说明,现在大家有个概念就好),不同的虚拟内存可能会使用一样的物理内存,所以在CPU准备执行其他进程之前,需要把当前进程的虚拟内存一并进行保存。同时还需要保存分配给这个进程的用户栈(一般存储一些方法的入口地址,变量等)。在加载下一个进程的过程中,除了加载该进程的内核态外同时需要刷线进程的虚拟内存以及用户栈。而每次的上下文切换需要经历几十纳秒甚至若干微秒,对于CPU执行而言,这是一个不小的性能开销。在程序大量进行上下文切换的情况下,CPU的时间片大部分都浪费在保存和恢复现场的环节,真正运行程序的时间会减少,这会导致比较平均负载的显著增高。同时在Linux中通过TLB来管理虚拟内存到物理内存的映射关系。当虚拟内存被刷新后,TLB也会被更新,在多核的技术下,这会极大的降低程序的执行效率,因为在现代的多核技术中,缓存(L3 Cache)是被所有核共享的,如下图所示,正常情况下,所有的CPU会先从缓存中获取数据,如果缓存被标记为不可用或访问的数据不存在,则会访问内存。在TLB被更新后,缓存中的TLB数据会失效,各个CPU核需要重新从主存中重新载入。所以某一个程序的进程上下文切换不仅影响了自身的执行效率,同时也影响了其他CPU核上的进程的执行效率。

上面描述了进程上下文切换的性能隐患,那么什么时候会发生进程上下文切换呢?从概念上讲,进程上下文切换是指CPU前后执行了不同的进程,所以一定是在CPU执行了调度算法时会发生切换。前面在介绍CPU进程状态转换时,曾经介绍过,并不是所有满足条件(R)的进程都处于正在运行的状态,CPU为这些进程分配了一个就绪队列,队列中的进程按照优先级和等待CPU的时间排序。每次调度时会挑选优先级高的进程或者等待CPU时间最长的进程进行调度。下面简单梳理下在何种场景下,及昵称会被调度到CPU下运行

1、在前一个进程的CPU时间片耗尽时,CPU将切换上下文,调度等待CPU的进程执行
2、在当前进程系统资源不足(如内存等)时,需要等到资源满足后才能运行,此时当前进程会被挂起,CPU此时会调度其他进程运行
3、当进程主动调用sleep函数将自己挂起,CPU将调度其他进程运行
4、当有更高优先级的进程就绪时,当前线程会被挂起,优先执行高优先级进程。
5、当发生硬件终端,CPU会优先执行内核终端服务程序

说道调度,这里就又需要扩展一下了,中断了一下去看了下《深入了解Linux内核》进程调度这一章。早期的Linux系统调度算法比较简单,每次进程上下文切换时,内核直接扫描就绪队列的链表,计算进程的有衔接,然后选择最佳的进程来运行。然而,这种调度机制存在一个不足,这种调度机制选取最佳进程的开销与就绪队列中的进程数相关。在Linux2.6之后通过对内核的改进,总体来说,每个进程总是按照以下的调度类型被调度

SCHED_FIFO:先进先出的实时进程,当调度程序把CPU分配给进程的时候,他把该进程的进程描述符保留在运行队列链表的当前位置。如果没有其他跟高优先记得实时进程时,进程就会继续使用CPU。
SCHED_RR:时间片轮转的实时进程。当调度程序把CPU分配给进程的时候,将进程描述符放在运行队列链表的末尾。这种策略保证所有相同优先级的进程公平的分配到CPU时间
SHCED_NORMAL:普通的分时进程

Linux在实时进程和普通进程的调度上有着比较大的不同。其中普通进程的调度则通过静态优先级以及动态的优先级来控制,静态优先级决定了进程的最长睡眠时间以及持有的CPU时间片长短。而动态优先级则用于CPU选取新的进程运行时使用的指标,它与静态优先级有一定联系。同时,动态优先级与静态优先级的表达式还可以定义这个程序是交互式进程还是批量进程。而实时进程的调度则依赖的是实时优先级,在实时进程运行过程中,禁止低优先级进程运行。

线程上下文切换

线程是比更小的单位,线程是调度的基本单位,而进程则是资源的持有单位,换句话说内核中的调度任务,调度的对象均为线程,而进程则为线程提供了虚拟内存全局变量。我们可以这样理解进程和线程

1、当进程只有一个线程时,线程相当于进程
2、当进程中拥有多个线程时,这些线程会共享进程中的虚拟内存和全局变量等内容。这些资源在上下文切换过程中不需要修改
3、每个线程同样拥有自己各自的私有数据,例如栈和寄存器等。这些在上下文切换时需要保存。

如此说来,线程上下文切换可以分为以下两种情况,分别为在同一个进程内的线程上下文切换以及不同进程间的线程上下文切换。下面分别进行说明

进程内部的线程上下文切换:线程间的虚拟内存是共享的,在进行切换时虚拟内存这些资源不需要改变只需要切换线程内部的私有寄存器以及私有变量
进程间的线程上下文切换:此时的线程上下问切换与进程的上下文切换相同

中断上下文切换

与进程上线文切换不同,中断上下文切换不涉及进程的用户态,所以在进程转而去调用中断处理程序时,不需要保存和恢复虚拟内存,全局变量等内容,只需要保存中断处理程序需要的一些状态,例如CPU寄存器,内核堆栈,硬件中断参数等。

CPU使用率

一般意义上,我们通常所说的CPU使用率定义如下:CPU使用率,是除了空闲时间外其他时间占总花间的百分比,为了计算CPU使用率,性能工具会隔一段时间去两次值,做差后在计算这段时间内的CPU平均使用率。这里简单介绍一下CPU时间,为了维护CPU时间,Linux通过事先定义节拍率(HZ),触发时间中断并使用全局变量Jiffies记录开机以来的节拍数,每发生一次中断,Jiffies变量+1。但是这个节拍率是内核变量,用户空间的程序无法直接访问,为了方便用户空间进程使用这个变量,系统提供了用户空间的节拍率USER_HZ。

中断

中断是系统用来相应硬件设备请求的一种机制,它会打断进程的正常调度和执行,然后调用内核中的中断处理程序来响应请求。中断处理总体可以分为两个阶段,上半部以及下半部,上半部分是,用于处理硬件请求,称为硬中断,用来快速处理中断,在中断禁止模式下运行。下半部则是软中断,延迟执行上半部分未完成的工作,通常由内核线程执行。下面通过一个例子来说明,例如网卡接收数据,当网卡接收到数据后会通过硬件中断的方式通知内核程序有新数据了。这是对于上半部分,直接将网卡数据读入内存,然后更新寄存器状态,然后直接发送软中断信号,通知内核线程进行后半部操作。在收到信号后,线程从内存中获取数据,按照网络协议栈,对数据进行逐层的解析和处理,最后发送给应用程序。

至此,我介绍了常见的CPU指标以及其中涉及的原理。为了让大家更直观的了解这些指标,我用了一张思维导图进行诠释,如图所示

相信大家通过这篇文章,对这些CPU指标有了初步的了解。在下一篇文章中,我将介绍这些CPU指标的查看方式以及涉及的工具,并用一些实例总结系统CPU问题的常见步骤和套路。

作者:BryantChang

来源:https://bryantchang.github.io/2019/03/09/cpu-profile/


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (1)