对cpu与load的理解及线上问题处理思路解读

前言java

2019双11还有不到2个月就要到来了,你们也都知道服务器在大促期间因为流量的增长势必致使机器的cpu与load变高。所以趁着这个时机正好再好好学习、巩固一下cpu和load的概念,为双11作准备的同时也是增长本身的技能储备。ios

不过cpu和load这块真的仍是很须要积累的,我本身经验尚浅,感受仍是有许多写的不到位与不对的地方,也是但愿若是有错误,你们能够帮助指正。数据库

 

top命令ubuntu

既然说了cpu和load,那总须要监控吧,没有监控就不知道cpu和load,后面的一切也就无从谈起了。服务器

top命令是最多见的查看cpu和load的命令,拿我本身虚拟机上装的ubuntu系统执行一下top命令(默认3秒刷1次,-d可指定刷新时间):网络

作了一张表格比较详细地解释了每一部分的含义,其中重要属性作了标红加粗:多线程

内存与SWAP输出格式是同样的,所以放在了一块儿写。架构

 

cpu如何计算并发

当咱们执行top命令的时候,看到里面的值(主要是cpu和load)值是一直在变的,所以有必要简单了解一下Linux系统中cpu的计算方式。框架

cpu分为系统cpu和进程、线程cpu,系统cpu的统计值位于/proc/stat下(如下的截图未截全):

cpu、cpu0后面的这些数字都和前面的us、sy、ni这些对应,具体哪一个对应哪一个值不重要,感兴趣的能够网上查一下文档。

进程cpu的统计值位于/proc/{pid}/stat下:

线程cpu的统计值位于/proc/{pid}/task/{threadId}/stat下: 

 

这里面的全部值都是从系统启动时间到当前时间的一个值。所以,对于cpu的计算的作法是,采样两个足够短的时间t一、t2:

  • 将t1的全部cpu使用状况求和,获得s1
  • 将t2的全部cpu使用状况求和,获得s2
  • s2 - s1获得这个时间间隔内的全部时间totalCpuTime
  • 第一次的空闲idle1 - 第二次的空闲idle2,获取采样时间内的空闲时间
  • cpu使用率 = 100 * (totalCpuTime - idle) / totalCpuTime

其余时间例如us、sy、ni都是相似的计算方式,总结起来讲,cpu这个值反应的是某个采样时间内的cpu使用状况。所以有时候cpu很高,可是打印线程堆栈出来发现高cpu的线程在查询数据库等待中,不要以为奇怪,由于cpu统计的是采样时间内的数据。

假设top观察某段时间用户空间cpu一直很高,那么意味着这段时间用户的程序一直在占据着cpu作事情。

 

对load的理解

关于load的含义,其实有些文章把它跟行车过桥联系在一块儿是比较恰当和好理解的:

复制代码
一个单核的处理器能够形象得比喻成一条单车道,车辆依次行驶在这条单车道上,前车驶过以后后车才能够行驶。
若是前面没有车辆,那么你顺利经过;若是车辆众多,那么你须要等待前车经过以后才能够经过。 所以,须要些特定的代号表示目前的车流状况,例如: ·等于0.00,表示目前桥面上没有任何的车流。实际上这种状况0.00和1.00之间是相同的,总而言之很通畅,过往的车辆能够丝绝不用等待的经过 ·等于1.00,表示恰好是在这座桥的承受范围内。这种状况不算糟糕,只是车流会有些堵,不过这种状况可能会形成交通愈来愈慢 ·大于1.00,那么说明这座桥已经超出负荷,交通严重的拥堵。那么状况有多糟糕? 例如2.00的状况说明车流已经超出了桥所能承受的一倍,那么将有多余过桥一倍的车辆正在焦急的等待
复制代码

可是比喻终归是比喻,从比喻中咱们了解了,load表示的是系统的一个能力,可是咱们殊不知道什么样的任务会被归到load的计算中。关于具体怎么样的任务会被归到load的计算中,可使用man uptime命令看一下Linux对于load的解释:

