在Docker(K8s)环境下,Java应用的 heapdump 和 jstack 快照问题解决方案探析
场景
最近在基于kubernetes程序运行的时候遇到了一个OOM(OutOfMemoryException)异常,尝试着使用jstack
dump 堆栈的时候,发现jstack
无法dump PID=1
的进程。
原因
在Linux系统中,当内核初始化完毕后,会启动一个init进程,这个进程是整个操作系统的的第一个用户进程,所以它的进程ID为1,也就是我们常说的PID1进程。在这之后,所有的用户态进程都是该进程的后代进程,由此我们可以看出,整个系统的用户进程,是一棵由init进程作为根的进程树。
在Dockers中,虽然在容器中被标识为PID 1的进程在操作系统中是一个普通的用户进程,但是因为Linux 内核提供了PID Namespaces功能,如果宿主机的所有用户进程构成了一个完整的树形结构,那么PID Namespaces实际上就是将这个ENTRYPOINT进程(包括它的子进程)从系统的大树上裁剪下来,在容器中,它就是一个以它本身为PID1 的完整树。
在本例中,由于使用的是alpine+Java
镜像以直接启动Java的方式启动docker,因此容器中Application的PID=1,由于jstack
的限制
# jstack 1
1: Unable to open socket file: target process not responding or HotSpot VM not loaded
我们需要另想办法使得image的ENVTRYPOINT(PID1)的进程不能为 java
程序。
解决方案
1. 直接用 docker run
启动
docker run --init my-app
2. 非docker run
启动
2.1 制作基础镜像
在镜像中添加init功能,通过tini
进程来运行java
进程, 因为在项目中不止一个Java项目,所以,我们可以把该功能添加到基础镜像之中,
具体的Dockfile如下:
FROM openjdk:8-alpine
ARG JAR_FILE
ARG PROJECT_NAME
RUN apk update && apk --no-cache tini add bash curl && rm -rf /var/cache/*/*
2.2 使用JIB方式打包
因为在本项目中使用的是JIB 打包,传统的通过jvmFlags
和mainClass
方式设置镜像入口的方式就要有所改变。
具体pom.xml
配置如下:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.2.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<from>
<image>adaptation-engine-local.fpisoj70.americas.net/openjdk:8-alpine-bash</image>
<auth>
<username>xxx</username>
<password>AKCp5btpAnsuZSx1kdE8kwJateLChSQTfNW93JjghAge1u1Vst3cA7QrRiKVL6HsYvQeD6CQw
</password>
</auth>
</from>
<to>
<image>adaptation-engine-local.fpisoj70.americas.net/fp-portal/data-processor</image>
<auth>
<username>xxx</username>
<password>AKCp5btpAnsuZSx1kdE8kwJateLChSQTfNW93JjghAge1u1Vst3cA7QrRiKVL6HsYvQeD6CQw
</password>
</auth>
<tags>
<tag>${docker.image.tag}</tag>
</tags>
</to>
<container>
<entrypoint>
<shell>/sbin/tini</shell>
<option>--</option>
</entrypoint>
<args>
<arg>java</arg>
<arg>-Xms512m</arg>
<arg>-Xmx2048m</arg>
<arg>-Dsun.net.inetaddr.ttl=0</arg>
<arg>-XX:+HeapDumpOnOutOfMemoryError</arg> <!--OOM时快照-->
<arg>-cp</arg>
<arg>/app/resources/:/app/classes/:/app/libs/*</arg>
<arg>com.fcm.oss.fp.DataProcessorApplication</arg>
<arg>--spring.profiles.active=k8s</arg>
</args>
</container>
</configuration>
</plugin>
到此,即可使用maven 构建可dump jstack的docker镜像了。
容器操作
使用docker或者kubernetes 命令进入容器
# Docker
/ $ docker exec -ti <container_id> sh
# K8s
/ $ kubectl exec -ti <pod_id> -- sh
# 查找Java pid
/ $ ps -ef |grep java
1 root 0:08 /sbin/tini -- java -Xms512m -Xmx2048m -Dsun.net.inetaddr.ttl=0 -XX:+HeapDumpOnOutOfMemoryError -cp /app/resources/:/app/classes/:/app/libs/* com.fcm.oss.fp.DataProcessorApplication --spring.profiles.active=k8s
6 root 6h51 java -Xms512m -Xmx2048m -Dsun.net.inetaddr.ttl=0 -XX:+HeapDumpOnOutOfMemoryError -cp /app/resources/:/app/classes/:/app/libs/* com.fcm.oss.fp.DataProcessorApplication --spring.profiles.active=k8s
3463 root 0:00 grep java
# 如上图,pid =6 的进程即为java程序进程
/ $ jstack -l 1 > java_dump.tdump
# 退出docker命令行,拷贝刚才生成的文件
/ $ docker cp <container_id>:/java_pid6.hprof .
/ $ docker cp <container_id>:/java_dump.tdump .
至此,就可以使用Jprofiler 或者是Eclipse 的MAT工具分析快照文件了。