且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Java程序员也应该知道的系统知识系列之CPU

更新时间:2022-04-23 19:24:33

作者:林昊

去年在排查很多java应用的问题时候,看到一些现象是程序员对自己写完的程序所运行的环境了解很少,导致排查问题的时候会比较折腾,因此想到了写这个系列的文章,程序要提供功能给最终用户使用,代码只是其中的一个部分,它还需要依赖jvm、os、服务器硬件、网络、负载均衡等等来共同完成,在这个系列的文章中,将重点关注除jvm外的几个部分,更多的也只是一个科普作用,由于os我使用的都是linux,这个系列的文章中讲到的os也都默认就是linux,这是这个系列的第一篇:CPU。

 

Java程序在运行时和CPU的关系是怎么样的,是怎么去使用和更充分的使用CPU,以及我们有什么办法能够来控制CPU呢,这是这篇文章中关注的几个重点,如果还有你想关注的,但这里也没提到的,以及本文中一些错误的地方,都欢迎回复下,:)

首先需要知道程序运行的机器的CPU状况,至少得了解下有几个CPU,CPU的型号,是不是开启了超线程(Hyper Thread)等,这些信息会对程序的执行性能有不小的影响(一个好一点的型号带来的性能和吞吐量提升是非常明显的,所以在看到各种不提供硬件环境说明的超NB的性能测试报告时,都不需要太惊讶),这些可以通过在机器上cat /proc/cpuinfo来获取,如果是虚拟机的话看到的信息可能会有些奇怪,例如有些虚拟机看到的会是virtual cpu等,这种就只能到宿主机上去查看了。

 

在/proc/cpuinfo里可以看到具体的cpu数量,cpu型号,HT是否开启可以根据physical id和core id来判断,如果看到两个processor的这两个id是一致的,那说明是开启了HT的,开启HT对于支撑高并发的场景而言,基本是有利无弊,一般来说可以提升60%左右(但做不到翻倍的提升,也不太可能做到)。

 

在cpuinfo中还会看到有flags这栏,感兴趣的可以多去了解下,有些会对应用的运行性能有很大的影响,当然,这些也可以通过cpu型号多进行一些了解。

 

在了解了机器的CPU硬件情况外,可以来看看Java程序执行的过程中和CPU的关系,Java程序是一个进程,多线程的方式执行,Java线程和OS线程可以是一对一的关系,也可以是多对一的关系,多对一这个更多的是因为以前的linux对多线程支持不好,所以可以认为现在通常情况下Java线程和OS线程都是一对一的关系。

 

既然是一对一的关系,因此线程具体获取CPU的执行时间片也由OS来控制(虚拟机的话会更复杂一点,这个就得讲到虚拟化的一些知识点了,这个之后再另写吧),默认情况下启动的进程可以使用机器上所有的CPU,但也可以通过taskset命令来控制一个进程可使用的CPU,甚至可以更精细的去控制到某个线程使用的CPU(因为在linux上线程在某种程度上也可以认为是“进程”)。

 

linux是分时调度的操作系统,处于Runnable状态的线程将根据时间片获得调度(具体os是怎么去调度的,怎么去保障多个core的cpu是比较均衡的,建议参看相应的操作系统方面的文章或书),Java程序在执行过程中根据执行的代码将进入不同的线程状态,具体可通过java自带的jstack来查看线程的状态,但jstack看到的线程状态不一定就是对的,例如很多时候会看到epollWait这样的代码对应的线程一直处于Runnable,其实更多的是因为jstack默认情况下只能看到Java栈上的状况,但Java代码执行时很多时候需要依赖native的代码,所以这种时候通常看到的线程状态就有可能是不对的,在比较新的java版本里,是可以通过jstack -m来直接把native stack和Java stack合在一起的,这种情况下就会准确很多。

 

因此可以认为在Java程序方面更多的是通过改变线程的状态来一定程度上干涉对cpu的使用,例如像disruptor这些号称更实时的调度,是靠极短的wait时间或直接的while(true)来保证线程大部分时候处于runnable,同样像sleep等也是很多代码用来主动释放cpu使用的方法。

 

