jdk诊断命令击穿容器
背景介绍
随着容器的普及,越来越多的服务变成了容器的部署,给部署带来了很大的方便,于此同时也给运维、研发带来了一个诊断的不方便,执行jstack,jmap等诊断命令必须得进入容器中。并且不同的管理方式进入容器的命令还不一样。
docker
docker exec -ti <your-container-name>
k8s
kubectl exec -ti <your-pod-name> -n <your-namespace>
这种操作方式没有物理机直接操作方便。
jdk的改进
这个问题在jdk10做了改进,已经实现了在物理机对容器进程执行诊断命令的方式,代码量并不多,但是先得了解jstack这种诊断命令是如何运行的。
jstack的执行机制
诊断命令执行的机制是同一套代码,总结起来就是:jstack通过domain socket发送诊断命令给进程,进程收到诊断命令后把执行的结果返回。 domain socket的产生,有2种情况。
- 默认是第一次执行命令的时候。命令进程会发送SIGQUIT的信号量。jvm收到这个信号量的时候会产生socket文件。
- 通过参数-XX:+StartAttachListener也可以在jvm启动的时候产生。
socket产生的位置在/tmp/.java_pid${pid}. 通过上述的操作,就可以产生出domain socket,等待命令的发送。 诊断命令行为就比较简单。 第一步是找到socket文件。socket文件有进程号的信息。
private File findSocketFile(int pid, int ns_pid) {
// A process may not exist in the same mount namespace as the caller.
// Instead, attach relative to the target root filesystem as exposed by
// procfs regardless of namespaces.
String root = "/proc/" + pid + "/root/" + tmpdir;
return new File(root, ".java_pid" + ns_pid);
}
第二步就是发起命令。
public InputStream remoteDataDump(Object ... args) throws IOException {
return executeCommand("threaddump", args);
}
jstack对应的命令是threaddump。其他诊断工具只是命令不同,机制是一样的。
看到这里的实现,容器隔离之后,流程里只有一个问题,就是domain socket是文件的形式创建的。2边约定的位置找不到了。导致误差,主要来自容器的namespace,容器里的进程号和外部看到的进程号不一样。/proc/pid/root/tmp 通过物理机可以看到。虽然容器的磁盘地址变化了。但是通过/proc找到的还是一致的。
容器支持
根据上面的分析,可以看到主要的问题点在进程名。那我们只要做名字的映射就可以了。openjdk也是这么做的。从物理机看到的进程信息,找到容器里的进程信息。
cat /proc/23662/status
Name: java
Umask: 0022
NStgid: 23662 1
NSpid: 23662 1
NSsid: 23662 1
这个信息记录在/proc/pid/status下。NSpid就表示物理机进程id是23662,容器内进程是1.所以只要解读这个状态就可以实现映射切换了。
// Get namespace PID from /proc/<PID>/status.
private int getNamespacePID(Path statusPath) {
try (var lines = Files.lines(statusPath)) {
return lines.map(s -> s.split("\\s+"))
.filter(a -> a.length == 3)
.filter(a -> a[0].equals("NSpid:"))
.mapToInt(a -> Integer.valueOf(a[2]))
.findFirst()
.getAsInt();
} catch (IOException | NoSuchElementException e) {
return Integer.valueOf(statusPath.getParent()
.toFile()
.getName());
}
}
这里返回的容器id再传入findSocketFile即可。
场景缺陷
jdk目前的支持满足了大部分的场景。但是实际使用过程中,有以下场景无法满足。
- 平台只有linux。其他环境没有/proc/pid/status的文件路径。
- heapdump,jfr产生的文件依旧在容器里,并且指定的路径是容器里可以访问的。
- 基于jvmti实现的java agent也有问题,例如arthas,他是需要开启端口的,容器需要预留端口。并且arthas实现是相对路径加载agent
转载自:https://juejin.cn/post/7351685099006115891