Linux环境下Java应用性能分析定位-CPU使用篇
1 CPU热点分析定位背景
CPU资源还是很昂贵的,为了深刻感受到这种昂贵,间下图当前CPU的资源售价:
所以对于程序猿们来说,需要让程序合理高效的使用CPU资源。利用有限的CPU资源来解决完成我们面对的实际问题,这就是为什么我们要尽可能优化程序。
不篇从微观层面展开介绍说明,不做宏观层面的介绍(譬如数据中心级容量监控,管理调度(OpenStack,Kubernates等)以及迁移(手动,自动,冷迁,热迁))。
本篇将以倒推式方法组织目录结构。出现了问题怎么办,以及为什么要这样做,以及后续尽可能如何预防。
- CPU热点占用分析定位
- 什么是CPU
- Linux内核的进程调度
- 反映CPU占用的相关性能指标
- JVM的线程与Linux进程对应关系
- Java并发编程
2 CPU热点占用分析定位
当然也可以借助各种神器辅助定位,譬如JProfiler 等。
3 什么是CPU
CPU:解释计算机指令以及处理计算机软件中的数据。
计算机的性能在很大程度上由CPU的性能决定,而CPU的性能主要体现在其运行程序的速度上。影响运行速度的性能指标包括CPU的工作频率、Cache容量、指令系统和逻辑结构等参数。
主频:主频也叫时钟频率,单位是兆赫(MHz)或千兆赫(GHz),用来表示CPU的运算、处理数据的速度。通常,主频越高,CPU处理数据的速度就越快。
CPU的主频=外频×倍频系数。主频和实际的运算速度存在一定的关系,但并不是一个简单的线性关系。 所以,CPU的主频与CPU实际的运算能力是没有直接关系的,主频表示在CPU内数字脉冲信号震荡的速度。CPU的运算速度还要看CPU的流水线、总线等各方面的性能指标。
外频:外频是CPU的基准频率,单位是MHz。CPU的外频决定着整块主板的运行速度。通俗地说,在台式机中,所说的超频,都是超CPU的外频(当然一般情况下,CPU的倍频都是被锁住的)相信这点是很好理解的。但对于服务器CPU来讲,超频是绝对不允许的。前面说到CPU决定着主板的运行速度,两者是同步运行的,如果把服务器CPU超频了,改变了外频,会产生异步运行,(台式机很多主板都支持异步运行)这样会造成整个服务器系统的不稳定。
总线频率:前端总线(FSB)是将CPU连接到北桥芯片的总线。前端总线(FSB)频率(即总线频率)是直接影响CPU与内存直接数据交换速度。有一条公式可以计算,即数据带宽=(总线频率×数据位宽)/8,数据传输最大带宽取决于所有同时传输的数据的宽度和传输频率。
外频与前端总线(FSB)频率的区别:前端总线的速度指的是数据传输的速度,外频是CPU与主板之间同步运行的速度。也就是说,100MHz外频特指数字脉冲信号在每秒钟震荡一亿次;而100MHz前端总线指的是每秒钟CPU可接受的数据传输量是100MHz×64bit÷8bit/Byte=800MB/s。
缓存大小也是CPU的重要指标之一,而且缓存的结构和大小对CPU速度的影响非常大,CPU内缓存的运行频率极高,一般是和处理器同频运作,工作效率远远大于系统内存和硬盘。实际工作时,CPU往往需要重复读取同样的数据块,而缓存容量的增大,可以大幅度提升CPU内部读取数据的命中率,而不用再到内存或者硬盘上寻找,以此提高系统性能。但是由于CPU芯片面积和成本的因素来考虑,缓存都很小。
L1 Cache(一级缓存)是CPU第一层高速缓存,分为数据缓存和指令缓存。内置的L1高速缓存的容量和结构对CPU的性能影响较大,不过高速缓冲存储器均由静态RAM组成,结构较复杂,在CPU管芯面积不能太大的情况下,L1级高速缓存的容量不可能做得太大。一般服务器CPU的L1缓存的容量通常在32-256KB。
L2 Cache(二级缓存)是CPU的第二层高速缓存,分内部和外部两种芯片。内部的芯片二级缓存运行速度与主频相同,而外部的二级缓存则只有主频的一半。L2高速缓存容量也会影响CPU的性能,原则是越大越好,以前家庭用CPU容量最大的是512KB,笔记本电脑中也可以达到2M,而服务器和工作站上用CPU的
L2高速缓存更高,可以达到8M以上。
L3 Cache(三级缓存),分为两种,早期的是外置,内存延迟,同时提升大数据量计算时处理器的性能。降低内存延迟和提升大数据量计算能力对游戏都很有帮助。而在服务器领域增加L3缓存在性能方面仍然有显著的提升。比方具有较大L3缓存的配置利用物理内存会更有效,故它比较慢的磁盘I/O子系统可以处理更多的数据请求。具有较大L3缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度。
我们常说的核数,个数等之间什么关系呢?
术语 |
概述 |
查看指令 |
CPU个数 (socket ) |
就是实实在在插在主机上看得见摸得着那块CPU硬件 |
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l |
CPU核数 (core ) |
一块物理CPU上能处理数据的芯片组数量。也就是说一个物理CPU上可能会有多个核心,日常中说的双核,四核就是指的CPU核心 |
cat /proc/cpuinfo| grep "cpu cores"| uniq |
超线程 (thread) |
一个CPU核就是一个物理线程,由英特尔开发超线程技术可以把一个物理线程模拟出两个线程来使用,使得单个核心用起来像两个核一样,以充分发挥CPU的性能 |
|
逻辑CPU |
可简单理解为一个处理单元,通常来说,总的逻辑CPU数对应总的CPU核数,但借助超线程技术,一个核用起来像两个核,这时逻辑CPU数就是核心数的两倍了 |
cat /proc/cpuinfo| grep "processor"| wc -l |
vCPU |
对于host来说,kvm虚拟机是一个进程,虚拟机的vcpu都是这个进程衍生出来的线程 不同的vcpu只是不同的线程,而不同的线程是跑在不同的cpu上的 KVM 中,可以指定 socket,core 和 thread 的数目,比如 设置 “-smp 5,sockets=5,cores=1,threads=1”,则 vCPU 的数目为 5*1*1 = 5。客户机看到的是基于 KVM vCPU 的 CPU 核,而 vCPU 作为 QEMU 线程被 Linux 作为普通的线程/轻量级进程调度到物理的 CPU 核上。 用户希望把虚拟机的VCPU绑定在特定物理CPU上,VCPU只在绑定的物理CPU上调度,达到隔离VCPU并提升虚拟机性能的目的。如果没有作VCPU绑定,则虚拟机的VCPU可以在所有物理CPU上调度(libvirt中cputune提供了精细的vcpu绑定设定,可以具体到每个vcpu设置。而且提供vcpu能力的标准化,如quota,period,shares,可以用于实现cpu的Qos) |
宿主机上ps 指令可查看是否KVM虚拟机对应宿主的一个进程,而不同vCPU实际是此进程衍生的不同的线程 |
虚拟化场景下vCPU的亲和性示意图:
实践:
1:Linux中CPU的主频,总线频率,外频,Cache容量 如何查看
2:Linux中CPU个数,核数,是否开启了超线程,超线程个数如何查看?
3:KVM虚拟化下如何设置vCPU亲和性?
4:Docker容器化中如何设置CPU亲和性?
4 反映CPU占用的相关性能指标
Linux CPU 占用率计算,都是根据/proc/stat 文件内容计算而来,在Linux/Unix 下,CPU 利用率分为用户态、系统态和空闲态, 分别表示CPU 处于用户态执行的时间,系统内核执行的时间,和空闲系统进程执行的时间。
原始指标 |
描述 |
备注 |
用户时间(User time) |
表示CPU 执行用户进程的时间,包括nices时间。通常期望用户空间CPU 越高越好。 |
|
系统时间(System time) |
表示CPU 在内核运行时间,包括IRQ 和softirq 时间。系统CPU 占用率高,表明系统某部分存在瓶颈。通常值越低越好。 |
|
等待时间(Waiting time) |
CPI 在等待I/O 操作完成所花费的时间。系统部应该花费大量时间来等待I/O 操作,否则就说明I/O 存在瓶颈。 |
干嘛的 |
空闲时间(Idle time) |
系统处于空闲期,等待进程运行 |
干嘛的 |
Nice时间(Nice time) |
系统调整进程优先级所花费的时间。 |
干嘛的 |
硬中断处理时间(Hard Irq time) |
系统处理硬中断所花费的时间。 |
这个指标干嘛的 |
软中断处理时间(SoftIrq time) |
系统处理软中断中断所花费的时间 |
这个指标干嘛的 |
上述为Linux中CPU相关的原子指标,实际我们所说的占用率等都是统计指标,统计指标的解释和计算如下:
统计指标 |
概述 |
计算公式 |
CPU时间 |
内核中的时间HZ是系统时钟在一秒内固定发出时钟中断的次数。HZ在编译内核前是可以进行配置的,因此通过下述命令就可以查看当前系统的时钟中断频率:cat /boot/config-`uname -r` | grep CONFIG_HZ。tick为系统时钟每“滴答“一次的时间,其值为(1/HZ)秒。也就是连续两次时钟中断之间的时间间隔。jiffies用来计算自系统启动以来tick的次数,也就是说系统时钟每产生一次时钟中断,该变量的值就增加一次 |
user+system+nice+idle+iowait+irq+softirq+Stl |
用户态CPU占用率 |
用户使用CPU的进程包括:cpu运行常规用户进程,cpu运行niced process,cpu运行实时进程。一个Linux进程可以在用户方式下执行,也可以在系统(内核)方式下执行,当一个进程在内核代码中运行时,我们称其处于内核态;当一个进程正在执行用户自己的代码时,我们称其处于用户态,在用户方式下执行时,进程在它自己的应用应用代码中执行,不需要内核资源来进行计算、管理内存或设置变量 |
(User time + Nice time)/CPU时间*100% |
内核态CPU占用率 |
显示了系统方式下所花费cpu时间的百分比,包括内核进程(kprocs)和其他需要访问内核资源的进 程所消耗的cpu资源,系统使用cpu的进程包括:用于系统调用,用于I/O管理(中断和驱动),用于内存管理(paging and swapping),用于进程管理(context switch and process start),如果一个进程需要内核资源,它必须执行一个系统调用,并由此切换到系统方式从而使该资源可用 |
(System time + Hard Irq time +SoftIRQ time)/CPU时间*100% |
空闲率 |
如果没有线程可以执行(运行队列为空),系统分派一个叫做wait的线程,可称为idle kproc。如果ps报告显示这个线程的总计时间较高,这表明存在时间段,其中没有其它线程准备在cpu上运行或等待执行。系统因此大部分时间空闲或等待新任务 |
(Idle time)/CPU 时间*100% |
上述涉及的几个术语:用户态,内核态,中断。这就涉及了Linux内核的一些知识,主要是进程调度相关。
实践:
- 1. Linux中查看CPU使用的原始指标
- 2. Linux中查看CPU使用的统计指标
5 Linux内核的进程调度
由前面介绍可知:CPU是完成具体计算的,但是CPU是硬件,对于上层应用程序来说不直接与CPU交互,而是通过操作系统来完成的,也就是应用程序实际通过与操作系统指令交互,有操作系统内部通过设备驱动与CPU交互完成相关计算任务的。
对于Linux,内部可分三层:硬件-内核-用户空间
- 用户模式+内核模式
一般说来,一个进程在CPU上运行可以有两种运行模式,既可在用户模式下运行,又可在内核模式下运行(即进程分别工作在用户态和内核态,在内核态工作仍旧是这个进程,除非进行了进程的切换)。Linux整个内核就是由各种中断和异常处理程序组成的。即,正常情况下处理器在用户模式执行用户程序,在中断或异常情况下处理器切换到特权模式执行内核程序,处理完中断或异常之后再返回用户模式继续执行用户程序,例如,用户进程A调用了内核系统调用来获取当前的时钟滴答数,在执行用户进程A中的系统调用指令时会保存当前用户进程的IP,CS等当前寄存器状态,然后再跳转到内核空间(即内核代码区域)去执行像应的系统调用函数,获取当前的时钟滴答数。执行完后再通过IRET指令返回到进程A中(就是将进入时保存的信息再复位到相应的寄存器中),再接着从CS:EIP地址开始执行A进程的指令
- Linux调度方式:
Linux内核的调度方式基本上采用“抢占式优先级”方式,即当进程在用户模式下运行时,不管是否自愿,在一定条件下(如时间片用完或等待I/O),核心就可以暂时剥夺其运行而调度其它进程进入运行。但是,一旦进程切换到内核模式下运行,就不受以上限制而一直运行下去,直至又回到用户模式之前才会发生进程调度。Linux系统中的调度策略基本上继承了Unix的以优先级为基础的调度。就是说,核心为系统中每个进程计算出一个优先权,该优先权反映了一个进程获得CPU使用权的资格,即高优先权的进程优先得到运行。核心从进程就绪队列中挑选一个优先权最高的进程,为其分配一个CPU时间片,令其投入运行。在运行过程中,当前进程的优先权随时间递减,这样就实现了“负反馈”作用:经过一段时间之后,原来级别较低的进程就相对“提升”了级别,从而有机会得到运行。当所有进程的优先权都变为0时,就重新计算一次所有进程的优先权。
- Linux调度算法:
Linux执行进程调度时,首先查找所有在就绪队列中的进程,从中选出优先级最高且在内存的一个进程。如果队列中有实时进程,那么实时进程将优先运行。如果最需要运行的进程不是当前进程,那么当前进程就被挂起,并且保存它的现场所涉及的一切机器状态,包括程序计数器和CPU寄存器等,然后为选中的进程恢复运行现场
- CPU上下文切换
在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程 的上下文是存储在进程的私有堆栈中的。占用处理器时,是恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针PC,于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。进程的切换可以用中断技术来实现
由上述可知:CPU的上下文切换时影响内核CPU占用率的关键因素。
6 JVM的线程与Linux进程对应关系
上面说的是Linux的进程调度,对于我们第一章节介绍的是分析Java应用的热点线程,进程和Java线程,这到底怎么回事儿呢?
Java里的线程是由JVM来管理的,它如何对应到操作系统的线程是由JVM的实现来确定的。Linux 2.6上的HotSpot使用了NPTL机制,JVM线程跟内核轻量级进程有一一对应的关系。线程的调度完全交给了操作系统内核,当然jvm还保留一些策略足以影响到其内部的线程调度,举个例子,在linux下,只要一个Thread.run就会调用一个fork产生一个线程。
Java线程在Windows及Linux平台上的实现方式,是内核线程的实现方式。这种方式实现的线程,是直接由操作系统内核支持的——由内核完成线程切换,内核通过操纵调度器(Thread Scheduler)实现线程调度,并将线程任务反映到各个处理器上。内核线程是内核的一个分身。程序一般不直接使用该内核线程,而是使用其高级接口,即轻量级进程(LWP),也即线程
说明:KLT即内核线程Kernel Thread,是“内核分身”。每一个KLT对应到进程P中的某一个轻量级进程LWP(也即线程),期间要经过用户态、内核态的切换,并在Thread Scheduler 下反应到处理器CPU上。)
在程序面上使用内核线程,必然在操作系统上多次来回切换用户态及内核态;另外,因为是一对一的线程模型,LWP的支持数是有限的。
各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。
从上述可知我们所说的Java的线程实际就是用户态的轻量级进程,其映射到内核态的线程。(线程是CPU调度的基本单位)
7 Java并发
- 线程数量
当Java线程数大于cpu线程数,操作系统使用时间片机制,采用线程调度算法,频繁的进行线程切换。活跃线程数为 CPU(核)数时最佳。过少的活跃线程导致 CPU 无法被充分利用,过多的活跃线程导致过大的线程上下文切换开销。线程应该是活跃的,处于 IO 的线程,休眠的线程等均不消耗 CPU。
I/O密集型 (IO-bound) |
I/O bound 指的是系统的CPU效能相对硬盘/内存的效能要好很多,此时,系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存) 的读/写,此时 CPU Loading 不高。 我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。 |
计算密集型 (CPU-bound) |
在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。 在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:
|
在Java中获取CPU核数方法:Runtime.getRuntime().availableProcessors()
- Wait,sleep,notify:阻塞,睡眠,唤醒。
- Java多线程框架:ExecutorService等。
- 线程间通信:信号量,内存队列等
- 线程安全:各种锁,各种安全集合等