如何从 CPU 飙升定位到热点方法?

对于系统也是这样的,当你发现了资源异常,你需要继续寻找发生问题的根因,所以作为一名专业的性能测试工程师,你也应当具备顺着表象去找问题根因的能力。这一讲我就以最流行的 Java 语言为例,带你学习如何透过现象看本质。

对于排查问题,不要只满足于掌握一些排查工具或者命令,你应当对被测语言以及运行原理有所了解,这样得出来的结论才可能更全面。

什么是 JVM 呢?
JVM 是 Java Virtual Machine 的缩写,它是一个独立出来的运行环境,通过这样的环境去进行 Java 代码中各种逻辑运行。

图片3.png

从图中你可以看到,一般在底层物理机上会部署多个云服务器,而云服务器上又可以部署多个基于 Docker 的 JVM 节点,这样的部署结构也是比较常用的,既能做到环境的隔离也能节约机器成本

JVM 本身是一个较为庞大的知识体系,对于测试来说,不一定要理解 JVM 特别晦涩的概念,但至少需要了解 JVM 的结构以及运行的机制,你可以认为 JVM 是运行在 Win 或者 Linux 系统上专门运行 Java 的虚拟机,Java 虚拟机直接和操作系统交互。

Java 文件是如何被运行的

比如我们现在写了一个 HelloTester.java,这个 HelloTester.java 就类似一个文本文件,不过这个文件里面包含了符合 Java 语法规范的文本。比如我在 idea 里写一个简单的方法,如下代码所示:

 public class HelloTester {
    public void sayName(String name){
        System.out.println("my name is "+name);
    }
    public  static void main(String[] args){
        HelloTester helloTester=new HelloTester();
        helloTester.sayName("cctester");
}

那我们的JVM 是不认识文本文件的所以它需要编译,让其成为一个会读二进制文件的 HelloTester.class,一般这个文件会产生在工程文件夹下的 Target 当中。

如果 JVM 想要执行这个 .class 文件,我们需要将其装进一个类加载器中,它就像一个搬运工一样,会把所有的 .class 文件全部搬进 JVM 里面来。如下图所示:

图片2.png

对于如上的过程我们再总结概括一下:

  1. Java 文件经过编译后变成 .class 字节码文件;

  2. 字节码文件通过类加载器被搬运到 JVM 中,生成的对象一般会在 JVM 中堆空间运行。

Java 对象又是如何在堆空间运行的?

同样还是根据以上代码示意,我带你看下 Java 对象如何进入堆空间以及在堆空间中运行的。

通过上文可知,编译 HelloTester.java 便会得到 HelloTester.class,执行 class 文件后系统会启动一个 JVM 进程,找到 HelloTester.class 后将类信息加载到 JVM 中。

JVM 找到 mian 方法后就可以执行 main 中的 HelloTester helloTester=new HelloTester(),也就是在 JVM 里创建一个 helloTester 对象,不过此时方法区里面还没有 HelloTester 类的信息,所以 JVM 就会去加载该类:

  • 加载 HelloTester 类后,JVM 在堆内就会为新的 HelloTester 实例进行内存的分配使用;

  • 然后执行 helloTester.sayName(),JVM 根据 HelloTester 对象引用定位到方法区中 HelloTester 类的类型信息的方法表,获得 sayName() 的字节码地址;

  • 最后执行 sayName("cctester")。

以上便是 Java 对象在 JVM 中运行的大体过程,了解了这些基本信息之后,再来了解下堆空间中 Java 运行的线程状态,当程序开始创建线程时,便开始有了生命周期,其实就和人一样,会有“生老病死”几个状态,而对于线程来说会经历六个状态,如下表所示:

图片1.png

我们用一张图来直观地概括下这几个状态的演变:

image (8).png

从字面上来看,NEW、RUNNABLE、TERMINATED 这几个状态比较好理解,但对于 BLOCKED、WAITING、TIMED_WAITING 很多人却分不清楚,我想通过一些实际生活中的例子来帮助你理解。

BLOCKED

先来说下 BLOCKED,比如你去参加面试,可是接待室里面已经有张三正在面试,此时你是线程 T1,张三是线程 T2,而会议室是锁。这时 T1 就被 blocked,而 T2 获取了会议室的锁。

WAITING

接着我们来说 WAITING,你已经进入面试环节,面试官对你的第一轮面试比较满意,让你在会议室等第二轮面试,此时就进入了 WAITING 状态,直到第二轮面试开始你才能结束 WAITING 状态。

TIMED_WAITING

当你结束了所有面试环节,HR 对你说我们一般会在三天内给回复,如果三天内没有回复就不要再等了,此时你就进入 TIMED_WAITING 状态,如果三天内没答复,你可能会看其他机会或者直接入职备选公司了。

这几个例子我想可以帮助你理解 TIMED_WAITING、WATING、BLOCKED 状态。

一般哪些线程状态占用 CPU 呢?

处于 TIMED_WAITING、WATING、BLOCKED 状态的线程是不消耗 CPU 的,而处于RUNNABLE 状态的线程要结合当前线程代码的性质判断是否消耗 CPU:

  • 纯 Java 运算代码,并且未被挂起,是消耗 CPU 的;

  • 网络 IO 操作,在等待数据时是不消耗 CPU 的。

通过如上的学习,你了解了线程的状态,可以知道这个线程是在“休息”还是在“奔跑”。如果很多线程处于“奔跑”状态,必定会消耗相关的硬件资源,反过来理解,如果在性能测试过程中发现资源消耗是不是也能定位到相关的线程,从而发现代码问题呢?当你定位到具体的代码行,是不是可以和研发人员讨论下有没有优化的空间,而不是简单地将机器升级配置去解决问题,所以我将继续沿着如何定位代码问题这条思路为你讲解。

举一个实际例子,我以一个问题为切入点,首先看下面示意代码,可以看出 CPU 占用比较高的线程。

top - 17:41:39 up 168 days,  8:55,  2 users,  load average: 0.71, 0.81, 0.57
Tasks: 155 total,   1 running, 153 sleeping,   0 stopped,   1 zombie
%Cpu(s): 68.4 us,  6.4 sy,  0.0 ni, 23.5 id,  0.0 wa,  0.0 hi,  1.7 si,  0.0 st
KiB Mem :  8010676 total,   326472 free,  6196656 used,  1487548 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  1120940 avail Mem 
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 6937 root      20   0 4778684 518804   6
 140 S 141.9  6.5  17:46.36 java
14643 root      20   0 4639440 821244   2472 S  11.6 10.3   1789:33 java

通过如上示例的第 3 行你可以发现服务器上 CPU 占用蛮高的,空闲值为 23.5%,也就是说占用了 76.5%;再看第 8 行,你可以看到 PID 为 6937 的进程消耗 CPU 为 141.9%。可能你有疑问了,为什么使用率可以超过 100%。这和你的服务器核数有关系,因为这个数值是每个核上该进程消耗的 CPU 之和,会有叠加关系。那你已经知道了消耗 CPU 最高的进程,然后执行如下命令:

[root@JD jmeter_test]# top -Hp 6937
top - 23:20:53 up 168 days, 14:35,  3 users,  load average: 1.33, 0.71, 0.88
Threads: 788 total,   1 running, 787 sleeping,   0 stopped,   0 zombie
%Cpu(s): 75.0 us,  6.2 sy,  0.0 ni, 18.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  8010676 total,   576860 free,  5697612 used,  1736204 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  1616168 avail Mem 
PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
25695 root      20   0 5409224   1.0g   4892 S  6.2 13.2   0:00.09 java

我们可以看到每个线程的使用状态,你可以选择 25695 这个线程号,将 25695 转化为 16 进制,如下所示:

printf "%x\n" 25695
645f

然后通过 jstack 命令定位可能存在问题的方法:

jstack 6937|grep 645f -A 30

通过运行上面的命令可以查看到的内容如下图所示:

截图 (1).png

标红部分就是定位的业务代码,能够比较清晰地知道哪个方法在消耗 CPU 资源。

总结下来,要确定哪些线程状态占用 CPU 至少需要如下步骤:

  • 使用 top 命令找出有问题 Java 进程的 ID;

  • 开启线程显示模式(top -Hp);

  • 按照 CPU 使用率将线程排序(打开 top 后按 P 可以按 CPU 使用降序展示);

  • 记下 Java 进程 ID 及其 CPU 高的线程 ID;

  • 用进程 ID 作为参数,手动转换线程 ID 成十六进制,通过 jstack 去剖析对应的线程栈,以分析问题。

你可以看到,实际过程略显烦琐,可以做成 shell 脚本,这样会比较方便,当然社区也已经有这样的开源脚本供大家使用,点击访问地址

下载完成之后进入 useful-scripts,执行 ./show-busy-java-threads.sh,执行完成后的示意图如下所示:

截图 (2) (1).png

这样的方式是可以看到这台服务上所有导致 CPU 飙升的 Java 方法的,当然直接一键也可以查看指定进程里的 java 方法,非常简单方便,方法如下所示:

 show-busy-java-threads -p <指定的Java进程Id>

  • 48
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值