Kubernetes中Java应用Heap Dump

伴随着微服务及容器化的发展,越来越多的应用运行在kubernetes集群中,运维、调试的问题也随之而来。以Java为例,当线上环境出现内存问题,比如OOM,这时候需要Dump内存进行分析的时候,就会发现对于普通开发人员来说他们没有操作kubernetes集群机器的权限,从而导致,Dump出来的文件无法回传到开发手中进行MAT之类的分析。

本文的解决办法是这样的,当用户需要Dump某个应用实例的时候,只需要在实例终端界面点击一下按钮,后台会自动Dump Heap到OSS上,上传完成后,会将下载的信息展示在列表页,这时候开发人员就可以进行下载了。

具体的操作流程是这样的:
1、检测Pod中是否有JDK TOOLS
2、拷贝Dump工具到对应的Pod中
3、赋予Dump工具可执行权限
4、运行Dump工具

Dump工具会识别Java进程,如果有多个Java进程会Dump进程号小的那一个。

核心代码主要是拷贝Dump工具到对应的Pod中

jmap.go

package dump

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"strings"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/remotecommand"
)

var jmapDumpTool = "jmap-dump-tool"

func init() {
	// jmap-dump-tool 文件名
	if os.Getenv("JMAP_DUMP_TOOL_NAME") != "" {
		jmapDumpTool = os.Getenv("JMAP_DUMP_TOOL_NAME")
	}
	log.Println("JMAP_DUMP_TOOL_NAME:" + jmapDumpTool)
}

func DumpJavaHeap(ctx context.Context, project, app, env, cluster, namespace, podID, container string) error {
   
	// todo 获取k8s信息部分 需要替换成自己的
	ctxName := meta.GetContextName(cluster)
	kclient, err := k8s.GetClient(ctxName)
	if err != nil {
		log.Println(err)
		return errors.New("获取kubernetes client失败!")
	}

	// 获取kube config配置
	config, err := k8s.GetClientConfig(ctxName)
	if err != nil {
		log.Println(err)
		return errors.New("获取kube config失败!")
	}

	pod := new(pod)
	pod.Namespace = namespace
	pod.Name = podID
	pod.ContainerName = container

	log.Println("开始检测目标容器中是否有JDK Tool")
	//检测是否 容器中是否有jps命令
	cmds := []string{"sh", "-c", "jps"}
	req := kclient.CoreV1().RESTClient().
		Post().
		Namespace(pod.Namespace).
		Resource("pods").
		Name(pod.Name).
		SubResource("exec").
		VersionedParams(&corev1.PodExecOptions{
			Container: pod.ContainerName,
			Command:   cmds,
			Stdin:     true,
			Stdout:    true,
			Stderr:    true,
			TTY:       false,
		}, scheme.ParameterCodec)

	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
	if err != nil {
		log.Println(err)
		return &customerr.JavaHeapDumpError{Msg: "检查对应容器中是否有JDK Tools时发生异常!"}
	}
	bufErr := new(bytes.Buffer)
	err = exec.Stream(remotecommand.StreamOptions{
		Stdin:  strings.NewReader(""),
		Stdout: os.Stdout,
		Stderr: bufErr,
		Tty:    false,
	})
	if bufErr.Len() > 0 {
		e := string(bufErr.Bytes())
		if e != "" {
			log.Println(e)
			return &customerr.JavaHeapDumpError{Msg: "请检查对应容器中是否有JDK Tools," + e}
		}
	}

	log.Println("检测完成,目标容器中有JDK Tool")

	srcPath := "/" + jmapDumpTool
	log.Println("srcPath:" + srcPath)
	destPath := "/" + jmapDumpTool
	log.Println("destPath:" + destPath)

	err = pod.copyToPod(ctx, kclient, config, srcPath, destPath)
	if err != nil {
		log.Println(err)
		return &customerr.JavaHeapDumpError{Msg: "dump工具拷贝失败!"}
	}
	log.Println("jmap-dump-tool copy to pod end")
	// 赋予可执行权限
	cmds = []string{"sh", "-c", "chmod +x /" + jmapDumpTool}
	err = pod.Exec(ctx, kclient, config, cmds)
	if err != nil {
		log.Println(err)
		return &customerr.JavaHeapDumpError{Msg: "dump工具赋予可执行权限失败!"}
	}
	log.Println("jmap-dump-tool 已赋予可执行权限")
	go execDump(*pod, jmapDumpTool, project, app, env, kclient, config)
	return nil
}

