linux中如何排查CPU与Load异常问题

74次阅读
没有评论

共计 6113 个字符,预计需要花费 16 分钟才能阅读完成。

这篇文章主要介绍了 linux 中如何排查 CPU 与 Load 异常问题,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让丸趣 TV 小编带着大家一起了解一下。

一、top 命令

既然说了 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 的计算的做法是,采样两个足够短的时间 t1、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 高通常都是由于某段发布的代码有 bug 或者引入某些第三方 jar 而又使用不合理导致的,因此注意首先区分 load 高,是由于 cpu 高导致的还是 io 高导致的,根据不同的场景采取不同定位问题的方式。

当束手无策时,jstack 打印堆栈多分析分析吧,或许能灵光一现能找到错误原因。

感谢你能够认真阅读完这篇文章,希望丸趣 TV 小编分享的“linux 中如何排查 CPU 与 Load 异常问题”这篇文章对大家有帮助,同时也希望大家多多支持丸趣 TV,关注丸趣 TV 行业资讯频道,更多相关知识等着你来学习!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-25发表,共计6113字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)