如果想运行的线程获取更多的执行机会,看起来Java中提供的设置线程优先级的方法是比较合用的,但由于这个优先级的生效与否和os有很大的关系,并且不一定合理,所以不用比较好,对于实在是对应用运行很关键的线程,例如在通信密集的java程序中,nio的io处理线程可能非常关键,但通常nio的io处理线程数会很少,而其他线程数很多,如果都处于分时调度的情况下,那么有可能会导致有些情况下io处理线程得到的执行机会不够,对于这种特殊情况可以采用taskset来解决,可能有些同学看到这会想到那jvm是怎么让它的一些线程享有高的执行权限的呢,例如gc线程等,这个在之后讲内存的时候会讲到。

 

在运行的过程中,可以通过top -H来查看运行的java应用的每个线程耗cpu的状况,如果你看到的现象是消耗cpu的线程不断的变化,且每个线程消耗的cpu都不多,那说明应用的运行状况是不错的(当然,这种情况通常可能也有优化空间),反之如果总是看到有个别线程消耗的cpu比例比较高,就需要查查是什么原因了,可通过看到的线程id做十六进制转化,然后对应到jstack出来的线程信息的nid上,看看堆栈具体做的动作,更好的办法则是通过perf这样的工具来查看,但默认版本的perf是无法统计经过c2编译优化后的java代码的cpu消耗的,会统计到?里,所以对查问题帮助就会显得小了很多,这个要支持的话得修改perf和jvm才行。

 

对于Java程序而言,更多的需要考虑的是如何能充分的发挥cpu,而对于多数Java应用而言,通常其他硬件资源都不太会成为瓶颈,所以写的Java程序在随着并发量上升的情况下,应该尽可能去做到跑满cpu。

 

top后按数字1可以看到每个cpu core的消耗状况,主要会有us sy ni id wa hi si st这几个指标,分别对应用户态的消耗 系统内核的消耗 调过ni值的进程的cpu us的消耗 cpu空闲 iowait的消耗 硬中断响应的消耗 软中断响应的消耗 被其他虚拟机借用的消耗,要跑满cpu,不是其中的一个cpu core的id这个值接近0,而应该是所有的cpu core的id都接近0才是合理的,并且对于Java程序而言,应该尽可能让us这个值跑的比较高。

 

有些应用可能会出现即使并发量上涨,但cpu的id也一直降不太下去或者说us上不来,在排查了不是其他硬件资源的瓶颈问题外,通常可能会是以下的一些原因:
1.处理的线程数不够,多数请求处理的过程中并不是全部耗cpu的,例如锁等待、io事件等待等等,这种时候线程会进入blocked或waiting等状态,如运行的线程不够多,就会导致cpu us上不去,现在的java应用从请求进来一直到真正的处理,通常会经历多个线程池,因此在查这类问题的时候一定要清楚整个处理过程,然后看看每个过程的线程数是不是够用,判断是否够用还是比较容易的,jstack如果看到所有的线程都是在执行应用相应的代码,而不是线程池类的wait等时,那基本就说明线程数可能不够用了,这种情况下可以尝试增加线程池的线程数试试,看看cpu的利用率能否上去,但这个地方不太好折腾的是线程数到底设置为多大合适,太小的话cpu不能充分使用,太大则可能造成全部运行起来后存活的线程比较多,导致占用的Java堆内存也增加,从而形成频繁的gc,问题更严重,所以一直以来线程池的线程数设置多大都是个痛苦的问题,基本上得靠经验以及在不同硬件情况下的测试来设置,动态的貌似一直没找到什么好的办法,如果大家有知道的,拜托推荐下,:)

 

2.另外一种状况是线程池的线程数加大了,但仍然没什么作用,这种通常有可能是处理的过程中各并发线程处理过程中串行的部分耗时较长,例如需要同一把锁等场景,这种通过jstack通常也能看出,需要做的则是引入各种高并发的技巧,尤其是无锁数据结构或锁粒度的控制来解决。

通常情况下,如果能把上面两问题都解决好,那么基本上是可以在随着并发量上升的情况下,把cpu也充分利用起来的,如果还碰到了诡异的其中某个cpu的消耗一直比其他的高不少而成为瓶颈的现象,则有可能是网卡中断等的处理造成的,这种可以看看我之前写的一篇关于网卡中断的文章。