func execDump(p pod, jmapDumpTool, project, app, env string, client *kubernetes.Clientset, config *rest.Config) {
	// 执行
	c := fmt.Sprintf("/%s %s %s %s", jmapDumpTool, project, app, env)
	cmds := []string{"sh", "-c", c}
	err := p.Exec(context.Background(), client, config, cmds)
	if err != nil {
		log.Println(err)
	}
}

cp.go(参考kubectl cp的实现)

package dump

import (
	"archive/tar"
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path"
	"strings"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/remotecommand"
)

type pod struct {
	Name          string
	Namespace     string
	ContainerName string
}

func (i *pod) copyToPod(ctx context.Context, client *kubernetes.Clientset, config *rest.Config, srcPath string, destPath string) error {
	reader, writer := io.Pipe()

	if destPath != "/" && strings.HasSuffix(string(destPath[len(destPath)-1]), "/") {
		destPath = destPath[:len(destPath)-1]
	}

	if err := checkDestinationIsDir(ctx, client, config, i, destPath); err == nil {
		destPath = destPath + "/" + path.Base(srcPath)
	}

	go func() {
		defer writer.Close()
		err := makeTar(srcPath, destPath, writer)
		if err != nil {
			fmt.Println(err)
		}
	}()

	var cmdArr []string

	cmdArr = []string{"tar", "-xf", "-"}
	destDir := path.Dir(destPath)
	if len(destDir) > 0 {
		cmdArr = append(cmdArr, "-C", destDir)
	}
	//remote shell.
	req := client.CoreV1().RESTClient().
		Post().
		Namespace(i.Namespace).
		Resource("pods").
		Name(i.Name).
		SubResource("exec").
		VersionedParams(&corev1.PodExecOptions{
			Container: i.ContainerName,
			Command:   cmdArr,
			Stdin:     true,
			Stdout:    true,
			Stderr:    true,
			TTY:       false,
		}, scheme.ParameterCodec)

	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
	if err != nil {
		return err
	}

	err = exec.Stream(remotecommand.StreamOptions{
		Stdin:  reader,
		Stdout: os.Stdout,
		Stderr: os.Stderr,
		Tty:    false,
	})
	if err != nil {
		return err
	}
	return nil
}

func checkDestinationIsDir(ctx context.Context, client *kubernetes.Clientset, config *rest.Config, i *pod, destPath string) error {
	return i.Exec(ctx, client, config, []string{"test", "-d", destPath})
}

func makeTar(srcPath, destPath string, writer io.Writer) error {
	// TODO: use compression here?
	tarWriter := tar.NewWriter(writer)
	defer tarWriter.Close()

	srcPath = path.Clean(srcPath)
	destPath = path.Clean(destPath)
	return recursiveTar(path.Dir(srcPath), path.Base(srcPath), path.Dir(destPath), path.Base(destPath), tarWriter)
}

func recursiveTar(srcBase, srcFile, destBase, destFile string, tarWriter *tar.Writer) error {

	filepath := path.Join(srcBase, srcFile)
	stat, err := os.Lstat(filepath)
	if err != nil {
		return err
	}
	if stat.IsDir() {
		files, err := ioutil.ReadDir(filepath)
		if err != nil {
			return err
		}
		if len(files) == 0 {
			//case empty directory
			hdr, _ := tar.FileInfoHeader(stat, filepath)
			hdr.Name = destFile
			if err := tarWriter.WriteHeader(hdr); err != nil {
				return err
			}
		}
		for _, f := range files {
			if err := recursiveTar(srcBase, path.Join(srcFile, f.Name()), destBase, path.Join(destFile, f.Name()), tarWriter); err != nil {
				return err
			}
		}
		return nil
	} else if stat.Mode()&os.ModeSymlink != 0 {
		//case soft link
		hdr, _ := tar.FileInfoHeader(stat, filepath)
		target, err := os.Readlink(filepath)
		if err != nil {
			return err
		}

		hdr.Linkname = target
		hdr.Name = destFile
		if err := tarWriter.WriteHeader(hdr); err != nil {
			return err
		}
	} else {
		//case regular file or other file type like pipe
		hdr, err := tar.FileInfoHeader(stat, filepath)
		if err != nil {
			return err
		}
		hdr.Name = destFile
		err = tarWriter.WriteHeader(hdr)
		if err != nil {
			log.Println(err)
			return err
		}

		f, err := os.Open(filepath)
		if err != nil {
			return err
		}
		defer f.Close()

		if _, err := io.Copy(tarWriter, f); err != nil {
			return err
		}
		return f.Close()
	}
	return nil
}

