您现在的位置是:首页 >技术杂谈 >jacoco dump基于k8s的实现网站首页技术杂谈

jacoco dump基于k8s的实现

dray_ 2023-06-21 12:00:03
简介jacoco dump基于k8s的实现

问题描述

总所周知,jacoco 的dump操作如果是使用server模式只需要使用以下命令就能获取到exec文件

java -jar jacococli.jar dump --address 192.169.110.1 --port 6300 --destfile ./jacoco-demo.exec

如果是非k8s的集群,也只需要遍历执行这条命令即可,但是对于k8s服务的处理有有点力所不逮
当我们使用k8s部署服务后,应用实例将会无状态话,用户不再去关心实例的ip,端口等信息,service自动会帮我们做负载均衡等操作,pod不会暴露出ip和端口等信息给集群外部访问,这样对我们的dump操作带来了困难。

问题解决

针对上述问题,网络上也有一些解决方案,最常用的方式是切换jacooc server模式为client模式,这样当jvm关闭时就会将dump 数据写入指定服务的文件里。虽然能从一定程度解决问题,但是这样生成报告的节奏就会被打断,就不能随时生成报告了,这里提供一种解决方式。
首先,我们还是采用server模式,在服务启动时注入

 -javaagent:/jacoco/agent/jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=0.0.0.0

然后,当我们想要去获取exec文件时,可以在pod中执行

java -jar /jacoco/agent/jacococli.jar dump --address 127.0.0.1 --port 36300 --destfile /app/jacoco.exec

然后我们从pod读取文件/app/jacoco.exec写入我们的报告生成服务即可
怎么去pod内部执行shell命令,各种手动都有,这里我们java基于一个k8s的sdk工具fabric8实现

    public List<String> dumpK8sExecData(K8sDumpParam k8sDumpParam) {
        try {
            String dumpCmd = "JAVA_TOOL_OPTIONS="" java -jar /jacoco/agent/jacococli.jar dump --address 127.0.0.1 --port 6300 --destfile /app/jacoco.exec";
            if (k8sDumpParam.getResetFlag()) {
                dumpCmd += " --reset";
            }
            String[] cmd = {"sh", "-c", dumpCmd};
            K8sCmdParam k8sCmdParam = OrikaMapperUtils.map(k8sDumpParam, K8sCmdParam.class);
            k8sCmdParam.setCmd(cmd);
            k8sCmdParam.setExecutor(executor);
            return executeCmd(k8sCmdParam);
        } catch (Exception e) {
            log.error("dump操作失败,失败原因:", e);
            throw new BizException(BizCode.JACOCO_DUMP_ERROR);
        }
    public List<String> executeCmd(K8sCmdParam k8sCmdParam) {
        KubernetesClient client = K8sClientProxy.getOrCreateClient(k8sCmdParam.getKubeConfig());
        if (client == null || k8sCmdParam.getNameSpace() == null || CollectionUtil.isEmpty(k8sCmdParam.getPodList())) {
            throw new BizException(BizCode.JACOCO_DUMP_PARAM_ERROR);
        }
        List<CompletableFuture<String>> priceFuture = k8sCmdParam.getPodList().stream().map(pod ->
                CompletableFuture.supplyAsync(() -> {
                    String filename = "";
                    // 异步操作
                    dumpFileService.podExec(pod, k8sCmdParam.getCmd(), k8sCmdParam.getNameSpace(), client);
                    try {
                        //中间等待文件写入一段时间,再去尝试获取
                        Thread.sleep(1000);
                        filename = dumpFileService.downloadFile(pod, k8sCmdParam.getNameSpace(), client, k8sCmdParam.getTaskWorkspace());
                    } catch (Exception e) {
                        throw new BizException(BizCode.DUMP_FILE_GET_ERROR);
                    }
                    return filename;
                }, k8sCmdParam.getExecutor())
        ).collect(Collectors.toList());
        // 等待所有异步操作完成,多个pod并发执行以上操作,减少dump的时间消耗
        CompletableFuture.allOf(priceFuture.toArray(new CompletableFuture[0])).join();
        return priceFuture.stream().map(CompletableFuture::join).filter(Objects::nonNull).collect(Collectors.toList());
    }

    /**
     * 执行单个pod命令
     *
     * @param podName   pod名字
     * @param cmd       cmd
     * @param namespace 名称空间
     * @param client    客户端
     */
    public void podExec(String podName, String[] cmd, String namespace, KubernetesClient client) {
        try (ExecWatch watch = client.pods().inNamespace(namespace)
                .withName(podName)
                .redirectingOutput()
                .exec(cmd)) {
        }
    }


    /**
     * 获取文件
     *
     * @param podName   pod名字
     * @param namespace 名称空间
     * @param client    客户端
     * @param workspace 工作空间
     */
    @Retryable(value = {IOException.class}, backoff = @Backoff(delay = 1000))
    public String downloadFile(String podName, String namespace, KubernetesClient client, String workspace) throws IOException {
        try (InputStream is = client.pods().inNamespace(namespace)
                .withName(podName)
                .file("/app/jacoco.exec").read()) {
            String execPath = workspace + "/exec/" + podName + "/jacoco.exec";
            FileUtil.writeFromStream(is, execPath);
            return execPath;
        }
    }

这里有两个细节点

  • Thread.sleep(1000)操作,是因为执行dump命令后,我们无法判定exec文件什么时候能在本地生成完成,立马获取就会抛出IO异常,等待一定时间后即可获取到文件,这个时间的等待只是第一层保障,具体等待时间,可以视自己的dump文件大小调整,当然哪怕没调整也没有关系
  • @Retryable(value = {IOException.class}, backoff = @Backoff(delay = 1000))
    这段代码是使用了spring的一个重试框架,当文件获取失败后,默认会重试3次,每次重试间隔1秒,这是获取文件的第二步保障,用户可以通过调整重试次数来减少文件获取失败风险

这里说明下spring Retryable必须在public方法上,而且调用它的方法不能和他处于同一个类,否则不会生效重试。
通过以上手段就可以主动去dump出想要的数据,当然更好的方式是判断exec文件是否存在,或者还在写入中,等写入完成再去获取文件,这个操作也可以通过shell去完成,本文只是提供一种实现方案。
更多jacoco相关姿势可以参考这里

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。