Kubernetes中Java应用Heap Dump

Posted 衣舞晨风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kubernetes中Java应用Heap Dump相关的知识,希望对你有一定的参考价值。

Dump Java Heap to OSS

伴随着微服务及容器化的发展,越来越多的应用运行在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.JavaHeapDumpErrorMsg: "检查对应容器中是否有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.JavaHeapDumpErrorMsg: "请检查对应容器中是否有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.JavaHeapDumpErrorMsg: "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.JavaHeapDumpErrorMsg: "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


以上是关于Kubernetes中Java应用Heap Dump的主要内容,如果未能解决你的问题,请参考以下文章

JAVA中Stack和Heap的区别

JMeter内存溢出:java.lang.OutOfMemoryError: Java heap space解决方法(实测有效)

如何在生产环境中调试 java heap OutOfMemory 错误?

Java webstart max-heap-size 导致JVM无法启动

性能监控之常见 Java Heap Dump 方法

数据结构之Heap (Java)