func (i *pod) Exec(ctx context.Context, client *kubernetes.Clientset, config *rest.Config, cmd []string) error {

	req := client.CoreV1().RESTClient().
		Post().
		Namespace(i.Namespace).
		Resource("pods").
		Name(i.Name).
		SubResource("exec").
		VersionedParams(&corev1.PodExecOptions{
			Container: i.ContainerName,
			Command:   cmd,
			Stdin:     true,
			Stdout:    true,
			Stderr:    true,
			TTY:       false,
		}, scheme.ParameterCodec)

	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
	if err != nil {
		return err
	}

	err = exec.Stream(remotecommand.StreamOptions{
		Stdin:  strings.NewReader(""),
		Stdout: os.Stdout,
		Stderr: os.Stderr,
		Tty:    false,
	})

	if err != nil {
		return err
	}
	return nil
}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: javacore文件是Java虚拟机生成的一种文本文件,它记录了Java应用程序在运行过程的线程堆栈信息、锁信息、内存使用情况等。它通常用于分析Java应用程序的性能问题和调试。 heapdump文件是Java虚拟机生成的一种二进制文件,它记录了Java应用程序在运行过程的堆内存使用情况。它通常用于分析Java应用程序的内存泄漏和内存溢出问题。 ### 回答2: Javacore文件和Heapdump文件是两种不同类型的Java虚拟机(JVM)诊断文件。 Javacore文件是一种详细描述JVM线程状态和系统信息的文件。当JVM遇到致命错误或自然死亡时,它会自动创建Javacore文件。此文件列出了所有线程的状态,包括等待、运行和阻塞状态。还包括实时统计信息、堆栈跟踪和Java虚拟机配置信息等。这些信息对于诊断和修复JVM问题非常有用,例如内存泄漏、死锁、无响应等。 Heapdump文件是另一种JVM诊断文件,用于描述JVM的堆内存信息。当JVM内存达到一定限制或开发人员手动请求时,它会自动创建Heapdump文件。此文件包含了所有当前分配的对象和它们所占用的堆内存大小。它可用于分析内存泄露或内存占用量过高等问题。通过分析Heapdump文件,可以确定哪些对象占用了大量内存并进行相应的调整。 总体来说,Javacore和Heapdump文件都是非常有用的JVM诊断工具,可以用于分析和解决JVM问题。它们为开发人员提供了深入了解JVM运行时信息的机会,并帮助他们识别和消除潜在的性能问题。 ### 回答3: JavaCore和HeapDump文件是Java程序开发常用的两种类型的文件。JavaCore是指Java进程在崩溃后生成的核心转储文件,而HeapDump则是指Java进程的堆内存转储文件。这两种文件在程序调试和性能优化都有重要的作用。 JavaCore文件是Java进程崩溃时生成的核心转储信息,可以用于分析崩溃的原因。JavaCore文件包含了关于崩溃时程序的状态信息,如进程活动的线程、线程的堆栈信息、内存的使用情况等。开发人员可以通过分析JavaCore文件来确定崩溃的根本原因,是由于代码错误还是由于内存溢出等问题。 HeapDump文件则是Java虚拟机在程序运行时生成的堆内存转储信息,用来分析Java程序的内存使用情况,可以帮助开发人员找出程序的内存泄漏和对象的引用循环等问题。因为Java虚拟机的垃圾回收机制会自动回收不再使用的内存,所以开发人员需要及时分析HeapDump文件,找出那些占用大量内存且无法被回收的对象,以避免程序的内存溢出或内存泄漏等问题。 在实际开发JavaCore和HeapDump文件都扮演着非常重要的角色,可以帮助开发人员快速诊断和解决程序出现的问题。因此,开发人员需要学会如何生成和分析这两种类型的文件,并及时修复程序出现的问题,以提高程序的稳定性和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值