文章目录

  • 一、场景介绍
  • 二、基于 JRE 环境运行 Docker 容器的 JVM 调优
  • 三、基于 JDK 环境运行 Docker 容器的 JVM 调优


一、场景介绍

  • 在线上运行的应用程序,如果出现 OOM 等等 JVM 的异常,我们需要通过灾难现场来判断问题代码的所在
  • 如果是传统的 Tomcat 等服务器部署,则可以直接使用服务器的 JDK 环境变量自带的例如:jmap jstack 这些内存分析工具进行问题的分析
  • 如果是通过 Docker 容器部署,想去查看容器内应用的堆栈信息,则需要根据不同容器化启动方式来具体分析

二、基于 JRE 环境运行 Docker 容器的 JVM 调优

  • 场景一:基于 Alpine JRE 基础镜像运行的 Docker 容器
  • 基于该场景的容器,由于采用的是精简版的 JRE,容器内部没有类似 jmap jstack 这些内存分析工具
  • 如果想通过 jmap jstack 这些内存分析工具进行 JVM 调优,我们必须在容器内部添加 JDK 的环境
  • 添加方式可以是在线安装方式或者将 JDK 环境复制到容器内部的方式,根据自己的实际情况来抉择,具体操作步骤如下:
  1. 进入容器内部,查看该容器内部是否拥有类似 jmapjstack 这些内存分析工具
# 进入容器内部
[root@node42 ~]# docker exec -it example-service /bin/sh
# 查看是否拥有调优命令
/opt/java/example-service # jmap
/bin/sh: jmap: not found
/opt/java/example-service # jstack
/bin/sh: jstack: not found
/opt/java/example-service #
  1. 我们可以尝试在容器内部安装 OpenJDK 来解决
# 进入容器内部
docker exec -it container-name /bin/sh

# 更新 apk 源
/opt/java/example-service # echo https://mirrors.aliyun.com/alpine/v3.14/main > /etc/apk/repositories && echo https://mirrors.aliyun.com/alpine/v3.14/community >> /etc/apk/repositories

# 安装 OpenJDK
/opt/java/example-service # apk update && apk upgrade && apk add openjdk8

# 查看 OpenJDK 是否安装成功
/opt/java/example-service # ls -l /usr/lib/jvm/java-1.8-openjdk
total 184
-r--r--r--    1 root     root          1522 Apr 20 15:03 ASSEMBLY_EXCEPTION
-r--r--r--    1 root     root         19274 Apr 20 15:03 LICENSE
-r--r--r--    1 root     root        155003 Apr 20 15:03 THIRD_PARTY_README
drwxr-xr-x    1 root     root          4096 Jul 19 16:05 bin
drwxr-xr-x    3 root     root           132 Jul 19 16:05 include
drwxr-xr-x    1 root     root            95 Jul 19 16:04 jre
drwxr-xr-x    1 root     root           126 Jul 19 16:05 lib
-rw-r--r--    1 root     root            84 Apr 20 15:03 release

# 进入 OpenJDK 二进制目录(由于在线安装的没有配置环境变量,这里就直接在二进制目录进行 JVM 参数排查操作)
/opt/java/example-service # cd  /usr/lib/jvm/java-1.8-openjdk/bin
  1. 通过新增的 OpenJDK 来分析内存信息
/usr/lib/jvm/java-1.8-openjdk/bin # ps -ef
PID   USER     TIME  COMMAND
    1 root      2h16 java -jar ./example-service.jar
  891 root      0:00 sh
 1106 root      0:00 ps -ef
/opt/java/example-service # /usr/lib/jvm/java-1.8-openjdk/bin/./jstack 6
2021-07-19 16:28:09
Full thread dump OpenJDK 64-Bit Server VM (25.212-b04 mixed mode):

"Keep-Alive-Timer" #49 daemon prio=8 os_prio=0 tid=0x00007f76b08ff000 nid=0x62 waiting on condition [0x00007f768925d000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at sun.net.www.http.KeepAliveCache.run(KeepAliveCache.java:172)
	at java.lang.Thread.run(Thread.java:748)
  1. 其它问题分析和解决方案
  • 考虑到以后可能需要多次使用本次下载下来的 OpenJDK,我们可以将容器中的 OpenJDK 拷贝出来,方便后续再次使用时,再次拷贝到容器内部使用