大体意思就是说,系统load是处于运行状态或者不可中断状态的进程的平均数(标红部分表示被算入load的内容)。一个处于运行状态的进程表示正在使用cpu或者等待使用cpu,一个不可中断状态的进程表示正在等待IO,例如磁盘IO。load的平均值经过3个时间间隔来展现,就是咱们看到的1分钟、5分钟、15分钟,load值和cpu核数有关,单核cpu的load=1表示系统一直处在负载状态,可是4核cpu的load=1表示系统有75%的空闲。

特别注意,load指的是全部核的平均值,这和cpu的值是有区别的。

还有一个重要的点是,查了资料发现,虽然上面一直强调的是"进程",可是进程中的线程数也是会被看成不一样的进程来计算的,假如一个进程产生1000个线程同时运行,那运行队列的长度就是1000,load average就是1000。

 

请求数和load的关系

以前我本身一直有个误区:当成千上万的请求过来,且在排队的时候,后面的请求得不处处理,load值必然会升高。认真思考以后,这个观点可真是大错特错,所以特别做为一段写一下和你们分享。

以Redis为例,咱们都知道Redis是单线程模型的,这意味着同一时间能够有无数个请求过来,可是同一时间只有一个命令会被处理(图片来源https://www.processon.com/view/5c2ddab0e4b0fa03ce89d14f):

单独的一条线程接到就绪的命令以后,会将命令转给事件分发器,事件分发器根据命令的类型执行对应的命令处理逻辑。因为只有一条线程,只要后面排队的命令足够多到让这条线程一个接一个不停地处理命令,那么load表现就等于1。

整个过程当中,回看load这个值,它和请求数没有任何关系,真正和load相关的是工做线程数量,main线程是工做线程、Timer是工做线程、GC线程也是工做线程,load是以线程/进程做为统计指标,不管请求数是多少,最终都须要线程去处理,而工做线程的处理性能直接决定了最终的load值。

举个例子,假设一个服务中有一个线程池,线程池中线程数量固定为64:

  • 正常来讲一个任务执行时间为10ms,线程拿到任务10ms处理完,很快回归线程池等待下一个任务到来,天然不多有处于运行状态或者等待IO的线程,从一个统计周期来看load表现为很低
  • 某段时间因为系统问题,一个任务10s都处理不完,至关于线程一直在处理任务,在load的统计周期里面就体现出的值=64(不考虑这64条线程外的场景)

所以,总而言之,搞清楚load值和请求数、线程数的关系很是重要,想清楚这些才能正确地进行下一步的工做。

 

load高、cpu高的问题排查思路

首先抛出一个观点:cpu高不是问题,由cpu高引发的load高才是问题,load是判断系统能力指标的依据

为何这么说呢,以单核cpu为例,当咱们平常cpu在20%、30%的时候其实对cpu资源是浪费的,这意味着绝大多数时候cpu并无在作事,理论上来讲一个系统极限cpu利用率能够达到100%,这意味着cpu彻底被利用起来了处理计算密集型任务,例如for循环、md5加密、new对象等等。可是实际不可能出现这种状况,由于应用程序中不消耗cpu的IO不存在是几乎不可能的,例如读取数据库或者读取文件,所以cpu不是越高越好,一般75%是一个须要引发警惕的经验值。

注意前面提到的是"引发警惕",意味着cpu高不必定是问题,可是须要去看一下,尤为是平常的时候,由于一般平常流量不大,cpu是不可能打到这么高的。若是只是普通的代码中确实在处理正常业务那没问题,若是代码里面出现了死循环(例如JDK1.7中经典的HashMap扩容引起的死循环问题),那么几条线程一直占着cpu,最后就会形成load的增高。

在一个Java应用中,排查cpu高的思路一般比较简单,有比较固定的作法:

  • ps -ef | grep java,查询Java应用的进程pid
  • top -H -p pid,查询占用cpu最高的线程pid
  • 将10进制的线程pid转成16进制的线程pid,例如2000=0x7d0
  • jstack 进程pid | grep -A 20 '0x7d0',查找nid匹配的线程,查看堆栈,定位引发高cpu的缘由

网上有不少文章写到这里就停了,实践过程当中并非这样。由于cpu是时间段内的统计值、jstack是一个瞬时堆栈只记录瞬时状态,两个根本不是一个维度的事,所以彻底有可能从打印出来的堆栈行号中看到代码停留在如下地方:

  • 不消耗cpu的网络IO
  • for (int i = 0, size = list.size(); i < size; i++) {...}
  • 调用native方法

若是彻底按照上面那一套步骤作的话碰到这种状况就傻眼了,左思右想半天却不得其解,根本不明白为何这种代码会致使高cpu。针对可能出现的这种状况,实际排查问题的时候jstack建议打印5次至少3次,根据屡次的堆栈内容,再结合相关代码段进行分析,定位高cpu出现的缘由,高cpu多是代码段中某个bug致使的而不是堆栈打印出来的那几行致使的

另外,cpu高的状况还有一种可能的缘由,假如一个4核cpu的服务器咱们看到总的cpu达到了100%+,按1以后观察每一个cpu的us,只有一个达到了90%+,其余都在1%左右(下图只是演示top按1以后的效果并不是真实场景):

这种状况下能够重点考虑是否是频繁FullGC引发的。由于咱们知道FullGC的时候会有Stop The World这个动做,多核cpu的服务器,除了GC线程外,在Stop The World的时候都是会挂起的,直到Stop The World结束。以几种老年代垃圾收集器为例:

  • Serial Old收集器,全程Stop The World
  • Parallel Old收集器,全程Stop The World
  • CMS收集器,它在初始标记与并发标记两个过程当中,为了准确标记出须要回收的对象,都会Stop The World,可是相比前两种大大减小了系统停顿时间

不管如何,当真正发生Stop The World的时候,就会出现GC线程在占用cpu工做而其余线程挂起的状况,天然表现也就为某个cpu的us很高并且他cpu的us很低。

针对FullGC的问题,排查思路一般为:

  • ps -ef | grep java,查询Java应用的进程pid
  • jstat -gcutil pid 1000 1000,每隔1秒打印一次内存状况共打印1000次,观察老年代(O)、MetaSpace(MU)的内存使用率与FullGC次数
  • 确认有频繁的FullGC的发生,查看GC日志,每一个应用GC日志配置的路径不一样
  • jmap -dump:format=b,file=filename pid,保留现场
  • 重启应用,迅速止血,避免引发更大的线上问题
  • dump出来的内容,结合MAT分析工具分析内存状况,排查FullGC出现的缘由

若是FullGC只是发生在老年代区,比较有经验的开发人员仍是容易发现问题的,通常都是一些代码bug引发的。MetaSpace发生的FullGC常常会是一些诡异、隐晦的问题,不少和引入的第三方框架使用不当有关或者就是第三方框架有bug致使的,排查起来就很费时间。

那么频繁FullGC以后最终会致使load如何变化呢?这个我没有验证过和看过具体数据,只是经过理论分析,若是全部线程都是空闲的,只有GC线程在一直作FullGC,那么load最后会趋近于1。可是实际不可能,由于若是没有其余线程在运行,怎么可能致使频繁FullGC呢。因此,在其余线程处理任务的状况下Stop The World以后,cpu挂起,任务得不处处理,更大可能的是load会一直升高。

最后顺便提一句,前面一直在讲FullGC,频繁的YoungGC也是会致使load升高的,以前看到过的一个案例是,Object转xml,xml转Object,代码中每处都new XStream()去进行xml序列化与反序列化,回收速度跟不上new的速度,YoungGC次数陡增。

 

load高、cpu低的问题排查思路

关于load的部分,咱们能够看到会致使load高的几个因素:

  • 线程正在使用cpu
  • 线程正在等待使用cpu
  • 线程在执行不可被打断的IO操做

既然cpu不高,load高,那么线程要么在进行io要么在等待使用cpu。不过对于后者"等待使用cpu"我这里存疑,好比线程池里面10个线程,任务来的很慢,每次只会用到1个线程,那么9个线程都是在等待使用cpu,可是这9个线程明显是不会占据系统资源的,所以我认为天然也不会消耗cpu,因此这个点不考虑。

所以,在cpu不高的状况下假如load高,大几率io高才是罪魁祸首,它致使的是任务一直在跑,迟迟处理不完,线程没法回归线程池中。首先简单讲讲磁盘io,既然wa表示的是磁盘io等待cpu的百分比,那么咱们能够看下wa确认下是否是磁盘io致使的:

若是是,那么按照cpu高一样的方式打印一下堆栈,查看文件io的部分进行分析,排查缘由,例如是否是多线程都在读取本地一个超大的文件到内存。

磁盘io致使的load高,我相信这毕竟是少数,由于Java语言的特色,应用程序更多的高io应当是在处理网络请求,例如:

  • 从数据库中获取数据
  • 从Redis中获取数据
  • 调用Http接口从支付宝获取数据
  • 经过dubbo获取某服务中的数据

针对这种状况,我以为首先咱们应该对整个系统架构的依赖比较熟悉,例如我画一个草图:

对依赖方的调用任何一个出现比较高的耗时都会增长自身系统的load,出现load高的建议排查方式为:

  • 查日志,不管是HBase、MySql、Redis调用仍是经过http、dubbo调用接口,调用超时,拿链接池中的链接超时,一般都会有错误日志抛出来,只要系统里面没有捕获异常以后不打日志直接吞掉通常都能查到相关的异常
  • 对于dubbo、http的调用,建议作好监控埋点,输出接口名、方法入参(控制大小)、是否成功、调用时长等必要参数,有些时候可能没有超时,可是调用2秒、3秒同样会致使load升高,因此这种时候须要查看方法调用时长进行下一步动做

若是上面的步骤仍是没用或者没有对接口调用作埋点,那么仍是万能的打印堆栈吧,连续打印五次十次,看一下每次的堆栈是否大多都指向同一个接口的调用,网络io的话,堆栈的最后几行通常都有at java.net.SocketInputStream.read(SocketInputStream.java:129)

 

Java应用load高的几种缘由总结

前面说了这么多,这里总结一下load高常见的、可能的一些缘由:

  • 死循环或者不合理的大量循环操做,若是不是循环操做,按照现代cpu的处理速度来讲处理一大段代码也就一会会儿的事,基本对能力无消耗
  • 频繁的YoungGC
  • 频繁的FullGC
  • 高磁盘IO
  • 高网络IO

线上遇到问题的时候首先不要慌,由于大部分load高的问题都集中在以上几个点里面,如下分析问题的步骤或许能帮你整理思路:

  • top先查看用户us与空闲us(id)的cpu占比,目的是确认load高是否高cpu起的
  • 若是是高cpu引发的,那么确认一下是否gc引发的,jstat命令 + gc日志基本就能确认
  • gc引发的高cpu直接dump,非gc引发的分析线程堆栈
  • 若是不是高cpu引发的,查看磁盘io占比(wa),若是是,那么打线程堆栈分析是否有大量的文件io
  • 若是不是高cpu引发的,且不是磁盘io致使的,检查各依赖子系统的调用耗时,高耗时的网络调用极可能是罪魁祸首

最后仍是不行,当一筹莫展时,jstack打印堆栈多分析分析吧,或许能灵光一现能找到错误缘由。

 

结语

先有理论,把理论想透了,实战碰到问题的时候才能头脑清楚。

坦白讲,cpu和load高排查是一个很偏实战的事情,这方面我还也有很长一条路须要走,身边在这块经验比我丰富的同事多得很。不少人有问过我,项目比较简单,根本没有这种线上问题须要我去排查怎么办?这个问题只能说,平时多积累、多实战是惟一途径,假如没有实战机会,那么推荐三种方式:

  • 本身经过代码模拟各类异常,例如FullGC、死锁、死循环,而后利用工具去查,可能比较简单,可是万丈高楼平地起,再复杂的东西都是由简单的变化过来的
  • 多上服务器上敲敲top、sar、iostat这些命令,熟记每一个命令的做用及输出参数的含义
  • 去网上找一下其余人处理FullGC、cpu高方法的文章,站在巨人的肩膀上,看看前人走过的路,总结记录一些实用的点

当真的有实战机会来的时候把握住,即便是同事排查的问题,也能够在过后搞清楚问题的前因后果,长此以往天然这方面的能力就会提升上去。

相关文章
相关标签/搜索