对于 Java 应用的诊断, 一直以来都是使用 JDK 自带的 Jps
命令来查看 Java 进程 ID, 最近在一个生产环境的的某个 Kubernetes pod 里面 Jps
一直无法给出对应的进程 ID, 什么输出都没有.
I have no name!@mypod:~$ jps
I have no name!@mypod:~$
初步检查
在 JDK 里面, 很多小工具的命令都搬迁到 jcmd
命令, 如果 jcmd
命令不带任何参数, 其实就相当于 Jps
命令. 于是尝试 jcmd
命令, 结果是一样的.
I have no name!@mypod:~$ jcmd
I have no name!@mypod:~$
以前在 VM 时代, 遇到过类似的问题. 原因是当前运行 bash 的用户和对应的 Java 进程不属于同一个用户, 要切换到对应 Java 进程运行的用户才能看到该进程 ID. 但是这次当前 Bash 的用户和对应 Java 进程的用户本来就是同一个用户.
I have no name!@mypod:~$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
84289 53 1.7 1.5 48923744 6226704 ? Sl Dec02 527:44 /prod/app/jre/bin/java -Xms4096m -Xmx4096m myApp
I have no name!@mypod:~$ id
uid=84289 gid=84677 groups=84677
怀疑当前 pod 的 JDK 文件有啥特别设置(为了减小JDK 大小和不必要文件, 他们把 JDK bin 去掉了, 有需要的移动到了 /jre/bin/ 目录去了). 于是从本地复制一个同版本的 JDK 到目标 pod, 并解压使用, 发现 Jps
和 Jcmd
是一样的结果.
但是这时候发现, 如果给 jcmd
命令一个进程ID, 它就能正常运行, 只是无法发现当前 Pod 的其它 Java 进程.
I have no name!@mypod:~$ jcmd 53 help
53:
The following commands are available:
Compiler.CodeHeap_Analytics
GC.class_histogram
GC.finalizer_info
GC.heap_dump
GC.heap_info
GC.run
GC.run_finalization
Thread.print
...
进一步检查
这时候去 Google 了一下, 发现有人说要给 Jps
命令添加一个参数: -J-Djava.io.tmpdir=/tmp/
告诉它临时文件夹所在的地方. 去试了一下, 发现没有任何改进.
https://stackoverflow.com/questions/3805376/jps-returns-no-output-even-when-java-processes-are-running
于是想使用源码 debug 一下, 于是找到 Jps
的代码: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.jcmd/share/classes/sun/tools/jps/Jps.java
其实就是2个类, 于是合并一下, 并入到一个类里面, 然后复制到到远程 pod 里面, 命名为: JpsTest.java
import java.util.*;
import java.io.*;
import java.net.*;
import sun.jvmstat.monitor.*;
/**
* Application to provide a listing of monitorable java processes.
*
* @author Brian Doherty
* @since 1.5
*/
public class JpsTest {
private static Arguments arguments;
public static void main(String[] args) {
try {
arguments = new Arguments(args);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
Arguments.printUsage(System.err);
System.exit(1);
}
if (arguments.isHelp()) {
Arguments.printUsage(System.err);
System.exit(0);
}
System.err.println(arguments.hostId());
try {
HostIdentifier hostId = arguments.hostId();
MonitoredHost monitoredHost =
MonitoredHost.getMonitoredHost(hostId);
System.err.println(monitoredHost);
// get the set active JVMs on the specified host.
Set<Integer> jvms = monitoredHost.activeVms();
System.err.println(jvms);
for (Integer jvm: jvms) {
StringBuilder output = new StringBuilder();
Throwable lastError = null;
System.err.println(jvm);
int lvmid = jvm;
output.append(String.valueOf(lvmid));
if (arguments.isQuiet()) {
System.out.println(output);
continue;
}
MonitoredVm vm = null;
String vmidString = "//" + lvmid + "?mode=r";
String errorString = null;
try {
// Note: The VM associated with the current VM id may
// no longer be running so these queries may fail. We
// already added the VM id to the output stream above.
// If one of the queries fails, then we try to add a
// reasonable message to indicate that the requested
// info is not available.
errorString = " -- process information unavailable";
VmIdentifier id = new VmIdentifier(vmidString);
vm = monitoredHost.getMonitoredVm(id, 0);
errorString = " -- main class information unavailable";
output.append(' ').append(MonitoredVmUtil.mainClass(vm,
arguments.showLongPaths()));
if (arguments.showMainArgs()) {
errorString = " -- main args information unavailable";
String mainArgs = MonitoredVmUtil.mainArgs(vm);
if (mainArgs != null && mainArgs.length() > 0) {
output.append(' ').append(mainArgs);
}
}
if (arguments.showVmArgs()) {
errorString = " -- jvm args information unavailable";
String jvmArgs = MonitoredVmUtil.jvmArgs(vm);
if (jvmArgs != null && jvmArgs.length() > 0) {
output.append(' ')
.append(
// multi-line args are permitted
jvmArgs.replace("\n", "\\n").replace("\r", "\\r")
);
}
}
if (arguments.showVmFlags()) {
errorString = " -- jvm flags information unavailable";
String jvmFlags = MonitoredVmUtil.jvmFlags(vm);
if (jvmFlags != null && jvmFlags.length() > 0) {
output.append(' ').append(jvmFlags);
}
}
errorString = " -- detach failed";
monitoredHost.detach(vm);
System.out.println(output);
errorString = null;
} catch (URISyntaxException e) {
// unexpected as vmidString is based on a validated hostid
lastError = e;
assert false;
} catch (Exception e) {
lastError = e;
} finally {
if (errorString != null) {
/*
* we ignore most exceptions, as there are race
* conditions where a JVM in 'jvms' may terminate
* before we get a chance to list its information.
* Other errors, such as access and I/O exceptions
* should stop us from iterating over the complete set.
*/
output.append(errorString);
if (arguments.isDebug()) {
if ((lastError != null)
&& (lastError.getMessage() != null)) {
output.append("\n\t");
output.append(lastError.getMessage());
}
}
System.out.println(output);
if (arguments.printStackTrace()) {
lastError.printStackTrace();
}
continue;
}
}
}
} catch (MonitorException e) {
if (e.getMessage() != null) {
System.err.println(e.getMessage());
} else {
Throwable cause = e.getCause();
if ((cause != null) && (cause.getMessage() != null)) {
System.err.println(cause.getMessage());
} else {
e.printStackTrace();
}
}
System.exit(1);
}
}
}
/**
* Class for processing command line arguments and providing method
* level access to the command line arguments.
*
* @author Brian Doherty
* @since 1.5
*/
class Arguments {
private static final boolean debug = Boolean.getBoolean("jps.debug");
private static final boolean printStackTrace = Boolean.getBoolean(
"jps.printStackTrace");
private boolean help;
private boolean quiet;
private boolean longPaths;
private boolean vmArgs;
private boolean vmFlags;
private boolean mainArgs;
private String hostname;
private HostIdentifier hostId;
public static void printUsage(PrintStream ps) {
ps.println("usage: jps [--help]");
ps.println(" jps [-q] [-mlvV] [<hostid>]");
ps.println();
ps.println("Definitions:");
ps.println(" <hostid>: <hostname>[:<port>]");
ps.println(" -? -h --help -help: Print this help message and exit.");
}
public Arguments(String[] args) throws IllegalArgumentException {
int argc = 0;
if (args.length == 1) {
if ((args[0].compareTo("-?") == 0)
|| (args[0].compareTo("-h")== 0)
|| (args[0].compareTo("--help")== 0)
// -help: legacy.
|| (args[0].compareTo("-help")== 0)) {
help = true;
return;
}
}
for (argc = 0; (argc < args.length) && (args[argc].startsWith("-"));
argc++) {
String arg = args[argc];
if (arg.compareTo("-q") == 0) {
quiet = true;
} else if (arg.startsWith("-")) {
for (int j = 1; j < arg.length(); j++) {
switch (arg.charAt(j)) {
case 'm':
mainArgs = true;
break;
case 'l':
longPaths = true;
break;
case 'v':
vmArgs = true;
break;
case 'V':
vmFlags = true;
break;
default:
throw new IllegalArgumentException("illegal argument: "
+ args[argc]);
}
}
} else {
throw new IllegalArgumentException("illegal argument: "
+ args[argc]);
}
}
switch (args.length - argc) {
case 0:
hostname = null;
break;
case 1:
hostname = args[args.length - 1];
break;
default:
throw new IllegalArgumentException("invalid argument count");
}
try {
hostId = new HostIdentifier(hostname);
} catch (URISyntaxException e) {
IllegalArgumentException iae =
new IllegalArgumentException("Malformed Host Identifier: "
+ hostname);
iae.initCause(e);
throw iae;
}
}
public boolean isDebug() {
return debug;
}
public boolean printStackTrace() {
return printStackTrace;
}
public boolean isHelp() {
return help;
}
public boolean isQuiet() {
return quiet;
}
public boolean showLongPaths() {
return longPaths;
}
public boolean showVmArgs() {
return vmArgs;
}
public boolean showVmFlags() {
return vmFlags;
}
public boolean showMainArgs() {
return mainArgs;
}
public String hostname() {
return hostname;
}
public HostIdentifier hostId() {
return hostId;
}
}
当前的 JDK 版本是17, 涉及JDK 内部类: sun.jvmstat.monitor, 所以需要特殊的编译运行命令:
# 编译
/tmp/jdk/bin/javac --add-exports jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED JpsTest.java
# 运行
/tmp/jdk/bin/java --add-exports jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED JpsTest
在上面的类代码里面加入了几处打印信息, 发现对于 //localhost
给出的进程 ID 是一个空列表. 顺着代码去找, 发现这些代码: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/perfdata/monitor/protocol/local/LocalVmManager.java#L129
仔细研究一下, 其实是从 临时文件夹去找特定的文件. 文件夹的命名习惯是: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/perfdata/monitor/protocol/local/PerfDataFile.java#L61C54-L61C65
于是找到一个本地 Java 应用, 然后去对应的临时文件夹去找对应文件夹, 结果找到如下(本地运行在 container 里面的 Neo4j 的应用):
/tmp/hsperfdata_neo4j/7
hsperfdata_neo4j
是一个文件夹, 里面只有 7
这个文件, 它里面有很多该进程的元数据信息(截取部分如下):
�����7�R �8J0sun.rt._sync_Inflations@8J0sun.rt._sync_Deflations@J8sun.rt._sync_ContendedLockAttempts8J0sun.rt._sync_FutileWakeups0J(sun.rt._sync_Parks~8J0sun.rt._sync_Notificationsw8J0sun.rt._sync_MonExtant8J0sun.rt.createVmBeginTime�bs�8J0sun.rt.createVmEndTimebds�8J0sun.rt.vmInitDoneTime�cs�8J0java.threads.started*0J(java.threads.live'8J0java.threads.livePeak(0J(java.threads.daemon8J0sun.rt.safepointSyncTime�wJ0J(sun.rt.safepoints)8J0sun.rt.safepointTime�c�8J0sun.rt.applicationTimeߪ*DE�0J(sun.rt.jvmVersiopAB+sun.rt.jvmCapabilities11000000000000000000000000000000000000000000000000000000000000008J0java.cls.loadedClasses�(8J0java.cls.unloadedClasses@J8java.cls.sharedLoadedClasses�@J8java.cls.sharedUnloadedClasses0J(sun.cls.loadedBytes��G8J0sun.cls.unloadedBytes8J0sun.cls.sharedLoadedBytes�-8J0sun.cls.sharedUnloadedBytes0J(sun.cls.methodBytes��0J(sun.cls.timet�D[8J0sun.cls.classInitTime��*K8J0sun.cls.classInitTime.
......
sel@sun.classloader.parentDelegationTime8J0sun.classloader.findClasses@J8sun.classloader.findClassTimeHJ@sun.urlClassLoader.readClassBytesTime0J(sun.zip.zipFiles�8J0sun.zip.zipFile.openTime
于是想到对应的 pod 里面在 /tmp/目录新建了 /tmp/hsperfdata_test/53
空文件, 并再次运行 jps
. 这次给出了 53
这个进程ID, 只不过没有更多信息.
I have no name!@mypod:~$ jps
53 -- process information unavailable
这时候才发现一开始就忽略了一个信息: 以前运行 Jps
的时候, 至少会出现当前 Jps
本身一行, 一般还有另外一个目标 Java 进程.
回过头来, 我们研究临时文件夹里面的这个文件夹: hsperfdata_neo4j
, 它的后半部分其实是运行进程的用户名, 而一开始, 我们就看到我们在生产环境里面的, bash 给我们一直抱怨 I have no name!
. 正是因为无法确认当前用户名, 只有用户ID, 才导致在临时目录的 hsperfdata_<user name>
无法创建出来, 最终导致 Jps
失效.
于是去问 chatGPT 关于临时文件夹里面的 hsperfdata_ 文件夹:
在 Java 进程运行的系统上,hsperfdata_ 开头的目录是用于存储 Java 虚拟机(JVM)性能数据的临时目录。这些目录和文件由 JVM 创建,用于支持 Java 性能监控和分析工具,例如 jstat。
这些文件的产生时机和机制:
创建时机:
当 JVM 启动时,它会在系统的临时目录中创建一个以 hsperfdata_ 开头的目录。这个目录的名称通常是 hsperfdata_<username>,其中 <username> 是运行 Java 进程的用户的用户名。
在这个目录中,JVM 会为每个 Java 进程创建一个性能数据文件,其文件名为该进程的 PID(进程标识符)。
创建机制:
JVM 使用这些文件来存储与进程相关的性能数据,例如内存使用情况、垃圾回收统计信息和线程状态等。
这些文件是通过共享内存的方式实现的,允许性能监控工具在不干扰 Java 应用程序运行的情况下访问这些数据。
这些文件在 JVM 进程终止时通常会被自动删除。不过,在某些情况下(例如非正常退出),这些文件可能会残留在系统中。
使用这些文件的工具:
jstat: Java 统计监控工具,用于显示 Java 应用程序的各种运行时统计信息。jstat 工具利用这些性能数据文件来获取 JVM 的性能指标。
其他可能使用这些数据文件的工具包括 jps(Java 进程状态工具)和 jconsole(Java 监控与管理控制台)。
这些性能数据文件对于调试和性能分析非常有用,但在某些情况下,可能需要手动清理残留的 hsperfdata_ 目录,特别是在非正常关闭的情况下。
所以, 正如之前 Google 到的那个问答一样, 最终原因是在临时文件夹下面无法创建出 hsperfdata_ 目录导致的. 这是在早期写 Java 性能监控和分析工具的人无法预料到竟然有的系统上面只能看到用户ID, 不能获得用户名.