# 将 OpenJDK 拷贝到宿主机
docker cp example-service:/usr/lib/jvm/java-1.8-openjdk .

# 将宿主机的 OpenJDK 拷贝到容器内部
docker cp java-1.8-openjdk example-service:/usr/lib/jvm/
  • 如果分析过程中出现如下问题:
/usr/lib/jvm/java-1.8-openjdk/bin # ps -ef
PID   USER     TIME  COMMAND
    1 root      2h16 java -jar ./example-service.jar
  891 root      0:00 sh
 1106 root      0:00 ps -ef
/usr/lib/jvm/java-1.8-openjdk/bin # ./jstack 1
1: Unable to get pid of LinuxThreads manager thread
/usr/lib/jvm/java-1.8-openjdk/bin # ./jmap -histo 1
1: Unable to get pid of LinuxThreads manager thread
/usr/lib/jvm/java-1.8-openjdk/bin #

请采用 docker --init 的方式启动容器后,再次尝试即可

  • 场景二: 基于宿主机的 JRE 环境运行的 Docker 容器
  1. 基宿主机的 JRE 环境运行的 Docker 容器,同样也没有 jmapjstack 这些内存分析工具
[root@node42 ~]# docker exec -it example-service /bin/sh
/opt/java/example-service # jmap
/bin/sh: jmap: not found
/opt/java/example-service # jstack
/bin/sh: jstack: not found
/opt/java/example-service #
  1. 我们可以通过更换宿主机的 JREJDK 环境,或者进入容器内部安装 OpenJDK 来进行 JVM 调优
  2. 以上方案一可以参考网上资料。搜索关键字 Linux 安装配置 JDK 环境,方案二可以参考本段场景一

三、基于 JDK 环境运行 Docker 容器的 JVM 调优

  • 场景一:基于 Alpine OpenJDK 基础镜像运行的 Docker 容器
  1. 基于 OpenJDK 基础镜像运行的 Docker 容器,本身拥有 jmap jstack 这些内存分析工具
  2. 我们可以直接采用 jmap jstack 这些内存分析工具来进行应用的 JVM 调优
  3. 我们还可以通过阿里开源的 Arthas 来进行 JVM 调优

P.S

  • Arthas 也是通过 JDK 中的 jps 去查找 JAVA 进程的,所以运行时 JAVA 环境变量必须要有 jps 这些命令
  • 很多时候,应用在 Docker 里出现 Arthas 无法工作的问题,是因为应用没有安装 JDK,而是安装了 JRE。如果只安装了 JRE,则会缺少很多 JAVA 的命令行工具和类库,Arthas 也没办法正常工作。下面介绍两种常见的在 Docker 里使用 JDK 的方式
FROM openjdk:8-jdk
# 或者
FROM openjdk:8-jdk-alpine
  • Docker 容器内部的应用的 PID=1 可能会出现无法 Attach,请尽量采用非 PID=1 的方式运行应用,应用 PID=1 为系统默认进程
docker run --init .........
  • 场景二:基于宿主机的 JDK 环境运行的 Docker 容器
  1. 基于宿主机的 JDK 环境变量运行的 Docker 容器,本身拥有 jmap jstack 这些内存分析工具
  2. 我们可以直接采用 jmap jstack 这些内存分析工具来进行应用的 JVM 调优
  3. 我们还可以通过阿里开源的 Arthas 来进行 JVM 调优
  4. 切记在此场景下通过 Arthas 进行调优,请采用和宿主机拥有相同 root 权限的命令启动容器,不然会出现无法 Attach 的情况,不管内部应用的 PID 是否为 1
[INFO] Try to attach process 1
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.io.IOException: Bad pathname
	at java.lang.ClassLoader.findBootstrapClass(Native Method)
	at java.lang.ClassLoader.findBootstrapClassOrNull(ClassLoader.java:1008)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:407)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:405)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:601)
[ERROR] attach fail, targetPid: 1
docker run --privileged=true .........

P.S

  • Docker 容器内部的应用请尽量采用非 PID=1 的方式运行应用,应用 PID=1 为系统默认进程
docker run --init --privileged=true .........