掘金 后端 ( ) • 2024-03-30 09:46

背景介绍

随着容器的普及,越来越多的服务变成了容器的部署,给部署带来了很大的方便,于此同时也给运维、研发带来了一个诊断的不方便,执行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种情况。

  1. 默认是第一次执行命令的时候。命令进程会发送SIGQUIT的信号量。jvm收到这个信号量的时候会产生socket文件。
  2. 通过参数-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目前的支持满足了大部分的场景。但是实际使用过程中,有以下场景无法满足。

  1. 平台只有linux。其他环境没有/proc/pid/status的文件路径。
  2. heapdump,jfr产生的文件依旧在容器里,并且指定的路径是容器里可以访问的。
  3. 基于jvmti实现的java agent也有问题,例如arthas,他是需要开启端口的,容器需要预留端口。并且arthas实现是相对路径加载agent