线上CPU飙高不下解决方案

背景

最近运行的平台经常出现CPU非常高,并长时间居高不下,通过arthas-boot工具发现C1编译器线程占用CPU资源久久不释放。

问题排查

查看哪个进程消耗的CPU高

输入top命令,按P进行排序,查看占cpu高的进程pid。

jstack命令

jstack:Java提供的命令。可以查看某个进程的当前线程栈运行情况。根据这个命令的输出可以定位某个进程的所有线程的当前运行状态、运行代码,以及是否死锁等等。

jstack记录进程的堆栈信息

执行jstack -l pid >> err.txt命令,拿到进程的线程dump文件。这个命令会打出这个进程的所有线程的运行堆栈。

jstack统计线程数

jstack -l 28367 | grep 'java.lang.Thread.State' | wc -l

jstack检测cpu高

  1. 查看cpu占用高进程

    top命令按M查看pid

  2. 查看cpu占用高线程

    top -H -p pid命令查看线程占用

  3. 转换线程ID(转换为16进制查询)

    printf "%x\n" tid

  4. 定位cpu占用线程

    jstack pid|grep 转换16进制后的tid -A 30

使用arthas-boot

  1. 启动
    java -jar arthas-boot.jar
  2. 输入需要进入的jar
  3. dashboard查看线程信息

解决方案

说到底,还是设计的太烂。不管是代码还是产品设计,雪崩时没有一片雪花是无辜的。不过,此处我们聊一下技术解决发难

修改代码

出现此类问题该先从代码中找原因,循环调用的处理逻辑,频繁调库的处理逻辑、频繁新增对象、一次性查询数据过多,等等。
此处对数据查询方面进行了优化,添加了Caffeine缓存功能,把频繁查库获取的数据从缓存中取出。Caffeine缓存使用请看之前写的Caffeine缓存使用文章。

关闭JIT分层编译

了解JIT编译原理

  1. 什么是JIT编译?

    编译器在编译过程中通常会考虑很多因素。比如:汇编指令的顺序。假设我们要将两个寄存器的值进行相加,执行这个操作一般只需要一个CPU周期;但是在相加之前需要将数据从内存读到寄存器中,这个操作是需要多个CPU周期的。编译器一般可以做到,先启动数据加载操作,然后执行其它指令,等数据加载完成后,再执行相加操作。由于解释器在解释执行的过程中,每次只能看到一行代码,所以很难生成上述这样的高效指令序列。而编译器可以事先看到所有代码,因此,一般来说,解释性代码比编译性代码要慢。

    java 作为静态语言十分特殊,他需要编译,但并不是在执行之前就编译为本地机器码。Java的实现在解释性和编译性之间进行了折中,Java代码是编译性的,它会被编译成一个平台独立的字节码程序。JVM负责加载、解释、执行这些字节码程序,在这个过程中,还可能会将这些字节码实时编译成目标机器码,以便提升性能。

    所以,在谈到 java的编译机制的时候,其实应该按时期,分为两个部分。一个是 javac指令 将java源码变为 java字节码的静态编译过程。 另一个是 java字节码编译为本地机器码的过程,并且因为这个过程是在程序运行时期完成的所以称之为即时编译(JIT:Just In Time)。

  2. JIT编译类型:C1编译器、C2编译器、分层编译器

    通常我们说即时编译器有两种类型,Client Compiler(C1编译器)和Server Compiler(C2编译器)。这两种编译器最大的区别就是,编译代码的时间点不一样。C1编译器会更早的对代码进行编译,因此在程序刚启动的时候,C1编译器比C2编译器执行的更快,所以C1编译器适用于一些GUI应用,可以缩短应用启动时间。C2编译器会收集更多的信息,然后才对代码进行编译优化,所以从长远角度考虑,C2编译器最终可以产生比C1编译器更优秀的代码,适用于长时间运行的后台接口服务。

    可能大家都有一个困扰,JVM为什么要将编译器分为client和server,为什么不在程序启动时,使用client编译器,在程序运行一段时间后,自动切换为server编译器? 其实,这种技术是存在的,一般称之为 Tiered Compiler(分层编译器)。Java7 和Java 8可以使用选项-XX:+TieredCompilation来打开(-server选项也要打开)。在Java 8中,-XX:+TieredCompilation默认是打开的。

    分层编译将 JVM 的执行状态分为了 5 个层次:

    • 第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译;
    • 第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;
    • 第 2 层:也称为 C1 编译,开启 Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译;
    • 第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;
    • 第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

    在一些特殊情况下,激进优化后的代码并不能有更高的性能。需要进行优化回退,将重新对代码进行解释执行。因此

    1. C2编译器相对于C1编译器更适用于我们系统
    2. 分层编译器是综合考虑C1和C2编译器的优点衍生出的一种进化版本编译器,但是由于我们是纯后台应用,这种衍生优化是否有效未可知。
    3. 分层编译器在一些特殊情况下可能比较激进、不可靠。
  3. 关闭分层编译,启用C2编译器

    JVM启动脚本中添加如下参数

    -XX:-TieredCompilation -server

    改良服务器配置

有钱的话这个问题就好解决多了,果然没钱是万万不能滴。囧。
一个服务提供者cpu占比高,我们可以起三个服务提供者嘛,并发不够数量来凑。
什么?你说网关cpu占比高?那多个网关嘛,搞一个nginx轮训映射到不同网关还不是分分钟的事情。
一个服务器cpu满了?多个服务器嘛。

参考链接

linux cpu飙高的原因
C2 CompilerThread9 长时间占用CPU解决方案
arthas快速入门